您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
从源码角度分析VueRouter路由实现
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
从源码角度分析VueRouter路由实现
自猿其说Tech
2022-01-11
IP归属:未知
29000浏览
Vue
### 1 前言 随着前端应用的复杂度增加、用户对于体验的要求提高,单页面(SPA)成为前端应用主流形式。Vue 提供了一个官方的CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。同时vue官方文档也指明,对于大多数单页面应用,都推荐使用官方支持的vue-router,所以本文我们从源码角度分析路由的实现原理以及核心流程。 ![](//img1.jcloudcs.com/developer.jdcloud.com/9d30f6ff-8866-4853-9953-e594ba44405220220111172638.png) ### 2 前置准备 使用说明文档:https://router.vuejs.org/zh/guide/#javascript 源码下载地址:https://github.com/vuejs/vue-router/ 分支:dev(不断更新中) 版本:3.5.2 ### 3 思考点 我们在开始使用vue的时候,一定会接触到VueRouter,那同时我们可能会存在一些疑问点,比如: - Vue.use(VueRouter) 、new VueRouter()等操作是在做什么事情? - beforeRouterEnter中为什么获取不到Vue实例? - 使用路由守卫的时为什么一定要调用next方法? - 为什么hash模式下打开localhost:8080会自动添加/#/? - 一次完整导航解析的流程是什么样的? - ... 当然,在我们长期使用后,肯定会有了深入的了解,接下来,我们会从源码角度去进行简单的说明。 ### 4 基础使用 常用的路由功能 - 路由信息 $route - 路由实例 $router - 路由跳转 $router.push $router.replace - 视图渲染容器 router-view - 路由导航 router-link - 路由守卫 beforeEach... 下面这部分代码是我们对于VueRouter的基础使用示例 ```json // route.ts import Vue from 'vue' import VueRouter from 'vue-router' // 1.注册VueRouter插件 调用VueRouter.install方法 Vue.use(VueRouter) // 2.创建router实例 传入对应配置 export default router = new VueRouter({ mode: 'hash', routes:[ { path: '/home', component: Home}, { path: '/my', component: My} ] }) // main.ts import Vue from 'vue' import router from './router' import App from './App.vue' // 3.在创建和挂载根实例时,将router实例作为参数传入到Vue中,使得整个应用都可使用该功能 new Vue({ router, render: (h: (arg0: any) => any) => h(App) }).$mount('#app') // App.vue <template> <div id="app"> <--路由导航--> <router-link to="/home">首页</router-link> <router-link to="/my">我的</router-link> <--视图渲染容器--> <router-view /> </div> </template> <script> export default { name: 'App' } </script> ``` ### 5 源码分析 #### 5.1 插件注册 Vue.use(plugins)是vue注册插件的方法,该方法会检测插件是否有install方法,有则执行。 ```javascript import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) ``` ##### 5.1.1 install源码解析 ```javascript import View from './components/view' import Link from './components/link' // 声明vue 方便其他文件使用 export let _Vue // 插件注册方法 export function install (Vue) { // 避免多次注册插件 if (install.installed && _Vue === Vue) return install.installed = true _Vue = Vue const isDef = v => v !== undefined // 调用RouterView上绑定的data.registerRouteInstance方法 // 该方法为当前路由记录RouteRecord添加了对应的实例instance // 即matched.instances[name] = callVal const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } // 全局混入beforeCreate和destroyed生命周期 为各组件绑定vue根实例 Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { // 根组件 绑定_routerRoot为根实例 this._routerRoot = this // 声明_router,指向VueRouter实例 this._router = this.$options.router // 初始化VueRouter this._router.init(this) // vue根实例响应式绑定_route 也就是当前路由信息Route // 此处的核心在于:router-view组件依赖了该属性,而该属性是响应式声明 // 所以在路由变化的时候,视图就会更新 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { // 非根组件 绑定_routerRoot为根实例 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // 为当前路由记录RouteRecord添加了对应的实例instance // *** 保证后续执行beforeRouteEnter中next回调函数时有实例可用 registerInstance(this, this) }, destroyed () { registerInstance(this) } }) // Vue原型初始化属性 $router(路由实例) $route(当前路由信息Route,实时变化) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 注册路由相关组件 Vue.component('RouterView', View) Vue.component('RouterLink', Link) const strats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created } ``` ##### 5.1.2 VueRouter.init源码解析 ```javascript init (app: any /* Vue component instance */) { this.apps.push(app) // 保证一个vue应用只初始化一次 app表示的是vue根实例 if (this.app) { return } this.app = app const history = this.history if (history instanceof HTML5History || history instanceof HashHistory) { const handleInitialScroll = routeOrError => { const from = history.current const expectScroll = this.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll && 'fullPath' in routeOrError) { handleScroll(this, routeOrError, from, false) } } // 开启路由监听 const setupListeners = routeOrError => { // 监听popState或者hashChange事件触发后 重新导航一次 history.setupListeners() handleInitialScroll(routeOrError) } // 完成首次导航,并开启路由监听 history.transitionTo( history.getCurrentLocation(), setupListeners, setupListeners ) } // 调用history.listen方法,传入一个回调函数,该回调函数会在路由信息Route更新后执行 // 即在路由信息Route更新后会同步更新vue根实例上的_route // _route属性被劫持,所以会通知相关依赖,其中包括RouterView组件,达到视图更新的效果 history.listen(route => { this.apps.forEach(app => { app._route = route }) }) } ``` #### 5.2 VueRouter实例 我们在创建VueRouter实例时,传入了一些配置,比如mode(路由模式)、routes(路由相关配置)等。那这些配置的实际用途是什么呢?接下来我们根据源码来了解下。 ```javascript import VueRouter from 'vue-router' export default router = new VueRouter({ mode: 'hash', routes:[ { path: '/home', component: Home}, { path: '/my', component: My} ] }) ``` ##### 5.2.1 VueRouter实例源码解析 通过源码,我们可以看到在构造VueRouter实例时,我们会判断当前浏览器是否支持history模式,不支持则会回退到hash模式。同时,我们调用createMatcher方法创建路由映射表并返回matcher对象。 ```javascript export default class VueRouter { // ... constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] // ※※※ 创建路由映射表 this.matcher = createMatcher(options.routes || [], this) // 处理路由模式 并创建对应模式的history实例 let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode 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}`) } } } } ``` ##### 5.2.2 matcher源码解析 createMatcher方法中调用createRouteMap方法,生成了pathList、pathMap、nameMap三个路由映射表。 matcher包含了四个属性 - match 根据目标路由与当前路由信息Route,在路由映射表中进行匹配,并返回匹配到的路由信息Route - addRoute 动态添加单个路由 - addRoutes 动态添加多个路由 - getRoutes 获取路由记录RouteRecord列表 该示例为实际的路由记录RouteRecord对象结构 ![](//img1.jcloudcs.com/developer.jdcloud.com/225935ce-6e56-46ec-8510-8ad9bef9e80c20220111173051.png) 该示例为实际的路由信息Route对象结构 ![](//img1.jcloudcs.com/developer.jdcloud.com/c3ed7291-2f04-4951-b8cf-59ba5f76a29920220111173333.png) ###### 1)createRouteMap 创建路由映射表,维护pathList、pathMap、nameMap数据。 addRoute和addRoutes动态添加路由方法的核心就是createRouteMap,也就是往路由映射表里新增配置数据。 ```javascript export function createRouteMap ( routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord>, parentRoute?: RouteRecord ){ // 若是首次创建路由映射表则直接默认为空,若是后期动态添加路由则需要传入历史的路由映射表,以保证在原有基础上添加 const pathList: Array<string> = oldPathList || [] const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) // 遍历创建VueRouter实例时传入的配置信息routes 进行路由记录RouteRecord的添加 // 此处的route可能是嵌套路由即存在children,所以我们需要递归调用addRouteRecord // 其中pathList顺序是先子后父 routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route, parentRoute) }) // 确保通配符*路由一定在pathList的最后 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 } } ``` ###### 2)match macth方法根据参数raw(目标路由)以及currentRoute(当前路由信息Route)在nameMap或者pathMap中查找到对应的路由记录RouteRecord,调用_createRoute生成路由信息Route并返回。 ```javascript // raw是我们实际调用push或者replace等跳转路由方法传入的参数 // 形如'/my?param=123'或者是{name:'My',params:{param:123}}等。 function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { // 根据入参生成格式化后的目标路径信息 const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location // 我们会优先使用name去匹配 没有配置name再使用path匹配 if (name) { const record = nameMap[name] // 未匹配到路由 _createRoute第一个参数是匹配到的路由记录RouteRecord // 此处传null最后生成对路由信息Route.matched为[],也就是没有组件需要渲染,即页面空白 if (!record) return _createRoute(null, location) // 获取所有必须的params。如果optional为true说明params不是必须的 const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) if (typeof location.params !== 'object') { location.params = {} } if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } // path和params整合返回一个真实的路径 location.path = fillParams(record.path, location.params, `named route "${name}"`) // 返回匹配的冻结路由信息Route 避免外部更改 // Route.matched是当前路由相关路有记录RouteRecord集合 顺序:先父后子 return _createRoute(record, location, redirectedFrom) } else if (location.path) { location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] // 使用路由记录RouteRecord的regex对目标路由进行匹配 若匹配则返回路由信息Route if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // 未匹配到路由 return _createRoute(null, location) } ``` 看完match方法源码的解释,你是否会有一个疑问,为什么使用name直接去nameMap中匹配路由即可,而使用path匹配路由却需要遍历pathList呢? 这是因为我们配置中存在形如'/my/:id'的path路径,而这种路径在实际跳转时访问的是类似于'/my/123'带有实际参数的路径,如果我们直接使用pathMap去匹配是匹配不到的,所以我们需要遍历pathList,根据每一个path的正则以及目标路由的path和params去进行匹配,得到'/my/:id'对应的RouteRecord就是'/my/123'的对应的RouteRecord。 #### 5.3 History ![](//img1.jcloudcs.com/developer.jdcloud.com/494b5cd3-b513-4d0e-9cb0-401e3cfdb21720220111173620.jpg) ##### 5.3.1 HashRouter实例 在创建HashRouter实例时,我们会检查url中是否包含/#/,没有则进行添加。同时HashRouter为我们提供了路由跳转能力,比如push、replace、go等,之后我们会一一说明。 ```javascript export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // 若是采用降级方案采用的hash模式 则会判断当前路径是否有/#/ // 没有则会添加,并使用window.location.replace切换路径 if (fallback && checkFallback(this.base)) { return } // 没有采用降级方案,直接采用的hash模式 ensureSlash() } } ``` 结合上下两部分代码,我们能看到,首次创建HashRouter实例时,会调用checkFallback或者ensureSlash来确保hash模式下url中有/#/。 同时,这也就解释了为什么hash模式下打开localhost:8080会自动添加/#/。 ```javascript function ensureSlash (): boolean { // 获取#后的字符串 const path = getHash() // 若第一个字符是/ 则表示为正确的路由且有# 则不做处理 if (path.charAt(0) === '/') { return true } // 若第一个字符是/ 则表示当前路由没有#或者#/字符 // 拼接好目标path 调用replaceHash来完成路径切换 replaceHash('/' + path) return false } ``` ##### 5.3.2 HistoryRouter实例 HistoryRouter和HashRouter实现基本一致,差异在于HistoryRouter不会做容错处理,不会判断是否支持 historyApi,而是直接使用。 #### 5.4 路由导航 上面的内容,都是在做路由能力的准备工作,接下来,我们将依托于这些准备工作,来分析路由导航的实现流程。 ##### 5.4.1 go、forward、back 这三个方法最终调用的都是window.history.go(n)。那么这个方法是如何更新的视图呢?还记得我们在分析插件注册时,有说过VueRouter.init完成了首次导航,并开启路由监听吗?这就是它们更新视图的原因。 我们监听了window上的popstate或者是hashchange事件,这样在路由变化时,就会调用transtionTo方法完成对_route路由信息的更新,从而触发视图渲染。所以,在这种情况下,浏览器地址变化是在视图更新前的。 ```javascript go (n) { this.history.go(n) } back () { this.go(-1) } forward () { this.go(1) } ``` ##### 5.4.2 push、replace push和replace方法的核心是类似的,也就是transitionTo,唯一不同之处在于导航切换完成后的回调中, push调用pushHash更新路由,replace调用replaceHash更新路由。 ```javascript push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { // 导航完成后调用window.location.push或者history.pushState更新浏览器地址 pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { // 导航完成后调用window.location.replace或者history.replaceState更新浏览器地址 replaceHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } ``` ##### 5.4.3 transitionTo 不管是浏览器地址变化,还是调用push、replace方法跳转,核心都在于transitionTo方法,接下来,我们将重点分析该方法。 ```javascript transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function ) { let route // 根据目标路由以及当前的路由信息Route调用match方法进行路由匹配 // 返回目标路由的Route对象 route = this.router.match(location, this.current) // 调用confirmTransition this.confirmTransition( route, () => { // ... 成功回调 }, err => { // ...失败回调 } ) } ``` ###### 1)核心工具方法 首先,我们先分析整个流程中用到的核心方法,后续再分析流程时就不需要再打断思路了。 ① resolveQueue 该方法是我们根据传入的当前和目标路由RouteRecord来获取updated(需要更新的路由记录数组)、deactivated(需要销毁的路由记录数组)、activated(需要激活的路由记录数组)的。而这些数组是我们判断执行哪些路由守卫的依据。 ![](//img1.jcloudcs.com/developer.jdcloud.com/4ce5a87b-454b-4066-b440-92629e06a37020220111173908.png) ```javascript function resolveQueue ( current: Array<RouteRecord>, next: Array<RouteRecord> ): { updated: Array<RouteRecord>, activated: Array<RouteRecord>, deactivated: Array<RouteRecord> } { let i const max = Math.max(current.length, next.length) for (i = 0; i < max; i++) { if (current[i] !== next[i]) { break } } // 结合代码与上面的图片,我们能清晰的看到,哪些需要更新,哪些需要销毁,哪些需要激活 return { updated: next.slice(0, i), activated: next.slice(i), deactivated: current.slice(i) } ``` ② extractLeaveGuards、extractUpdateHooks、extractEnterGuards 这几个方法是我们用来获取beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter守卫的。 ```javascript // 虽然在处理beforeRouteLeave、beforeRouteUpdate和beforeRouteEnter // 都调用了extractGuards提取守卫,但是传入的参数是不一致的 function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> { return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) } function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> { return extractGuards(updated, 'beforeRouteUpdate', bindGuard) } function extractEnterGuards ( activated: Array<RouteRecord> ): Array<?Function> { return extractGuards( activated, 'beforeRouteEnter', (guard, _, match, key) => { return bindEnterGuard(guard, match, key) } ) } // 提取路由守卫 function extractGuards ( records: Array<RouteRecord>, name: string, bind: Function, reverse?: boolean ): Array<?Function> { // 获取到所有的守卫 调用flatMapComponents遍历records,然后将参数传给回调函数 // def组件 instance组件实例 match路由记录RouteRecord数组 key组件名默认为default const guards = flatMapComponents(records, (def, instance, match, key) => { // 获取组件options下对应命名的路由守卫 const guard = extractGuard(def, name) // 绑定this指向 if (guard) { return Array.isArray(guard) ? guard.map(guard => bind(guard, instance, match, key)) : bind(guard, instance, match, key) } }) return flatten(reverse ? guards.reverse() : guards) } // beforeRouteLeave、beforeRouteUpdate调用该方法 function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard { if (instance) { return function boundRouteGuard () { // 绑定this指向为组件实例 return guard.apply(instance, arguments) } } } // beforeRouteEnter调用该方法 function bindEnterGuard ( guard: NavigationGuard, match: RouteRecord, key: string ): NavigationGuard { return function routeEnterGuard (to, from, next) { return guard(to, from, cb => { // 执行beforeRouteEnter守卫时,我们判断next方法传入的参数是否为函数 // 若是函数 则将该回调函数维护在对应路由记录RouteRecord.enteredCbs中 if (typeof cb === 'function') { if (!match.enteredCbs[key]) { match.enteredCbs[key] = [] } match.enteredCbs[key].push(cb) } next(cb) }) ``` 为什么beforeRouterEnter守卫需要做特殊处理,且支持next传入回调函数呢? 众所周知,我们在该路由守卫中是没有办法获取到vue实例的,但我们是可以在next回调函数中获取到的。所以我们在提取beforeRouteEnter守卫时需要先将其next回调函数存储起来留作后续使用。 ③ handleRouteEntered 批量处理beforeRouteEnter守卫中next传入的回调函数。 ```javascript // route为目标路由信息 function handleRouteEntered (route) { // 遍历目标路由匹配到的所有RouteRecord for (var i = 0; i < route.matched.length; i++) { var record = route.matched[i]; // 实例存在则处理 for (var name in record.instances) { var instance = record.instances[name]; // 获取到我们之前在bindEnterGuard中存储的回调函数 var cbs = record.enteredCbs[name]; if (!instance || !cbs) { continue } delete record.enteredCbs[name]; // 若回调函数存在 则调用,且传入instance也就是组件实例作为入参 // 所以这就是为什么我们能在beforeRouteEnter的next回调函数中获取到组件实例的原因 for (var i$1 = 0; i$1 < cbs.length; i$1++) { if (!instance._isBeingDestroyed) { cbs[i$1](instance); } } } } ``` ④ runQueue 该方法将传入的路由守卫队列同步依次执行。 ```javascript // queue路由守卫队列 fn迭代器 cb是队列执行完毕后执行的回调函数 export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) { const step = index => { if (index >= queue.length) { cb() } else { if (queue[index]) { // 当路由守卫存在时,则执行迭代器 传入路由守卫函数以及进入下一个迭代的回调函数 fn(queue[index], () => { step(index + 1) }) } else { step(index + 1) } } } step(0) } // 迭代器 hook路由守卫函数 next进入下一个迭代的回调函数 const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } try { // 路由守卫函数 目标路由信息 当前路由信息 next回调函数 // *** 假设我们在调用路由守卫时不执行next,那就导致我们没有调用runQueue的step方法 // 队列也就停止往后执行。所以这就是我们为什么一定要执行next的原因 hook(route, current, (to: any) => { if (to === false) { this.ensureURL(true) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { this.ensureURL(true) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeof to.name === 'string')) ) { abort(createNavigationRedirectedError(current, route)) if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // *** 调用next方法使得队列进入下一个迭代 next(to) } }) } catch (e) { abort(e) } } ``` ⑤ updateRoute 更新当前路由信息Route。 ```javascript updateRoute (route: Route) { // 更新当前路由信息 this.current = route // 你是否还记得,在插件注册时调用了VueRouter.init方法,其中调用了history.listen // 将其回调函数赋值给了cb 所以此处我们执行的就是app._route=route // 由于_route是响应式声明的,所以会通知对应的依赖 // 其中包含RouterView,也就完成了视图的渲染更新 this.cb && this.cb(route) } // VueRouter init(){ // ... history.listen(route => { this.apps.forEach(app => { app._route = route }) }) } ``` ###### 2)confirmTransition ① 判断是否为相同路由,若是则取消导航。 ```javascript const lastRouteIndex = route.matched.length - 1 const lastCurrentIndex = current.matched.length - 1 // 判断目标路由和当前路由是否为同一个路由,若导航到相同路由则会取消导航 if ( isSameRoute(route, current) && lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex] ) { this.ensureURL() return abort(createNavigationDuplicatedError(current, route)) } ``` ② 调用resolveQueue方法,获取updated、deactivated、activated。 ```javascript const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched ) ``` ③ 逐一提取路由守卫,并将他们合并到同一个队列queue中。 通过下面这段源码,我们能清晰的分析出来,各个路由守卫应该写在哪里 1. 组件内部:beforeRouteLeave、beforeRouteUpdate 1. VueRouter实例上:beforeEach 1. 路由配置routes中:beforeEnter ```javascript const queue: Array<?NavigationGuard> = [].concat( // 销毁组件的beforeRouteLeave守卫 extractLeaveGuards(deactivated), // 全局beforeEach守卫 this.router.beforeHooks, // 更新组件的beforeRouteUpdate守卫 extractUpdateHooks(updated), // 激活组件的beforeEnter守卫 activated.map(m => m.beforeEnter), // 异步加载的激活组件 resolveAsyncComponents(activated) ) ``` ④ 调用runQueue方法顺序依次执行queue队列。 ```javascript // 传入执行队列、迭代器、成功回调参数 // 将队列中每一个路由守卫函数传给迭代器,在迭代器中执行路由守卫 // 并且路由守卫中必须调用next方法,队列才会进入下一个迭代 // 迭代完成后,调用该成功回调 runQueue(queue, iterator, () => { // 激活组件可能包含需要异步加载的,为保证获取到所有激活组件beforeRouteEnter守卫 // 我们在第一个队列迭代完成后,再开启一个新的队列进行迭代 const enterGuards = extractEnterGuards(activated) // beforeRouterEnter、beforeResolve路由守卫 const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } this.pending = null // 执行成功回调 更新当前路由信息Route onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { // 执行beforeRouteEnter路由守卫中next传入的回调函数 handleRouteEntered(route) }) } }) }) ``` ⑤ 两个队列迭代完成后,调用confirmTransition的成功回调函数。 ```javascript this.confirmTransition( route, () => { // 更新路由信息 触发视图更新 this.updateRoute(route) // 调用transitionTo回调函数: // 即调用pushHash/replaceHash方法切换路由,执行push传入的成功回调函数 onComplete && onComplete(route) this.ensureURL() // 执行afterEach路由守卫 this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { // ... } ) ``` ##### 5.4.4 导航解析完整流程 push和replace是先更新的_route及视图,再更新的浏览器地址;而go是先更新的浏览器地址,再更新的_route及视图。 ![](//img1.jcloudcs.com/developer.jdcloud.com/2a49ec00-c3b4-44c2-99d7-e528e373942b20220111174335.png) beforeRouteEnter中获取不到实例原因解析: beforeRouteEnter路由守卫在视图更新前执行,registerInstance方法声明在RouterView,而我们调用registerInstance方法是在视图更新后,beforeCreate中。 所以我们在beforeRouteEnter中肯定没有办法获取到实例instance。而执行beforeRouteEnter路由守卫中next回调是在$nextTick中,也就是异步执行,这个时候beforeCreate已经执行完毕,所以此时可以获取到实例instance。 ### 6 总结 ![](//img1.jcloudcs.com/developer.jdcloud.com/e3c505b0-9437-43a1-af33-f1691c518e7d20220111174359.png) 至此,VueRouter源码部分已经大致分析完成,最后梳理出一张思维导图供给大家参考。当然vue3现在也已经走入我们的视野,相应的VueRouter版本也进行了升级,使用方法上也有了一定的变化,感兴趣的可以做下两个版本的比较和学习。 ------------ ###### 自猿其说Tech-京东物流技术发展部 ###### 作者:李鹤
原创文章,需联系作者,授权转载
上一篇:京东移动端组件库 React 版如约而来
下一篇:JSF本地联调工具实践
相关文章
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
京东mPaaS平台之Android组件化系统私有化部署改造实践!
【技术干货】企业级扫描平台EOS-Jenkins集群进阶之路
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号