[Vue.js进阶]从源码角度剖析vue-router(三)

前言

上篇中主要叙述了 vue-router 中生成 $route 对象的时机,路由懒加载的原理,以及异步路由以前执行的一系列路由守卫vue

在本篇中会讲述:node

  • 异步路由解析成功后执行的一系列路由守卫
  • vue-router 是如何经过路由来实现页面之间的切换
  • 为何 beforeRouteEnter 守卫须要经过回调的形式获取组件实例

同时本文会按照 vue-router 官网完整的导航解析流程的 7-12 步,逐个解析每一步的背后的原理git

图1:github

文中的源码截图只保留核心逻辑 完整源码地址vue-router

有兴趣的朋友也能够看我学习源码时的详细注释源码地址编程

vue-router 版本:3.0.2数组

生成 beforeRouteEnter 守卫

上文说到,当异步路由(组件)所有解析完毕后,会执行 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个问题咱们放到后面来讨论,继续往下走主线的流程

调用 beforeResolve 守卫

以后包含 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 才能获取到最终的数据,在同一个事件循环轮次中,异步任务永远是晚于同步任务的

执行 afterEach 守卫

因此视图的更新就被 Vue 延迟到 nextTick 后执行,先会在 updateRoute 中遍历 afterHooks 执行 afterEach 守卫

监听浏览器的前进后退事件

在执行完 afterEach 后,文档的下一步是触发 DOM 更新也就是视图的更新,但其实 vue-router 还会作一些别的逻辑,例如给 hash 模式下的路由设置监听事件,监听浏览器的前进后退,以及一些滚动事件

updateRoute 方法执行后会执行 transitionTo 方法的成功回调,hash 模式最终会执行 setupListeners 设置监听事件

图12:

当浏览器点击前进后退时,会再次执行 transitionTo 方法,即路由的跳转逻辑,达到视图的跳转

history 模式一样也会监听这2个事件,只是监听的时机不一样,它是在实例化时进行监听

图13:

随后会执行 ensureURL 方法,使用 pushState 或者 location.hash 的形式设置 url

执行 beforeRouteEnter 守卫中的回调

前面介绍 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 的回调中就能够获取到组件实例

总结

  • 当异步组件解析成功后,会执行 beforeRouteEnter 守卫
  • 经过 Vue 核心库的 defineReactive 方法,当 $route 被赋值时就会触发 router-view 组件的从新渲染,达到更新视图的功能
  • Vue 会异步更新视图,因此 beforeRouteEnter 中须要使用回调的形式访问到组件实例
  • vue-router 经过监听浏览器的 popState 或者 hashChange 使得点击前进后退也能更新视图

一些感悟

我的认为 vue-router 的源码并非那么容易理解,多层的回调很是跳跃(我的认为若是 vue-router 使用 async/await 语法会容易理解的多),而且伴随着不少边缘状况的处理,在阅读源码时,建议新建一个工程,找到源码文件,多经过 debugger 的形式执行文中所说的关键函数,观察参数以及调用栈的依赖关系

或许源码的阅读并不能像某些文章同样直接对平常开发有所帮助,它的影响是长远的,在源码中每每用到了不少 JavaScript 技巧,例如闭包,柯里化,回调,异步编程,事件循环,原型继承。而这些都是须要有足够扎实的 JavaScript 基础才可以理解的,同时在阅读的过程当中能够进一步提高你的 JavaScript 基础

不只如此,经过阅读源码可以对这个框架有着更深层的理解,而不是死记硬背框架某些的行为,就好比为何 beforeRouteEnter 中必需要经过 next 方法的回调形式才能得到 Vue 实例,以及路由守卫是怎么根据文档中的执行顺序一步步执行的

我的以为,关于源码分析的文章并非那么好理解,若是点开文章的你以为有什么不理解的,但愿在评论区留言我会第一时间回答,这会帮助我改善文章质量,很是感谢~

参考资料

Vue.js 技术揭秘