【源码解析】vue-router实现原理,揭秘每个组件的实例是如何拥有$router的

852 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情

前言

大家好,前面我们用了六个篇幅把Vuex相关的源码原理进行了分析介绍,那么除了vuex外在我们日常开发中还有个比较常用,跟vuex同样重要甚至比vuex还要重要的插件,那就是用来管理前端路由的vue-router。本系列分享我们将换一种方式,一边介绍vue-router的使用方法一边解读对应的源码,这样可以能功能容易理解一些。那么接下来我们还是从Vue.use(VueRouter)和new VueRouter开始。

vue-router的使用

首先我们先来看下vue-router是如何使用的。

import Vue from 'vue';
import VueRouter from 'vue-router'

Vue.use(VueRouter);
const router = new VueRouter({
    []
});
export default router

如上代码,其实跟vuex一样vue-router也是通过Vue.use进行注册然后再通过new VueRouter来创建实例,最后再将实例传给Vue。我们知道Vue.use有个特点:传给它的参数要么是一个函数,要么是一个带有install方法的对象。那么上面也提到我们在使用vue-router时除了使用Vue.use外还会通过new VueRouter来创建实例,因此这里可以推测VueRouter是一个类,并且该类中包含一个install方法。接下来我们就给Vue.use打个断点来调试一下代码,验证下我们的推测是否正确,如下图: caoshenhuinan.gif 从右侧调用栈可以看到当执行到Vue.use时,下一步就进入到了install方法,这就进一步印证了我们的推测是正确的。知道了vue-router的工作流程后,接下来我们就来解读下下它的源码,看看在install里都干了哪些事,new VueRouter时又做了什么。

源码解读

在解读vuex的源码时我们知道,install的内部其实就是通过Vue.mixin全局混入给每个组件的实例都挂载一个$store属性,这样就便于我们在每个组件中都能够方便的访问到store实例了。而vue-router与之类似其目的也是为了能够让我们方便的在每个组件实例中访问到router和route两个属性,但实现方式却与vuex稍有不同,下面我们来分析下源码。(本系列基于vue-router3.5.4分析)

// vue-router/dist/vue-router.js 1284行
  var _Vue;
  function install (Vue) {
    if (install.installed && _Vue === Vue) { return }
    install.installed = true;

    _Vue = Vue;

    var isDef = function (v) { return v !== undefined; };

    var registerInstance = function (vm, callVal) {
      var i = vm.$options._parentVnode;
      if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
        i(vm, callVal);
      }
    };

    Vue.mixin({
      beforeCreate: function beforeCreate () {
        //只有根组件才会进入
        if (isDef(this.$options.router)) {
          this._routerRoot = this;
          this._router = this.$options.router;
          this._router.init(this);
          Vue.util.defineReactive(this, '_route', this._router.history.current);
        } else {
          this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
        }
        registerInstance(this, this);
      },
      destroyed: function destroyed () {
        registerInstance(this);
      }
    });

    Object.defineProperty(Vue.prototype, '$router', {
      get: function get () { return this._routerRoot._router }
    });

    Object.defineProperty(Vue.prototype, '$route', {
      get: function get () { return this._routerRoot._route }
    });

    Vue.component('RouterView', View);
    Vue.component('RouterLink', Link);

    var strats = Vue.config.optionMergeStrategies;
    // use the same hook merging strategy for route hooks
    strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
  }
  • 首先install接收一个Vue参数,这个参数实际就是Vue
  • 然后检测vue-router是否已经安装,如果已经安装过则直接跳过
  • 如果没有安装过则继续向下执行,并将installed置为true,同时将Vue赋值给_Vue标记vue-router已经安装
  • 在内部定义一个_isDef方法,主要就是用于检测一些变量是否被定义并赋值
  • 接着又定义了一个registerInstance方法,该方法会在每个组件的beforeCreate钩子中调用,而在该方法的内部最终执行的其实是一个名为registerRouteInstance的方法,但是该方法不是每次都会执行,而是要满足一定的条件才会执行,在registerRouteInstance的内部其实就是将当前组件保存到一个matched.instances的对象中
  • 接下来则是install的核心:通过Vue.mixin全局混入向每个组件中混入beforeCreatedestroyed两个钩子函数
    • 在beforeCreate中又分为两个分支,只有根组件才会进入if分支,因为我们在创建Vue实例时会将router实例传递给Vue,也就是说一开始只有根组件实例的$options上才会有router实例。
    • 在if分支中分别又添加了两个属性:_routerRoot 指向的是Vue实例本身, _router指向的是router的实例
    • 调用router实例的init方法,做了一些初始化操作
    • 最后又通过Vue.util.defineRective给Vue实例添加了一个响应式属性_route,该属性保存的是对应的路由信息
    • 而在else分支中是每个子组件都会进来的,因为一开始在子组件实例的$options上并不存在router实例
    • 在else分支中主要就做了一件事:就是给当前组件实例添加一个_routerRoot属性,而这个_routerRoot始终指向的都是根组件Vue的实例。而后面不管在哪个组件中只要访问this.$router/this.¥route时,最终实际上访问的都是根组件Vue实例上的对应的两个属性
    • 最后再调用上面定义的registerInstance方法执行
    • 而在destroy钩子中仅仅是执行了registerInstance方法,并没有其它操作
  • mixin混入完成后,接着又通过Object.defineProperty向Vue的原型对象上分别添加了$router和¥route两个属性,分别对应的是router实例和当前路由对象,而在这两个属性的get函数中始终返回的都是_routerRoot上的_router和_route。_routerRoot就是根组件Vue的实例,而_router和_route则是上面混入时在if分支里添加的两个属性,这也是为什么在else分支中始终都让_routerRoot指向根组件Vue实例的原因
  • 最后又通过Vue.component分别注册了两个全局组件RouterView和RouterLink。View和Link对应的是两个函数式组件,我们后续会详细解读 以上就是vue-router在install时做的事,其核心简单总结就是:让每个组件都能够访问到router实例和当前路由route信息,同时注册两个全局组件RouterView和RouterLink

总结

本次分享我们介绍了vue-router的简单用法,以及在Vue.use(VueRouter)时都做了哪些事,简单总结如下:

通过Vue.mixin全局混入向所有组件中分别混入两个钩子函数beforeCreate和destroy,在beforeCreate中给跟组件Vue的实例添加_router、_routerRoot和_route三个属性,三个属性分别指向VueRouter实例,根组件Vue实例,和当前路由对象,而在else分支中给所有子组件实例添加_routerRoot属性,并让该属性总是指向根组件Vue的实例。接着通过Object.defineProperty给Vue的原型对象上添加$router和¥route两个属性,对应的get函数中返回的则是_routerRoot上对应的_router和_route两个值,目的就是让所有组件都能够访问到根组件中的_router和_route。最后再注册两个全局组件RouterView和RouterLink

好了本次关于VueRouter 的install就分享到这里了。欢迎大佬们多多指点哦!