Vue 源码深入解析之 Vue Router、路由注册 和 VueRouter 对象

一、Vue-Router

  1. 路由,它的作用就是根据不同的路径映射到不同的视图。我们在用 Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。Vue-Router 的能力十分强大,它支持 hashhistoryabstract 三种路由方式,提供了 <router-link><router-view> 两种组件,还提供了简单的路由配置和一系列好用的 API

  2. 大部分人已经掌握了路由的基本使用,但使用的过程中也难免会遇到一些坑,那么这里我们就来深挖 Vue-Router 的实现细节,一旦我们掌握了它的实现原理,那么就能在开发中对路由的使用更加游刃有余。同样我们也会通过一些具体的示例来配合讲解,先来看一个最基本使用例子:

<div id="app">
  <h1>Hello App!</h1>
  <p>
    <!-- 使用 router-link 组件来导航. -->
    <!-- 通过传入 `to` 属性指定链接. -->
    <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
    <router-link to="/foo">Go to Foo</router-link>
    <router-link to="/bar">Go to Bar</router-link>
  </p>
  <!-- 路由出口 -->
  <!-- 路由匹配到的组件将渲染在这里 -->
  <router-view></router-view>
</div>
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'

Vue.use(VueRouter)

// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
const Foo = {
    
     template: '<div>foo</div>' }
const Bar = {
    
     template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
  {
    
     path: '/foo', component: Foo },
  {
    
     path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
    
    
  routes // (缩写)相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
    
    
  el: '#app',
  render(h) {
    
    
    return h(App)
  },
  router
})

这是一个非常简单的例子,接下来我们先从 Vue.use(VueRouter) 说起。

二、路由注册

  1. Vue 从它的设计上就是一个渐进式 JavaScript 框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router 就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下 Vue 通用的插件注册原理。

  2. Vue.useVue 提供了 Vue.use 的全局 API 来注册这些插件,所以我们先来分析一下它的实现原理,定义在 vue/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
    }

    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
  }
}
  1. Vue.use 接受一个 plugin 参数,并且维护了一个 _installedPlugins 数组,它存储所有注册过的 plugin;接着又会判断 plugin 有没有定义 install 方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把 plugin 存储到 installedPlugins 中。

  2. 由此可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去import Vue 了。

  3. 路由安装,Vue-Router 的入口文件是 src/index.js,其中定义了 VueRouter 类,也实现了 install 的静态方法:VueRouter.install = install,它的定义在 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) => {
    
    
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
    
    
      i(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
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
  1. 用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行 install 函数,为了确保 install 逻辑只执行一次,用了 install.installed 变量做已安装的标志位。另外用一个全局的 _Vue 来接收参数 Vue,因为作为 Vue 的插件对 Vue 对象是有依赖的,但又不能去单独去 import Vue,因为那样会增加包体积,所以就通过这种方式拿到 Vue 对象。

  2. Vue-Router 安装最重要的一步就是利用 Vue.mixin 去把 beforeCreatedestroyed 钩子函数注入到每一个组件中。Vue.mixin 的定义,在 vue/src/core/global-api/mixin.js 中:

export function initMixin (Vue: GlobalAPI) {
    
    
  Vue.mixin = function (mixin: Object) {
    
    
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
  1. 它的实现实际上非常简单,就是把要混入的对象通过 mergeOptions 合并到 Vueoptions 中,由于每个组件的构造函数都会在 extend 阶段合并 Vue.options 到自身的 options 中,所以也就相当于每个组件都定义了 mixin 定义的选项。

  2. 回到 Vue-Routerinstall 方法,先看混入的 beforeCreate 钩子函数,对于根 Vue 实例而言,执行该钩子函数时定义了 this._routerRoot 表示它自身;this._router 表示 VueRouter 的实例 router,它是在 new Vue 的时候传入的;另外执行了 this._router.init() 方法初始化 router,这个逻辑之后介绍,然后用 defineReactive 方法把 this._route 变成响应式对象,这个作用我们之后会介绍。而对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候 this._routerRoot 始终指向的离它最近的传入了 router 对象作为配置而实例化的父实例。

  3. 对于 beforeCreatedestroyed 钩子函数,它们都会执行 registerInstance 方法。接着给 Vue 原型上定义了 $router$route 两个属性的 get 方法,这就是为什么我们可以在组件实例上可以访问 this.$router 以及 this.$route。接着又通过 Vue.component 方法定义了全局的 <router-link><router-view> 两个组件,这也是为什么我们在写模板的时候可以使用这两个标签。最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。

  4. 总结:我们分析了 Vue-Router 的安装过程,Vue 编写插件的时候通常要提供静态的 install 方法,我们通过 Vue.use(plugin) 时候,就是在执行 install 方法。Vue-Routerinstall 方法会给每一个组件注入 beforeCreatedestoryed 钩子函数,在 beforeCreate 做一些私有属性定义和路由初始化工作,接下来我们就来分析一下 VueRouter 对象的实现和它的初始化工作。

三、VueRouter 对象

  1. VueRouter 的实现是一个类,我们先对它做一个简单地分析,它的定义在 src/index.js 中:
export default class VueRouter {
    
    
  static install: () => void;
  static version: string;

  app: any;
  apps: Array<any>;
  ready: boolean;
  readyCbs: Array<Function>;
  options: RouterOptions;
  mode: string;
  history: HashHistory | HTML5History | AbstractHistory;
  matcher: Matcher;
  fallback: boolean;
  beforeHooks: Array<?NavigationGuard>;
  resolveHooks: Array<?NavigationGuard>;
  afterHooks: Array<?AfterNavigationHook>;

  constructor (options: RouterOptions = {
    
    }) {
    
    
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    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}`)
        }
    }
  }

  match (
    raw: RawLocation,
    current?: Route,
    redirectedFrom?: Location
  ): Route {
    
    
    return this.matcher.match(raw, current, redirectedFrom)
  }

  get currentRoute (): ?Route {
    
    
    return this.history && this.history.current
  }

  init (app: any) {
    
    
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)

    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
      })
    })
  }

  beforeEach (fn: Function): Function {
    
    
    return registerHook(this.beforeHooks, fn)
  }

  beforeResolve (fn: Function): Function {
    
    
    return registerHook(this.resolveHooks, fn)
  }

  afterEach (fn: Function): Function {
    
    
    return registerHook(this.afterHooks, fn)
  }

  onReady (cb: Function, errorCb?: Function) {
    
    
    this.history.onReady(cb, errorCb)
  }

  onError (errorCb: Function) {
    
    
    this.history.onError(errorCb)
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    
    
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    
    
    this.history.replace(location, onComplete, onAbort)
  }

  go (n: number) {
    
    
    this.history.go(n)
  }

  back () {
    
    
    this.go(-1)
  }

  forward () {
    
    
    this.go(1)
  }

  getMatchedComponents (to?: RawLocation | Route): Array<any> {
    
    
    const route: any = to
      ? to.matched
        ? to
        : this.resolve(to).route
      : this.currentRoute
    if (!route) {
    
    
      return []
    }
    return [].concat.apply([], route.matched.map(m => {
    
    
      return Object.keys(m.components).map(key => {
    
    
        return m.components[key]
      })
    }))
  }

  resolve (
    to: RawLocation,
    current?: Route,
    append?: boolean
  ): {
    
    
    location: Location,
    route: Route,
    href: string,
    normalizedTo: Location,
    resolved: Route
  } {
    
    
    const location = normalizeLocation(
      to,
      current || this.history.current,
      append,
      this
    )
    const route = this.match(location, current)
    const fullPath = route.redirectedFrom || route.fullPath
    const base = this.history.base
    const href = createHref(base, fullPath, this.mode)
    return {
    
    
      location,
      route,
      href,
      normalizedTo: location,
      resolved: route
    }
  }

  addRoutes (routes: Array<RouteConfig>) {
    
    
    this.matcher.addRoutes(routes)
    if (this.history.current !== START) {
    
    
      this.history.transitionTo(this.history.getCurrentLocation())
    }
  }
}
  1. VueRouter 定义了一些属性和方法,我们先从它的构造函数看,当我们执行 new VueRouter 的时候做了哪些事情,如下所示:
constructor (options: RouterOptions = {
    
    }) {
    
    
  this.app = null
  this.apps = []
  this.options = options
  this.beforeHooks = []
  this.resolveHooks = []
  this.afterHooks = []
  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}`)
      }
  }
}
  1. 构造函数定义了一些属性,其中 this.app 表示根 Vue 实例,this.apps 保存持有 $options.router 属性的 Vue 实例,this.options 保存传入的路由配置,this.beforeHooks
    this.resolveHooksthis.afterHooks 表示一些钩子函数,我们之后会介绍,this.matcher 表示路由匹配器,我们之后会介绍,this.fallback 表示在浏览器不支持 history.pushState 的情况下,根据传入的 fallback 配置参数,决定是否回退到 hash 模式,this.mode 表示路由创建的模式,this.history 表示路由历史的具体的实现实例,它是根据 this.mode 的不同实现不同,它有 History 基类,然后不同的 history 实现都是继承 History

  2. 实例化 VueRouter 后会返回它的实例 router,我们在 new Vue 的时候会把 router 作为配置的属性传入,beforeCreate 混入的时候有这么一段代码:

beforeCreate() {
    
    
  if (isDef(this.$options.router)) {
    
    
    // ...
    this._router = this.$options.router
    this._router.init(this)
    // ...
  }
}  

所以组件在执行 beforeCreate 钩子函数的时候,如果传入了 router 实例,都会执行 router.init 方法:

init (app: any) {
    
    
  process.env.NODE_ENV !== 'production' && assert(
    install.installed,
    `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
    `before creating root instance.`
  )

  this.apps.push(app)

  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
    })
  })
}
  1. init 的逻辑很简单,它传入的参数是 Vue 实例,然后存储到 this.apps 中;只有根 Vue 实例会保存到 this.app 中,并且会拿到当前的 this.history,根据它的不同类型来执行不同逻辑,由于我们平时使用 hash 路由多一些,所以我们先看这部分逻辑,先定义了 setupHashListener 函数,接着执行了 history.transitionTo 方法,它是定义在 History 基类中,代码在 src/history/base.js
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    
    
  const route = this.router.match(location, this.current)
  // ...
}
  1. 我们先不着急去看 transitionTo 的具体实现,先看第一行代码,它调用了 this.router.match 函数:
match (
  raw: RawLocation,
  current?: Route,
  redirectedFrom?: Location
): Route {
    
    
  return this.matcher.match(raw, current, redirectedFrom)
}

实际上是调用了 this.matcher.match 方法去做匹配,所以接下来我们先来了解一下 matcher 的相关实现。

  1. 总结:我们大致对 VueRouter 类有了大致了解,知道了它的一些属性和方法,同时了解到在组件的初始化阶段,执行到 beforeCreate 钩子函数的时候会执行 router.init 方法,然后又会执行 history.transitionTo 方法做路由过渡,进而引出了 matcher 的概念,接下来我们先研究一下 matcher 的相关实现。

猜你喜欢

转载自blog.csdn.net/weixin_42614080/article/details/106962419