vue-router源码分析(一)

255 阅读5分钟

一、路由注册

在vue当中,如果希望使用vue-router,会先通过import Router from 'vue-router'进行引入,vue-router和vue的打包是非常像的,他也是基于了rollup,rollup的相关配置,定义在了build文件夹下,其中的configs文件中,可以看到 input: resolve('src/index.js'),,也就是说他的入口,实际上在src/index.js,在src/index.js中,他通过export default class VueRouter{...}把VueRouter这个class暴露出来,所以我们import的实际上就是这个class。引入后,通过vue.use()进行注册,vue的use方法在initGlobalAPI函数中通过调用initUse(Vue)进行初始化,Vue.use函数,接受一个对象或函数,他会定义参数installedPlugins,赋值为this._installedPlugin||this._installedPlugins=[],如果是首次使用,他会对this._installedPlugins进行初始化,如果再次进入,那么会读取之前的数组。之后他会通过判断当前传入的插件,是否已经注册过,防止他重复注册。然后他会通过args这个数组,拿到赋值为传入的配置,再往数组的第一项放入当前的this,也就是Vue。之后会判断plugin是否有install这个方法,如果有则会执行,并把带Vue的args数组(其余的参数是之前传入的配置),作为参数传入install方法。否则的话如果plugin是一个函数,那么就执行plugin这个函数,并把args作为参数传入,把Vue插入args的目的是,插件当中如果想使用Vue的一些方法,那么他无需再去import Vue,而是通过传入的第一个参数,可以直接使用Vue。然后把当前的plugin,push到installedPlugins中,做一个保留。最后return this,所以vue.use是支持链式操作的。

// src/core/global-api/use.js
export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

调用vue.use(router)之后,他会调用VueRouter的install方法,首次执行,他会把install.installed置为true,并且把全局变量_Vue赋值为传入的Vue,如果多次执行install,那么他会通过判断这两个参数,来保证install只执行一次。把_Vue暴露出去,也可以方便其他的文件对Vue进行引入和使用。install函数首先通过Vue.mixin往全局混入了两个生命周期beforeCreatedestroyed,这样创建的每一个组件都拥有了这两个生命周期的扩展。 beforeCreate函数首先判断this.$options.router,在new Vue的时候,如果把router作为参数传入,那么他就会被扩展到this.$options上,如果没有在new Vue进行注册,那么这里就不会执行,也就是说,只有根的vue实例对象才会有这个options,如果是根的vue对象,他会把根的vue实例作为_routerRoot,然后把this._router赋值为this.$options.router,接着他调用了this.$options.router的init方法,并且把当前的vue实例作为参数传入。然后 通过Vue.util.defineReactive_route变成一个响应式对象。如果不是根的vue组件,他会把当前组件的this._routerRoot赋值为(this.$parent && this.$parent._routerRoot) || this也就是说,无论是根部的vue实例,还是子组件,都可以通过_routerRoot来访问到根部的vue实例。然后会调用registerInstance函数,这个函数是提供给router-view组件使用。destroyed函数也会调用registerInstance函数。混入了两个生命周期之后,通过Object.defineProperty在vue.prototype上定义$router$route这个两个变量,这也就是平时在组件当中使用的this.routerthis.router和this.route。之后通过vue.component注册 RouterViewRouterLink这两个全局组件,当平时开发当中,如果报错这两个组件未注册,那就是因为没有调用vue.use(router)来注册这两个组件。最后通过Vue.config.optionMergeStrategies拿到这created的合并策略,并赋值给beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate这三个生命周期,也就是说,这三个生命周期的合并策略和created相同

// src/install.js
export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
   ...
  }

  Vue.mixin({
    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 () {
      registerInstance(this)
    }
  })

  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
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

二、vueRouter对象

当我们通过new Router传入我们写入的router的配置,就会执行VueRouter的constructor函数,首先他会把传入的options保留在this.options上,之后通过let mode = options.mode || 'hash'来定义路由的mode,默认是hash模式。然后会定义this.fallback如果mode是history,他会判断!supportsPushStatesupportsPushState的目的是为了验证当前浏览器环境是否支持history模式,history模式,是html5的新特性,所以他需要判断是否支持,如果当前环境不支持,他会把this.fallback赋值为true,在下边的逻辑中,如果this.fallback,那么mode='hash'也就是如果你传入了mode为history,如果当前浏览器不支持,那么他会自动降级为hash路由(hash路由所有浏览器都支持),然后根据mode,对this.history进行赋值,history模式为new HTML5History,hash模式通过new HashHistory,这两个class都继承于classHistory

export default class VueRouter {
  ...
  constructor (options: RouterOptions = {}) {
    ...
    this.options = options
    ...
    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    ...
    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
      ...
    }
  }
  ...
}

之前提到,当判断当前的组件实例,是一个根部组件实例,会去执行this.$options.router.init(this)方法,也就是VueRouter的init方法,init方法首先会把传入的组件实例,push到VueRouter的this.apps中,接着判断是否有this.app,如果有,则return,首次进入的话,this.app为null,之后执行this.app = app这样就保证了如果执行多次init,那么只会执行一次后边的逻辑。之后他会判断,if(history instanceof HTML5History || history instanceof HashHistory)也就是说如果当前是hash模式或是history模式,那么会去执行history.transitionTo方法。history.transitionTo定义在class History中,其中定义了很多路由跳转相关的逻辑,他会先调用this.router.match(location, this.current),match方法return this.matcher.match(raw, current, redirectedFrom)来拿到跳转的路径。拿到路径之后调用了this.confirmTransition。执行完history.transitionTo,最后调用了history.listen函数。