vue-router 源码分析(一) - 初始化和安装

Vue-router使用demo

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <router-view></router-view>
</div>

const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]
Vue.use(VueRouter)

const router = new VueRouter({
  routes 
})

const app = new Vue({
  router
}).$mount('#app')

new VueRouter 发生了什么?

VueRouterindex.js暴露的一个类, 所以我们看看他的构造函数都做了什么:

constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options      // 开发者传入的 { routes } 对象
    this.beforeHooks = []       // beforeEach钩子函数
    this.resolveHooks = []      // beforeResolve钩子函数
    this.afterHooks = []        // afterEach钩子函数
    
    // 生成matcher对象, 包含两个方法 {addRoutes, match}
    this.matcher = createMatcher(options.routes || [], this)
    
    // mode默认是hash
    let mode = options.mode || 'hash'
    
    // IE9 自动降级成hash模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    // 根据开发者传入的mode, 构造不同的类实例赋值给this.history
    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}`)
        }
    }
}

显而易见, 构造函数主要是做了一些类属性的初始化工作, 包括生成matcher对象, 以及根据mode初始化相应的history类, 这里暂时不探究着这些属性有何意义, 我们只需知道, 开发者按规范传入了一个{routes} 对象, 经过一系列的操作, 我们会得到一个VueRouter实例router, 而后, 我们将router传入到new Vue的初始化配置项中, 了解vuevm.$options属性的同学都知道, 我们可以在根实例通过this.$options.router取得router, 而这一步的实现跟vue.use(VueRouter)有着莫大的干系.

install函数

众所周知, vue.usevue使用插件的方式, vue会调用插件的install方法, 而VueRouter的install 函数长啥样呢?

注: 为方便阅读, 代码有删减

export function install (Vue) {

  // 重复安装检测
  if (install.installed && _Vue === Vue) return
  install.installed = true
 
  // 小技巧, _Vue保留Vue的引用, 将_Vue导出, 整个项目即可使用Vue
  _Vue = Vue
  
  // 全局混入beforeCreate钩子函数
  Vue.mixin({
    ---
  })
  
  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)
}

install函数它干了一件很重要的事, Vue.mixin(...), 全局混入了一个钩子函数, 每个组件都会走一遍这个钩子函数, 而这个钩子函数做了些什么呢?

beforeCreate () {
  // 初始化根实例上的router, 并定义响应式的_route
  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
  }
}

我们知道只有根实例上才有this.$options.router, 所以根实例初始化的时候, if条件 成立, 就会走到里面的代码:

  • this._routerRoot 就是根实例;
  • this._router就是router(VueRoute 实例);
  • 调用Vue.util.defineReactive在根实例上注册了一个响应式的属性_route;
  • 调用router实例上的init方法(这个以后再讲).

接着, 当其他vue实例初始化的时候, 例如, 我们一般会使用的<App/>组件, 在它的$options上就没有router属性, 所以它会走到else逻辑: 如果当前存在父实例, 则this._routerRoot 等于this.$parent._routerRoot, 不存在的话, this._routerRoot就指向当前实例, 这就意味着, 所有实例的_routerRoot在经历过beforeCreate这个钩子函数以后, 实例的this._routerRoot都会指向根实例的this, 也就是说所有实例的this._routerRoot都保留了对根实例的引用. 这有什么用呢? 且看以下代码:

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

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

答案已经显而易见, 通过es5的Object.defineProperty, 在Vue.prototype上定义了$router的取值函数, 返回根实例上的_router, 也就是VueRouter的实例router, 还定义了$route的取值函数, 返回的是根实例上的_route, 因此, 我们可以在任何一个组件使用this.$router就可以拿到router对象, 可以使用router上的方法、属性.

最后, intall 方法还注册了两个组件, <router-view/><router-link/>:

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

下回分解

createMatcher函数实现分析.

相关推荐