探索Vue.js底层源码——Vue-Router 插件注册及初始化

引言

对于单页应用来说,很重要的一点,它可以被称之为应用,则是因为它内部的跳转不是传统网页的刷新跳转,而是无刷新跳转(网上也称这种跳转方式的实现为 Pjax)。并且,对于 Vue.js 来说,它本身仅仅是提供了数据驱动的能力,它的其他能力则是通过 Vue.use() 来安装注册插件来实现能力的扩展。

Pjax 的优点:

  • 可以在页面切换间平滑过渡,增加 Loading 动画
  • 可以在各个页面间传递数据,不依赖 URL
  • 可以选择性的保留状态,例如音乐网站,切换页面时不会停止播放歌曲
  • 所有的标签都可以用来跳转,并不仅是 a 标签
  • 避免公共 JS 的反复指向,如无需再各个页面断开都判断是否登录过等等
  • 减少了请求体积,节省流量,加快页面的响应速度
  • 可以适应低版本浏览器上,对 SEO 也不会影响

这些优点,对于使用过 Vue-Router 的同学来说,应该都体验过。那么接下来我们看看 Vue-Router 实现注册和初始化(考虑到这个过程牵扯的东西比较多,所有整个 Vue-Router 的实现会分多篇博客讲解)

注册

首先,对于 Vue.js 而言,每一个插件都需要通过 Vue.use() 方法进行注册,Vue.use 定义在 src/core/global-api/use.js

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 中所有的插件都被保存在 installedPlugins 中,并且通过 installed.plugins.index() 来防止重复注册,并且关键的一点就是,通过 args.unshift() 方法来将 Vue 实例加入到插件的 arguments 中,方便插件使用 Vue 的一些能力。最终会执行插件的 install 方法或本身(如果为函数)来实现插件。

对于 Vue-Router 而言,install 方法是这样的

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

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      // 如果此时是根 Vue 实例
      if (isDef(this.$options.router)) {
      	// 将根实例赋值给 _routerRoot
        this._routerRoot = this
        // 获取根实例上的 router(这个就是我们初始化的 VueRouter 实例)
        this._router = this.$options.router
        // 调用 init 方法
        this._router.init(this)
        // 定义 _route 属性为根 Vue 实例上可响应的,值为 this._router.history.current
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else { // 如果此时不是根 Vue 实例
      	// 获取它的 parent(它肯定是根 Vue 实例)
        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
}

可以看出,在 install 方法中,也同样对自己的调用进行了判断,保证 install 方法只执行一次。并且,install 方法调用了 Vue.tr() 方法,在 beforeCreate(创建)和 destoryed 方法中执行了相应的方法。然后,在 Vue 实例上定义了 $router 属性,很明显 $router 属性只是只读的。

初始化

从注册的代码中可以看出,如果此时是根实例,则会走 init 这个方法,当然在此之前 constructor 中的代码肯定是执行完毕的。

constructor

constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // createMatcher() 会返回一个对象
    // 这个对象是这样的 {pathList, pathMap, nameMap}
    // pathList 存了路由中的所有路径
    // pathMap 存了
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    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
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

其实 constructor 的逻辑很简单,即根据传入的配置初始化 History,除开服务端渲染的 abstract,默认或不支持 History 则走 Hash。 并且,其中 createMatcher 方法会为路由建立映射和路由

createMatcher

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {
  	// 为 pathList pathMap nameMap 添加记录
    addRouteRecord(pathList, pathMap, nameMap, route)
  })

  // ensure wildcard routes are always at the end
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  ...
  return {
    pathList,
    pathMap,
    nameMap
  }
}

createMatcher 方法的作用就创建一个 Matcher,暴露三个属性,其中 pathList 用于存储路由的 path,pathMap 和 nameMap 用于路由的 path 或 name 到 record 的映射,record 的结构是这样的:

const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props:
      route.props == null
        ? {}
        : route.components
          ? route.props
          : { default: route.props }
  }

init

  init (app: any /* Vue component instance */) {
  	...

    this.apps.push(app)

    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      if (this.app === app) this.app = this.apps[0] || null
    })

    // main app previously initialized
    // return as we don't need to set up new history listener
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

此时 app 则是我们在注册的时候,传入的根 Vue 实例(即也可以理解为带 router 选项的实例),将根 Vue 实例 push 到数组 apps 中,通过 Vue 实例的能力 $once 来监听 destoryed 事件,该事件的作用在于组件 destoryed 时,移除 apps 数组中保存的 app(改组件的实例,需要注意的是这种组件也是需要具备 router 选项,否则就是根实例),并且最后走了一个逻辑,当前的 app(即 this.app) 等于 destory 中移除的 app 时,此时需要令 this.app 等于 apps 数组中的第一个或者是 null,当前至少存在一个 Router,以便复用

你可能感兴趣的:(探索Vue.js底层源码——Vue-Router 插件注册及初始化)