在上篇中主要叙述了 vue-router 中生成 $route 对象的时机,路由懒加载的原理,以及异步路由以前执行的一系列路由守卫vue
在本篇中会讲述:node
同时本文会按照 vue-router 官网完整的导航解析流程的 7-12 步,逐个解析每一步的背后的原理git
图1:github
文中的源码截图只保留核心逻辑 完整源码地址vue-router
有兴趣的朋友也能够看我学习源码时的详细注释源码地址编程
vue-router 版本:3.0.2
数组
上文说到,当异步路由(组件)所有解析完毕后,会执行 next
方法遍历 queue 数组中的下个元素,但此时 queue 数组中的元素已经所有遍历完毕,因此会直接执行 runQueue
的第三个参数,即成功的回调函数浏览器
图2:闭包
紧接着会执行 extractEnterGuards
这个函数,而上文中介绍到 extract 开头的函数会根据传入的路由记录这个参数,从中获取组件配置项中的指定的路由守卫,这里 vue-router 会根据 activated 数组,也就是跳转先后新增的路由记录数组,从中获取 beforeRouteEnter 守卫app
和以前的那些路由守卫不一样的是,它会额外传入一个 postEnterCbs 参数来存储 beforeRouteEnter 守卫中,经过 next 方法传入的回调参数
图3:
若是在组件中 beforeRouteEnter 守卫里的 next 函数里,传入了一个回调函数,就会往 postEnterCbs 数组中添加这个回调,同时回调会被包裹一层 poll
函数用来指定参数,即组件实例 vm
图4:
经过 instance[key] 从路由记录的 instance 属性获取到组件实例,可是在注册回调时,这个时候组件实例为空对象
图5:
这是为何呢?咱们同时再来思考一个问题,为何 vue-router 的其余守卫能够直接在内部经过 this 访问组件实例,而 beforeRouteEnter 必须经过在 next 函数中传入回调的形式来获取组件实例?这2个问题咱们放到后面来讨论,继续往下走主线的流程
以后包含 beforeRouteEnter 守卫的数组会和 beforeResolve 守卫合并,而且再一次的执行 runQueue
,即开始第二轮的遍历
遍历逻辑在上文中也详细叙述过,主要就是每次遍历 queue 的一个路由守卫,而且当路由守卫调用 next 方法后才会继续遍历下个守卫,也就是说 beforeRouteEnter 和 beforeResolve 会依次执行,对应图1官网流程的 7,8 两步
当第二轮 queue 遍历完毕后,再一次执行 runQueue
方法成功的回调,在 runQueue
成功回调中会又执行到 onComplete
这个函数,它是 confirmTransition
的成功回调,执行确认导航的逻辑
由于 queue 数组是在 confirmTransition
这个方法内被遍历的的,而onComplete
也是在执行 confirmTransition
被传入的
图6:
其中的第二个参数即为 onComplete
函数,这个函数的第一行中会执行 history 实例的 updateRoute
方法
图7:
这个时候 vue-router 会更新 current 属性,也就是说此时的 current 已经不在是跳转前的 $route 对象了,更新成跳转后的 $route 对象,接着会执行 cb
方法
cb
方法定义在 vue-router 类中
图8:
当 vue-router 初始化的时候会执行 history.listen 并传入一个回调,而这个回调最终会成为 history 实例的 cb 方法,当执行这个回调时,就能够实现页面之间的切换
图9:
接下来咱们来分析这个能改变视图的函数, this.apps 咱们第一章分析过,是一个保存根 Vue 实例的数组,最终会将根实例的 _route 属性更新为当前的 $route 对象,就是这样短短一行代码就能够实现整个页面的切换,这是为何呢?
在第一章混入全局钩子那节,我留了一个悬念
图10:
观察图中第 8 行能够发现,vue-router 会调用 Vue 核心库中的 defineReactive
将根实例的 _route 属性变成响应式, 另外还经过 Object.defineProperty 定义了 $route 属性指向 _route,结合 Vue 的响应式原理,也就是说当 $route 被修改后,经过 defineReactive 会通知全部依赖 $route 的 watcher
而只有 render watcher 才有改变视图的功能,因此能够推测出在某个组件的 render 函数中依赖到了 $route,而这个组件就是 vue-router 内置的全局视图组件 router-view
图11 router-view 组件:
router-view 内部会经过 render 函数根据 $route 中的 components 属性也就是组件配置项,生成 vnode 最后交给 Vue 渲染出视图,因此就会依赖到 $route
回到图7,在确认导航的 updateRoute
方法中,执行 cb
就会触发视图的改变,可是这个行为不会当即被触发,即
视图并不会当即被改变
视图并不会当即被改变
视图并不会当即被改变
重要的事情说三遍,这里就简单提一下 Vue 的视图更新原理
Vue 会维护一个队列,保存全部 watcher,当 cb
执行后为了更新视图,会将 router-view 的 render watcher 推入这个队列,在推入的过程当中会进行惟一值的判断,使得同一个 watcher 在队列中只存在一个,并在 nextTick 后再执行全部的 watcher 回调,这个时候才会改变视图
Vue 之因此这么作是防止没必要要的屡次渲染,例如你在 methods 中写了个 10000 次的循环的方法,每一个循环都会改变一次视图,致使队列中有 10000 个 render watcher,最终触发了 10000 次渲染,这就很是的不合理
而优化后只在第一次循环时将 render watcher 推入队列,以后的 9999 次则只是数据的更新不会把相同的 render watcher 推入队列,最终队列中只有 1 个 render watcher
另外之因此数据更新是通常是同步的,而视图是在 nextTick 后异步更新的,缘由在于只有这样全部的 watcher 才能获取到最终的数据,在同一个事件循环轮次中,异步任务永远是晚于同步任务的
因此视图的更新就被 Vue 延迟到 nextTick 后执行,先会在 updateRoute
中遍历 afterHooks 执行 afterEach 守卫
在执行完 afterEach 后,文档的下一步是触发 DOM 更新也就是视图的更新,但其实 vue-router 还会作一些别的逻辑,例如给 hash 模式下的路由设置监听事件,监听浏览器的前进后退,以及一些滚动事件
在 updateRoute
方法执行后会执行 transitionTo
方法的成功回调,hash 模式最终会执行 setupListeners
设置监听事件
图12:
当浏览器点击前进后退时,会再次执行 transitionTo
方法,即路由的跳转逻辑,达到视图的跳转
history 模式一样也会监听这2个事件,只是监听的时机不一样,它是在实例化时进行监听
图13:
随后会执行 ensureURL
方法,使用 pushState 或者 location.hash 的形式设置 url
前面介绍 beforeRouterEnter 时提到,vue-router 会将 next 方法中的回调推入 postEnterCbs 数组中,当 confirmTransition
的成功回调执行完毕后,会把 postEnterCbs 数组放到 nextTick 后执行
图14:
前面还提到,当在更新视图的时候,Vue 会将视图更新的 render watcher 也放在 nextTick 后执行,也就是说当 postEnterCbs 数组被执行前,会先执行视图更新的逻辑
这就是为何只有 beforeRouteEnter 守卫得到组件实例时,须要定义一个回调并传入 next 函数中的缘由,由于守卫执行的时候是同步的,可是只有在 nextTick 后才能得到组件实例, vue-router 经过回调的形式,将回调的触发时机放到视图更新以后,这样就能保证可以得到组件实例
以前还留下一个问题是,在注册回调时,会给回调传入组件实例,也就是路由记录中 instance[key], 而在注册时它倒是一个空对象
答案显而易见,仍是由于这个时候组件并无生成,因此不会有组件实例,可是当组件生成后咱们须要将 instance[key] 赋值为当前组件
回到最初安装 vue-router 的时候,vue-router 会全局混入 beforeCreate 和 destroyed 2个钩子,以前我省略了 registerInstance
这个函数,完整的代码是这样的
图15:
而这个 registerInstance
的做用正是当组件被生成时,给路由记录的 instance 属性添加当前视图的组件实例( registerInstance
必定会在 next 的回调执行前执行,由于组件更新顺序在 next 的回调以前,而 beforeCreate 是组件更新时执行的逻辑)
图16:
最终在 router-view 组件中调用 matched.instances[name] = val
进行赋值,这样在执行 next 的回调中就能够获取到组件实例
defineReactive
方法,当 $route 被赋值时就会触发 router-view 组件的从新渲染,达到更新视图的功能我的认为 vue-router 的源码并非那么容易理解,多层的回调很是跳跃(我的认为若是 vue-router 使用 async/await 语法会容易理解的多),而且伴随着不少边缘状况的处理,在阅读源码时,建议新建一个工程,找到源码文件,多经过 debugger 的形式执行文中所说的关键函数,观察参数以及调用栈的依赖关系
或许源码的阅读并不能像某些文章同样直接对平常开发有所帮助,它的影响是长远的,在源码中每每用到了不少 JavaScript 技巧,例如闭包,柯里化,回调,异步编程,事件循环,原型继承。而这些都是须要有足够扎实的 JavaScript 基础才可以理解的,同时在阅读的过程当中能够进一步提高你的 JavaScript 基础
不只如此,经过阅读源码可以对这个框架有着更深层的理解,而不是死记硬背框架某些的行为,就好比为何 beforeRouteEnter 中必需要经过 next 方法的回调形式才能得到 Vue 实例,以及路由守卫是怎么根据文档中的执行顺序一步步执行的
我的以为,关于源码分析的文章并非那么好理解,若是点开文章的你以为有什么不理解的,但愿在评论区留言我会第一时间回答,这会帮助我改善文章质量,很是感谢~