Vue-router技术原理

233 阅读5分钟

原文链接:www.yuque.com/wuhaosky/vu…

Vue-router是Vue官方的路由管理库。本文基于Vue-router@3.1.3版本分析Vue-router的技术原理。

一 install函数

和Vuex一样,Vue-router也是作为Vue的插件使用。当使用Vue.use(Vue-router)时,会执行Vue-router的install函数。

1.1 install函数的作用:

1.1.1 为所有Vue组件混合beforeCreate和destoryed钩子方法

在beforeCreate钩子里做了如下事情:

1.为所有Vue组件实例设置_routerRoot实例属性,指向Vue根实例;

2.为Vue根实例设置_router实例属性,指向this.$options.router,也就是Vue-router实例对象;

3.只在Vue根实例的beforeCreate钩子里,调用_router的init方法;

4.设置Vue根实例的_route实例属性为响应式,指向当前路由对象;

5.调用registerInstance方法,在此方法里判断,如果当前Vue组件实例对应的VNode对象,有定义data.registerRouteInstance方法,则执行data.registerRouteInstance方法。

在destroyed钩子里:

1.调用registerInstance方法,在此方法里判断,如果当前Vue组件实例对应的VNode对象,有定义data.registerRouteInstance方法,则执行data.registerRouteInstance方法。

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)
      // _router是当前路由,把_router设置为响应式,当RouterView组件执行render方法时,触发收集渲染函数的观察者;当_router变化时,通知渲染函数的观察者,触发重新渲染。
      Vue.util.defineReactive(this, '_route', this._router.history.current) 
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
    // 设置当前路由的vue组件实例为this
    registerInstance(this, this)
  },
  destroyed () {
    // 设置当前路由的vue组件实例为undefined
    registerInstance(this)
  }
})

1.1.2 为Vue原型增加属性routerrouter和route

给Vue增加只读的原型属性$router,指向Vue-router实例对象;

给Vue增加只读的原型属性$route,指向当前路由对象;

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

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

1.1.3 设置Vue全局组件RouterView和RouterLink

RouterView组件比较重要,它是路由出口,在RouterView组件中根据当前路由渲染相应的Vue组件。

1.1.4 设置Vue组件导航钩子的合并策略

设置Vue组件导航钩子(beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave)的合并策略与Vue生命周期钩子的合并策略相同。

1.2 思考:

1.2.1 设置Vue根实例的_router实例属性为响应式的目的是什么?

通过defineReactive对_route属性进行响应式处理,这一步是路由变更时视图可以重渲染的关键。

Vue应用初始化时,会执行RouterView组件的render方法,render方法里有这样一句代码const route = parent.route,对Vue实例的route,对Vue实例的route求值,就会进入routeget方法里,收集渲染函数的观察者到依赖收集器;当路由变更时,route的get方法里,收集渲染函数的观察者到依赖收集器;当路由变更时,route就会被重新赋值,就会进入$route的set方法,在set方法里,调用所有观察者的update方法,包括渲染函数的观察者,触发重渲染。这就是路由变更时视图可以重渲染的原理。

总结:当前路由引用发生改变,就会重新执行****RouterView组件render方法,挂载当前路由对应的Vue组件,从而实现视图更新

1.2.2 哪些Vue组件实例的VNode的data属性,有定义data.registerRouteInstance方法?

定义data.registerRouteInstance方法的是RouterView组件的直接子组件。也就是当前路由对应的组件。

1.2.3 Vue实例的router属性和router属性和route属性有什么区别?

router指向Vuerouter实例对象,router指向Vue-router实例对象,route指向当前路由对象。

二 RouterView组件

RouterView组件比较重要,它是路由出口,在RouterView组件中根据当前路由渲染相应的Vue组件。

2.1 RouterView组件做了如下几件事情:

2.1.1 根据当前路由挂载相应的Vue组件

RouterView组件根据当前路由挂载相应的Vue组件,这是最重要的工作。RouterView组件是Vue函数组件,它直接提供了render方法,在render方法使用h(component, data, children)挂载Vue组件。

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    // 省略
    const matched = route.matched
    const component = matched.components[name]
        // 省略
    return h(component, data, children)
  }
}

2.1.2 当路由引用变更时,更新视图

上文也有介绍,RouterView组件的render方法里通过const route = parent.route对当前路由进行求值,因为route对当前路由进行求值,因为route是响应式的,所以会触发依赖收集器收集RouterView组件的渲染函数的观察者。当$route引用变更时,通知RouterView组件的渲染函数的观察者,重新执行RouterView组件的render方法。从而挂载当前路由对应的Vue组件,实现视图更新。

2.1.3 设置当前路由对应的Vue组件的实例

目的是为了在导航守卫时,可以拿到当前路由和下一个路由对应的Vue组件实例。

三 VueRouter类

VueRouter构造函数中,根据路由配置参数(options.routes),创建路由匹配器matcher;根据路由模式参数(options.mode),创建历史记录器history。

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

3.1 路由匹配器matcher

matcher是由createMatcher函数生成,matcher含有两个方法,分别是match方法和addRoutes方法。match方法是返回当前的路由对象,addRoutes方法是往路由映射表里增加路由。路由匹配器主要负责路由匹配路由映射

3.1.1 路由匹配

路由匹配是通过path-to-regexp实现的。

3.1.2 路由映射

路由映射是通过nameMap、pathMap实现的,通过path/name找到对应的路由记录。

3.2 历史记录器history

Vue-router的构造函数里,会根据路由模式(默认是hash模式),创建对应的历史记录器。历史记录器主要负责路由跳转路由回退监听导航守卫。本文介绍的是hash模式。

3.2.1 路由跳转

路由跳转是通过window.history.pushState实现的。

具体的说,当使用this.$router.push({ name: 'xxx'})方式跳转到新的路由时,会执行历史记录器的transitionTo/confirmTransition方法,更新当前路由,并执行window.history.pushState方法,把新的URL添加到history栈中。

pushState() 方法与设置hash值的区别

HTML5开始提供了对history栈中内容的操作,通过history.pushState/replaceState实现添加URL到history栈中。在某种意义上,调用 pushState() 与 设置 window.location = "#foo" 类似,二者都会在当前页面创建并激活新的历史记录。但 pushState() 具有如下几条优点:

1.新的 URL 可以是与当前URL同源的任意URL 。而设置 window.location 仅当你只修改了哈希值时才保持同一个文件。

2.如果需要,可以不必改变URL就能创建一条历史记录。而设置 window.location = "#foo",只有在当前哈希不是 #foo 的情况下, 才会创建一个新的历史记录项。

3.我们可以为新的历史记录项关联任意数据。而基于哈希值的方式,则必须将所有相关数据编码到一个短字符串里。

3.2.2 路由回退监听

路由回退监听是通过监听popstate事件实现的。对于hash路由,如果不支持popstate事件,则监听hashchange事件。当监听到路由回退,调用历史记录器的transitionTo/confirmTransition方法更新当前路由,从而触发视图更新,更新为当前路由对应的Vue组件。

3.2.3 导航守卫

上文介绍过,不论是路由跳转,还是路由回退,都会执行历史记录器的transitionTo/confirmTransition方法,由此可以transitionTo/confirmTransition方法是非常重要的。在transitionTo/confirmTransition方法里,除了负责更新当前路由和添加新的URL到history栈中,它还负责导航守卫。

Vue-router提供了三种类型的导航守卫:

1.全局路由守卫

全局路由守卫有3个钩子方法,分别是:beforeHooks、beforeResolve、afterHooks。

2.组件内守卫

组件内的守卫有3个钩子方法,分别是:beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter。

3.路由独享的守卫

路由独享的守卫有1个钩子方法,它是:beforeEnter,这个钩子方法在定义路由配置时直接定义。

Vue-router完整的导航解析流程:

  1. 导航被触发;

  2. 在失活的组件里调用组件内的守卫 beforeRouteLeave;

  3. 调用全局路由守卫 beforeHooks;

  4. 在重用的组件里调用组件内的守卫 beforeRouteUpdate;

  5. 在被激活的组件里调用路由独享的守卫 beforeEnter;

  6. 解析异步路由组件。

  7. 在被激活的组件里调用组件内的守卫 beforeRouteEnter;

  8. 调用全局路由守卫 beforeResolve;

  9. 导航被确认,更新当前路由;

  10. 调用全局路由守卫 afterHooks;

  11. 触发 DOM 更新。

  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

3.3 思考:

3.3.1.如何监听params的变化?

params的变化并没有改变当前路由的引用,因此并不会触发进入/离开的导航守卫。你可以通过对 $route 对象进行深度观察来应对这些变化,或使用组件内守卫的beforeRouteUpdate钩子。

四 参考文章

【vue-进阶】之vue-router源码分析:juejin.im/post/684490…

vue-router从源码到实践到进阶:juejin.im/post/684490…

简单聊聊H5的pushState与replaceState:juejin.im/post/684490…

需要技术交流可以加微信。