博客原文
两种路由模式的基本原理
用过 vue-router 就知道它提供了两种模式,hash
和 history
,通过 Router 构建选项 mode
) 可进行配置。
简单理解 SPA 中的前端路由就是:
- 利用一些现有的 API 实现 url 的改变,但不触发浏览器主动加载新的 url,新的页面展示逻辑全部交给 js 控制;
- 给 history 中添加记录,以实现页面的后退前进;
前端路由下面通过两个例子了解一下这两种路由最基本的原理。
hash 模式
hash 模式是通过修改 URL.hash(即 url 中 #
标识符后的内容)来实现的。
URL.hash 的改变不会触发浏览器加载页面,但会主动修改 history 记录。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router-hash</title>
</head>
<body>
// 页面跳转修改 hash
<a href="#/home">Home</a>
<a href="#/about">About</a>
<div id="app"></div>
</body>
<script>
// 页面加载完后根据 hash 显示页面内容
window.addEventListener('load', () => {app.innerHTML = location.hash.slice(1)
})
// 监听 hash 改变后修改页面显示内容
window.addEventListener('hashchange', () => {app.innerHTML = location.hash.slice(1)
})
</script>
</html>
history 模式
history 模式 主要原理是使用了浏览器的 history API,主要是 history.pushState()
和 history.replaceState()
两个方法。
通过这两种方法修改 history 的 url 记录时,浏览器不会检查并加载新的 url。
这两个方法都是接受三个参数:
- 状态对象 — 可以用来暂存一些数据
- 标题 — 暂无效 一般写空字符串
- url — 新的历史 url 记录
两个方法的区别是 replaceState()
仅修改当前记录而非新建。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router-history</title>
</head>
<body>
// 点击后调用 go 函数跳转路由
<a onclick="go('/home')">Home</a>
<a onclick="go('/about')">About</a>
<div id="app"></div>
</body>
<script>
// 修改 history 记录及页面内容
function go(pathname) {history.pushState(null, '', pathname)
app.innerHTML = pathname
}
// 监听浏览器的前进后退并修改页面内容
window.addEventListener('popstate', () => {app.innerHTML = location.pathname})
</script>
</html>
手写一个超简易的 VueRouter
看源码之前,先通过一个简易的 VueRouter 了解一下整体的结构和逻辑。
class HistoryRoute {constructor() {this.current = null}
}
class VueRouter {constructor(opts) {
this.mode = opts.mode || 'hash';
this.routes = opts.routes || [];
// 创建路由映射表
this.routesMap = this.creatMap(this.routes);
// 记录当前展示的路由
this.history = new HistoryRoute();
this.init();}
// 初始化 动态修改 history.current
init() {if (this.mode === 'hash') {
location.hash ? '': location.hash ='/';
window.addEventListener('load', () => {this.history.current = location.hash.slice(1);
});
window.addEventListener('hashchange', () => {this.history.current = location.hash.slice(1);
});
} else {
location.pathname ? '': location.pathname ='/';
window.addEventListener('load', () => {this.history.current = location.pathname;});
window.addEventListener('popstate', () => {this.history.current = location.pathname;})
}
}
// 创建路由映射表
// {
// '/': HomeComponent,
// '/about': AboutCompontent
// }
creatMap(routes) {return routes.reduce((memo, current) => {memo[current.path] = current.component;
return memo;
}, {})
}
}
// Vue.use(Router) 时触发
VueRouter.install = function (Vue) {
// 定义 $router $route 属性
Object.defineProperty(Vue.prototype, '$router', {get() {return this.$root._router;}
});
Object.defineProperty(Vue.prototype, '$route', {get() {return this.$root._route;}
});
// 全局混入 beforeCreate 钩子函数
Vue.mixin({beforeCreate() {
// 通过 this.$options.router 判断为根实例
if (this.$options && this.$options.router) {
this._router = this.$options.router;
// 给 this 对象定义一个响应式 属性
// https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
Vue.util.defineReactive(this, '_route', this._router.history);
}
},
});
// 渲染函数 & JSX https://cn.vuejs.org/v2/guide/render-function.html
// 注册全局组件 router-link
// 默认渲染为 a 标签
Vue.component('router-link', {
props: {
to: String,
tag: String
},
methods: {handleClick() {
const mode = this._self.$root._router.mode;
location.href = mode === 'hash' ? `#${this.to}` : this.to;
}
},
render: function (h) {
const mode = this._self.$root._router.mode;
const tag = this.tag || 'a';
return (
<tag
on-click={tag !== 'a' && this.handleClick}
href={mode === 'hash' ? `#${this.to}` : this.to }
>
{this.$slots.default}
</tag>
);
}
});
// 注册全局组件 router-view
// 根据 history.current 从 路由映射表中获取到对象组件并渲染
Vue.component('router-view', {render: function (h) {
const current = this._self.$root._route.current;
const routeMap = this._self.$root._router.routesMap;
return h(routeMap[current]);
}
});
}
export default VueRouter;
120 行代码实现了最最基本的 VueRouter,梳理一下整体的结构:
- 首先是一个 VueRouter 类,并有一个 install 方法,install 方法会在使用
Vue.use(VueRouter)
时被调用; - install 方法中添加了 Vue 原型对象上的两个属性
$router
$route
及router-view
router-link
两个全局组件; - VueRouter 类中通过构造函数处理传入的参数,生成路由映射表并调用 init 方法;
- init 方法中监听路由变化并改变 history.current;
- history.current 表示当前路由,在 install 中被定义为了一个响应式属性
_route
,在该属性被改变后会触发依赖中的响应已达到渲染router-view
中的组件;
现在已经对 VueRouter 有了一个基本的认识了,再去看源码时就容易了一些。
浅尝源码
下面是我自己看 VueRouter 源码并结合一些文章的学习笔记。
阅读源码的过程中写了一些方便理解的注释,希望给大家阅读源码带来帮助,github:vue-router 源码
vue-router 的 src 目录如下,下面依次来分析这里主要的几个文件的作用。
index.js
VueRouter 的入口文件,主要作用是定义并导出了一个 VueRouter 类。
下面是 index.js
源码,删除了 flow 相关的类型定义及函数的具体实现,先来看一下整体的结构和每部分的功能。
// ...
// 导出 VueRouter 类
export default class VueRouter {
// 定义类的静态属性及方法
// install 用于 vue 的插件机制,Vue.use 时会自动调用 install 方法
static install: () => void;
static version: string;
// 构造函数 用于处理实例化时传入的参数
constructor (options) {}
// 获取到路由路径对应的组件实例
match (raw, current, redirectedFrom) {}
// 返回 history.current 当前路由路径
get currentRoute () {}
// 存入根组件实例,并监听路由的改变
init (app) {}
// 注册一些全局钩子函数
beforeEach (fn) {} // 全局前置守卫
beforeResolve (fn) {} // 全局解析守卫
afterEach (fn) {} // 全局后置钩子
onReady (cb, errorCb) {} // 路由完成初始导航时调用
onError (errorCb) {} // 路由导航过程中出错时被调用
// 注册一些 history 导航函数
push (location, onComplete, onAbort) {}
replace (location, onComplete, onAbort) {}
go (n) {}
back () {}
forward () {}
// 获取路由对应的组件
getMatchedComponents (to) {}
// 解析路由表
resolve (to, current, append) {}
// 添加路由表 并自动跳转到首页
addRoutes (routes) {}}
// 注册钩子函数,push 存入数组
function registerHook (list, fn) {}
// 根据模式(hash / history)拼接 location.href
function createHref (base, fullPath, mode) {}
// 挂载静态属性及方法
VueRouter.install = install
VueRouter.version = '__VERSION__'
// 浏览器环境下且 window.Vue 存在则自动调用 Vue.use 注册该路由插件
if (inBrowser && window.Vue) {window.Vue.use(VueRouter)
}
下面看一些主要方法的具体实现。
constructor
VueRouter 构造函数,主要做了 3 件事:
- 初始化传入的参数 options,即调用
new VueRouter()
时传入的参数; - 创建 match 匹配函数,用于匹配当前 path 或 name 对应的路由组件;
- 根据不同模式生成 history 实例,history 实例提供一些跳转、监听等方法;
关于 history 实例 及 match 匹配函数后面会讲到。
constructor(options = {}) {
this.app = null // 根组件实例,在 init 中获取并赋值
this.apps = [] // 保存多个根组件实例,在 init 中被添加
this.options = options // 传入配置项参数
this.beforeHooks = [] // 初始化全局前置守卫
this.resolveHooks = [] // 初始化全局解析守卫
this.afterHooks = [] // 初始化全局后置钩子
this.matcher = createMatcher(options.routes || [], this) // 创建 match 匹配函数
let mode = options.mode || 'hash' // 默认 hash 模式
// history 浏览器环境不支持时向下兼容使用 hash 模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {mode = 'hash'}
// 非浏览器环境强制使用 abstract 模式
if (!inBrowser) {mode = 'abstract'}
this.mode = mode
// 根据不同模式生成 history 实例
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)
}
}
}
init
install.js
中,init 函数会在根组件实例的 beforeCreate 生命周期函数里调用,传入根组件实例。
// 传入根组件实例
init(app) {
// 非生产环境进行未安装路由的断言报错提示
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 保存该根组件实例
this.apps.push(app)
// 设置 app 销毁程序
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// 当销毁时,将 app 从 this.apps 数组中清除,防止内存溢出
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// app 已初始化则直接返回
if (this.app) {return}
this.app = app
// 跳转到当前路由
const history = this.history
if (history instanceof HTML5History) {history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {const setupHashListener = () => {history.setupListeners()
}
history.transitionTo(history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 设置路由监听,路由改变时改变 _route 属性,表示当前路由
history.listen(route => {this.apps.forEach((app) => {app._route = route})
})
}
install.js
该文件主要是定义并导出一个 install 方法,在 Vue.use(VueRouter)
时被调用。
install 方法主要做了这几件事:
- 通过全局混入 beforeCreate 钩子函数的方式,为每个 vue 组件实例添加了指向同一个路由实例的
_routerRoot
属性,使得每个组件中都可以获取到路由信息及方法。 - Vue.prototype 上挂载
$router
$route
两个属性,分别表示路由实例及当前路由。 - 全局注册 router-link router-view 组件。
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 若已调用过则直接返回
if (install.installed && _Vue === Vue) return
install.installed = true
// install 函数中将 Vue 赋值给 _Vue
// 可在其他模块中不用引入直接使用 Vue 对象
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal)
}
}
// 每个组件混入 beforeCreate 钩子函数的实现
Vue.mixin({beforeCreate () {
// 判断是否存在 router 对象,若存在则为根实例
if (isDef(this.$options.router)) {
// 设置根路由
this._routerRoot = this
this._router = this.$options.router
// 路由初始化,将根实例传入 VueRouter 的 init 方法
this._router.init(this)
// _router 属性双向绑定
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根实例则通过 $parent 指向父级的 _routerRoot 属性,最终指向根实例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {registerInstance(this)
}
})
// 注入 $router $route
Object.defineProperty(Vue.prototype, '$router', {get () {return this._routerRoot._router}
})
Object.defineProperty(Vue.prototype, '$route', {get () {return this._routerRoot._route}
})
// 全局注册 router-link router-view 组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
create-route-map.js
create-route-map.js
文件导出一个 createRouteMap 方法,用于创建路由根据 path 和 name 的映射表。
// ...
// 创建路由 map
export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap) {
// pathList 用于控制路径匹配优先级
const pathList = oldPathList || []
// 根据 path 的路由映射表
const pathMap = oldPathMap || Object.create(null)
// 根据 name 的路由映射表
const nameMap = oldNameMap || Object.create(null)
// 遍历路由配置添加到 pathList pathMap nameMap 中
routes.forEach(route => {addRouteRecord(pathList, pathMap, nameMap, route)
})
// 将通配符路由 * 取出插到末尾,确保通配符路由始终在尾部
for (let i = 0, l = pathList.length; i < l; i++) {if (pathList[i] === '*') {pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {}
function compileRouteRegex(path, pathToRegexpOptions) {}
function normalizePath(path, parent, strict) {}
addRouteRecord
createRouteMap 函数中最重要的一步就是 遍历路由配置并添加到映射表中 的 addRouteRecord 函数。
addRouteRecord 函数作用是生成两个映射表,PathMap 和 NameMap,分别可以通过 path 和 name 查询到对应的路由记录对象,路由记录对象包含 meta、props、及最重要的 components 视图组件实例用于渲染在 router-view
组件中。
// 增加 路由记录 函数
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {
// 获取 path, name
const {path, name} = route
// 编译正则的选项
const pathToRegexpOptions = route.pathToRegexpOptions || {}
// 格式化 path
// 根据 pathToRegexpOptions.strict 判断是否删除末尾斜杠 /
// 根据是否以斜杠 / 开头判断是否需要拼接父级路由的路径
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict // 末尾斜杠是否精确匹配 (default: false)
)
// 匹配规则是否大小写敏感?(默认值:false)
// 路由配置中 caseSensitive 和 pathToRegexpOptions.sensitive 作用相同
if (typeof route.caseSensitive === 'boolean') {pathToRegexpOptions.sensitive = route.caseSensitive}
// 路由记录 对象
const record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// 若非命名视图组件,则设为默认视图组件
components: route.components || {default: route.component},
instances: {},
name,
parent,
matchAs, // alias 匹配的路由记录 path 为别名,需根据 matchAs 匹配
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null ? {} : route.components ?
route.props : {default: route.props}
}
if (route.children) {
// 如果是命名路由,没有重定向,并且有默认子路由,则发出警告。// 如果用户通过 name 导航路由跳转则默认子路由将不会渲染
// https://github.com/vuejs/vue-router/issues/629
if (process.env.NODE_ENV !== 'production') {if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name:'${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
// 递归路由配置的 children 属性,添加路由记录
route.children.forEach(child => {
// 别名匹配时真正的 path 为 matchAs
const childMatchAs = matchAs ?
cleanPath(`${matchAs}/${child.path}`) :
undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 处理别名 alias 逻辑 增加对应的 记录
if (route.alias !== undefined) {const aliases = Array.isArray(route.alias) ?
route.alias : [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
// 更新 path map
if (!pathMap[record.path]) {pathList.push(record.path)
pathMap[record.path] = record
}
// 命名路由添加记录
if (name) {if (!nameMap[name]) {nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{name: "${name}", path: "${record.path}" }`
)
}
}
}
递归子路由中还有一个警告,对于命名路由且有默认子路由时在开发环境给出提示。
这个提示用于避免一个 bug,具体可以看一下对应的 issue
简单的说就是当命名路由有默认子路由时
routes: [{
path: '/home',
name: 'home',
component: Home,
children: [{
path: '',
name: 'home.index',
component: HomeIndex
}]
}]
使用 to="/home"
会跳转到 HomeIndex 默认子路由,而使用 :to="{name:'home'}"
则只会跳转到 Home 并不会显示 HomeIndex 默认子路由。
通过上面 addRouteRecord 函数源码就能知道这两种跳转方式 path 和 name 表现不同的原因了:
因为通过 path 和 name 是分别从两个映射表查找对应路由记录的,
pathMap 生成过程中是先递归子路由,如上例,当添加该子路由的路由记录时,key 就是 /home
,子路由添加完后父路由添加时判断 /home
已存在则不会添加进 pathMap。
而 nameMap 的 key 是 name,home
对应的就是 Home 组件,home.index
对应 HomeIndex。
create-matcher.js
createMatcher 函数根据路由配置调用 createRouteMap 方法建立映射表,并提供了 匹配路由记录 match 及 添加路由记录 addRoutes 两个方法。
addRoutes 用于动态添加路由配置;
match 用于根据传入的 location 和 路由对象 返回一个新的路由对象;
// 参数 routes 表示创建 VueRouter 传入的 routes 配置信息
// router 表示 VueRouter 实例
export function createMatcher(routes, router) {
// 创建路由映射表
const {pathList, pathMap, nameMap} = createRouteMap(routes)
function addRoutes(routes) {createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配
function match(raw, currentRoute, redirectedFrom) {const location = normalizeLocation(raw, currentRoute, false, router)
const {name} = location
if (name) {
// 命名路由处理
// 合并 location 及 record 的数据并返回一个新的路由对象
} else if (location.path) {
// 普通路由处理
// 合并 location 及 record 的数据并返回一个新的路由对象
}
// 没有匹配到路由记录则返回一个空的路由对象
return _createRoute(null, location)
}
function redirect(record, location) {// ...}
function alias(record, location, matchAs) {// ...}
// 根据条件创建不同的路由
function _createRoute(record, location, redirectedFrom) {
// 处理 重定向 redirect
if (record && record.redirect) {return redirect(record, redirectedFrom || location)
}
// 处理 别名 alias
if (record && record.matchAs) {return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
history/base.js
history/base.js 中定义了一个 History 类,主要的作用是:
路由变化时通过调用 transitionTo 方法以获取到对应的路由记录并依次执行一系列守卫钩子函数;
export class History {constructor (router, base) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []}
listen (cb) {this.cb = cb}
onReady (cb, errorCb) {if (this.ready) {cb()
} else {this.readyCbs.push(cb)
if (errorCb) {this.readyErrorCbs.push(errorCb)
}
}
}
onError (errorCb) {this.errorCbs.push(errorCb)
}
// 切换路由,在 VueRouter 初始化及监听路由改变时会触发
transitionTo (location, onComplete, onAbort) {
// 获取匹配的路由信息
const route = this.router.match(location, this.current)
// 确认切换路由
this.confirmTransition(route, () => {
// 以下为切换路由成功或失败的回调
// 更新路由信息,对组件的 _route 属性进行赋值,触发组件渲染
// 调用 afterHooks 中的钩子函数
this.updateRoute(route)
// 添加 hashchange 监听
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只执行一次 ready 回调
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {if (onAbort) {onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
// 确认切换路由
confirmTransition (route, onComplete, onAbort) {
const current = this.current
// 中断跳转路由函数
const abort = err => {if (isError(err)) {if (this.errorCbs.length) {this.errorCbs.forEach(cb => { cb(err) })
} else {warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
// 如果是相同的路由就不跳转
if (isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {this.ensureURL()
return abort()}
// 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件
const {updated, deactivated, activated} = resolveQueue(this.current.matched, route.matched)
// 导航守卫数组
const queue = [].concat(
// 失活的组件钩子
extractLeaveGuards(deactivated),
// 全局 beforeEach 钩子
this.router.beforeHooks,
// 在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
// 需要渲染组件 enter 守卫钩子
activated.map(m => m.beforeEnter),
// 解析异步路由组件
resolveAsyncComponents(activated)
)
// 保存路由
this.pending = route
// 迭代器,用于执行 queue 中的导航守卫钩子
const iterator = (hook, next) => {
// 路由不相等就不跳转路由
if (this.pending !== route) {return abort()
}
try {
// 执行钩子
hook(route, current, (to) => {
// 只有执行了钩子函数中的 next,才会继续执行下一个钩子函数
// 否则会暂停跳转
// 以下逻辑是在判断 next() 中的传参
if (to === false || isError(to)) {// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {// next('/') 或者 next({path: '/'}) -> 重定向
abort()
if (typeof to === 'object' && to.replace) {this.replace(to)
} else {this.push(to)
}
} else {
// 这里执行 next
// 也就是执行下面函数 runQueue 中的 step(index + 1)
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {abort(e)
}
}
// 同步执行异步函数
runQueue(queue, iterator, () => {const postEnterCbs = []
const isValid = () => this.current === route
// 当所有异步组件加载完成后,会执行这里的回调,也就是 runQueue 中的 cb()
// 接下来执行 需要渲染组件的导航守卫钩子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
// 队列中的函数都执行完毕,就执行回调函数
runQueue(queue, iterator, () => {
// 跳转完成
if (this.pending !== route) {return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {this.router.app.$nextTick(() => {postEnterCbs.forEach(cb => { cb() })
})
}
})
})
}
updateRoute (route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {hook && hook(route, prev)
})
}
}
参考
- vue-router 源码分析 – 整体流程
- 前端进阶之道 – VueRouter 源码解析
- MDN History_API
- 一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构
- vue-router 源码阅读学习