vue-router源码学习笔记
背景
这两天在学习vue-router的源码,通过造轮子,把学到的东西进行巩固吧。
Router类的设计
在使用vue-router的时候我们通常会传入这样的参数:
new Router({base: '/example',mode: 'hash',routes: [{path: '/',name: 'home',component: '<div>home</div>'}]})
复制代码
很显然Router是一个类,传入的参数就是构造函数的参数因此我们的构造函数设计如下:
class Router {constructor (options) {this.base = options.basethis.routes = options.routesthis.mode = options.mode || 'hash'this.init()}init() {}
}
复制代码
hash和history两种模式基类
设计完了Router类,我们还需要根据不同的mode采取不同的处理方式,即hash模式和history模式。但是这两种模式都有一些共同的属性,例如:path,query,params,name,fullPath, route等属性,还有对路由匹配的处理,因此我们将这些通用方法设计成一个基类。
class Base {constructor (router) {this.router = routerthis.current = {path: '/',query: {},params: {},name: '',fullPath: '/',route: {}}}// 这里的taregt就是浏览器中获取path,如:/foo /bartransitionTo(target, cb) {// 通过对比传入的 routes 获取匹配到的 targetRoute 对象const targetRoute = match(target, this.router.routes)this.confirmTransition(targetRoute, () => {this.current.route = targetRoutethis.current.name = targetRoute.namethis.current.path = targetRoute.paththis.current.query = targetRoute.query || getQuery()this.current.fullPath = getFullPath(this.current)cb && cb()})}confirmTransition (route, cb) {cb()}
}function getFullPath ({ path, query = {}, hash = '' }, _stringifyQuery){const stringify = _stringifyQuery || stringifyQueryreturn (path || '/') + stringify(query) + hash
}// 用这些path从而筛选出对应的route
export function match(path, routeMap) {let match = {}if (typeof path === 'string' || path.name === undefined) {for(let route of routeMap) {if (route.path === path || route.path === path.path) {match = routebreak;}}} else {for(let route of routeMap) {if (route.name === path.name) {match = routeif (path.query) {match.query = path.query}break;}}}return match
}
// 获取url中的参数
export function getQuery() {const hash = location.hashconst queryStr = hash.indexOf('?') !== -1 ? hash.substring(hash.indexOf('?') + 1) : ''const queryArray = queryStr ? queryStr.split('&') : []let query = {}queryArray.forEach((q) => {let qArray = q.split('=')query[qArray[0]] = qArray[1]})return query
}function stringifyQuery (obj) {const res = obj ? Object.keys(obj).map(key => {const val = obj[key]if (val === undefined) {return ''}if (val === null) {return key}if (Array.isArray(val)) {const result = []val.forEach(val2 => {if (val2 === undefined) {return}if (val2 === null) {result.push(key)} else {result.push(key + '=' + val2)}})return result.join('&')}return key + '=' + val}).filter(x => x.length > 0).join('&') : nullreturn res ? `?${res}` : ''
}
复制代码
HashHistory实现
class HashHistory extends Base {constructor (router) {super(router)this.ensureSlash()// 监听hashchange事件window.addEventListener('hashchange', () => {this.transitionTo(this.getCurrentLocation())})}push (location) {const targetRoute = match(location, this.router.routes)this.transitionTo(targetRoute, () => {changeUrl(this.current.fullPath.substring(1))})}replaceState (location) {const targetRoute = match(location, this.router.routes)this.transitionTo(targetRoute, () => {changeUrl(this.current.fullPath.substring(1), true)})}ensureSlash () {const path = this.getCurrentLocation()if (path.charAt(0) === '/') {return true}changeUrl(path)return false}// 获取当前路由getCurrentLocation() {const href = window.location.hrefconst index = href.indexOf('#')return index === -1 ? '' : href.slice(index + 1)}
}
// 处理浏览器路由跳转变化
// 这里使用了:
// window.history.replaceState({}, '', url)
// window.history.pushState({}, '', url)
function changeUrl(path, replace) {const href = window.location.hrefconst i = href.indexOf('#')const base = i >= 0 ? href.slice(0, i) : hrefif (replace) {window.history.replaceState({}, '', `${base}#/${path}`)} else {window.history.pushState({}, '', `${base}#/${path}`)}
}
复制代码
window.history.replaceState和window.history.pushState的区别
Html5History实现
class HTML5History extends Base {constructor (router) {super(router)window.addEventListener('popstate', () => {this.transitionTo(getLocation())})}push (location) {const targetRoute = match(location, this.router.routes)this.transitionTo(targetRoute, () => {changeUrl(this.router.base, this.current.fullPath)})}getCurrentLocation () {return getLocation(this.router.base)}
}function getLocation (base = ''){let path = window.location.pathnameif (base && path.indexOf(base) === 0) {path = path.slice(base.length)}return (path || '/') + window.location.search + window.location.hash
}function changeUrl(base, path, replace) {if (replace) {window.history.replaceState({}, '', (base + path).replace(/\/\//g, '/'))} else {window.history.pushState({}, '', (base + path).replace(/\/\//g, '/'))}
}
复制代码
整合Router
前面我聊了Router类,hash模式下如何处理,history模式下如何处理,但是如何与我们的构造函数Router类整合呢?或者说Router中对这两种路由模式如何处理呢?
import { HTML5History } from './history/HTML5History'
import { HashHistory } from './history/HashHistory'
class Router {constructor (options) {// 省略之前代码this.mode = options.mode || 'hash'this.history = this.mode === 'hash' ? new HashHistory(options) :new HTML5History(options)}
}
复制代码
这样就实现了针对不同的mode对应不同的路由解析办法, 但是还有一个问题,我们上面的代码实现了可以动态切换路由,但是切换路由的同时视图如何跟着渲染呢?我们可以利用vue的双向绑定,从而实现当路由切换的同时视图也跟着切换
import { Watcher } from './utils/Watcher'
class Router {constructor (options) {// 省略代码this.init()this.history = this.mode === 'hash' ? new HashHistory(options) :new HTML5History(options)}// 简单搞个render()说明问题就好render () {let iif ((i = this.history.current) && (i = i.route) && (i = i.component)) {document.getElementById(this.container).innerHTML = i}}init () {const history = this.historyobserver.call(this, this.history.current)new Watcher(this.history.current, 'route', this.render.bind(this))history.transitionTo(history.getCurrentLocation())}
}
复制代码
接下来我们来依次实现这几个函数,顺便也学习了vue中的双向绑定
- observer函数实现
class Observer {constructor (value) {this.walk(value)}walk (obj) {Object.keys(obj).forEach(key => {if (typeof obj[key] === 'object') {this.walk(obj[key])}defineRective(obj, key, obj[key])})}
}function defineRective (obj, key, value) {let dep = new Dep()Object.defineProperty(obj, key, {get () {if (dep.target) {dep.add()}return value},set (newVal) {// 注意这两个顺序value = newValdep.notify()}})
}
复制代码
- Dep类的实现
class Dep {constructor () {this.listeners = []}add () {this.listeners.push(Dep.target)}notify () {this.listeners.forEach(listen => listen.update())}
}export function setTarget (target) {Dep.target = target
}export function cleanTarget() {Dep.target = null
}
复制代码
- watcher函数实现
import {setTarget, cleanTarget} from './dep'export class Watcher {constructor (vm, expression, callback) {this.vm = vmthis.callbacks = []this.expression = expressionthis.callbacks.push(callback)this.value = this.getVal()}getVal () {setTarget(this)let val = this.vmthis.expression.split('.').forEach((key) => {val = val[key]})cleanTarget()return val}update () {this.callbacks.forEach((cb) => {cb()})}
}
复制代码
参考资料:
- 前端路由简介以及vue-router实现原理
转载于:https://juejin.im/post/5c863e46e51d453a630fe3d7
vue-router源码学习笔记相关推荐
- Vuex 4源码学习笔记 - Vuex是怎么与Vue结合?(三)
在上一篇笔记中:Vuex源码学习笔记 - Vuex开发运行流程(二) 我们通过运行npm run dev命令来启动webpack,来开发Vuex,并在Vuex的createStore函数中添加了第一个 ...
- Vuex 4源码学习笔记 - 通过dispatch一步步来掌握Vuex整个数据流(五)
在上一篇笔记中:Vuex 4源码学习笔记 - Store 构造函数都干了什么(四) 我们通过查看Store 构造函数的源代码可以看到主要做了三件事情: 初始化一些内部变量以外 执行installMod ...
- Java多线程之JUC包:Semaphore源码学习笔记
若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...
- RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?
RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 文章目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目 ...
- Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)
在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十) 我们学到了通过conventional-changelog来生成项目的Changelog更新日志,通过更新日志 ...
- jquery源码学习笔记三:jQuery工厂剖析
jquery源码学习笔记二:jQuery工厂 jquery源码学习笔记一:总体结构 上两篇说过,query的核心是一个jQuery工厂.其代码如下 function( window, noGlobal ...
- 雷神FFMpeg源码学习笔记
雷神FFMpeg源码学习笔记 文章目录 雷神FFMpeg源码学习笔记 读取编码并依据编码初始化内容结构 每一帧的视频解码处理 读取编码并依据编码初始化内容结构 在开始编解码视频的时候首先第一步需要注册 ...
- Apache log4j-1.2.17源码学习笔记
(1)Apache log4j-1.2.17源码学习笔记 http://blog.csdn.net/zilong_zilong/article/details/78715500 (2)Apache l ...
- PHP Yac cache 源码学习笔记
YAC 源码学习笔记 本文地址 http://blog.csdn.net/fanhengguang_php/article/details/54863955 config.m4 检测系统共享内存支持情 ...
- JDK源码学习笔记——Integer
一.类定义 public final class Integer extends Number implements Comparable<Integer> 二.属性 private fi ...
最新文章
- Windows Server 2008 R2远程设置选项灰色解决方法
- centos 7 配置 到多站点设置
- golang变量的数据类型:整型及其使用细节
- JS~重写alter与confirm,让它们变成fancybox风格
- homebrew install php53
- DELL服务器T410进行系统修复,ibm T410 BIOS修复过程-BIOS维修网站www.biosrepair.com
- canvas笔记-clip裁剪函数的使用及探照灯实例
- docker0: iptables: No chain/target/match by that name.
- lnmp无法删除.user.ini
- 点击百度地图获取位置详细信息(点击获取当前点击位置信息)
- 3-ESP8266 SDK开发基础入门篇--点亮一个灯
- MyBatis使用log4j输出日志
- 《图解算法》学习笔记之递归
- Delphi 实现多国语言
- 算法题目打卡:Ques20201007
- Windows文件换行符转Linux换行符
- Mybatis|CURD|配置详解|注解开发|多点查询|动态sql|缓存
- PHP接入谷歌验证器(Google Authenticator)
- 中断linux命令快捷键_Linux常用快捷键和基本命令
- mySQL的备份及导入
热门文章
- android Json详解
- 第十三周项目4-数组的排序:冒泡排序
- php float浮点型 用round方法比较
- C++primer习题4.9
- 2018年十大云宕机事故盘点:主流无一幸免!
- redis系列:基于redis的分布式锁
- 持续集成篇-- SonarQube代码质量管理平台的安装
- jsp常见获取地址函数之间的不同
- MySQL参数优化辅助工具_mysqltuner.pl
- flash build 4.6 不能debug 报错 C:\WINDOWS\system32\...