Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue-router 源码分析 #16

Open
ZengTianShengZ opened this issue Apr 1, 2020 · 0 comments
Open

vue-router 源码分析 #16

ZengTianShengZ opened this issue Apr 1, 2020 · 0 comments

Comments

@ZengTianShengZ
Copy link
Owner

vue-router 源码分析

思考 🤔

1、vue-router 是怎么被初始化以及装载的
2、路由改变是怎么更新视图的

image

流程图

结合流程图来分析下面代码的执行流程

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
  ]
})

new Vue({
  router,
  template: `
    <div id="app">
      <router-view></router-view>
    </div>
  `
}).$mount('#app')

1、vue-router 初始化以及装载

vue-router 作为 Vue 的一个插件,通过全局方法 Vue.use() 的方式将 vue-router 挂载到 Vue 上。

对照流程图:

(1)、Vue.use(VueRouter) 首先会调用 install 方法,
(2)、利用 mixinVue 混入一个 beforeCreat 钩子,接着 beforeCreat 钩子做了哪些事情呢,
(3)、检测 Vueoptions 有没有 router 实例,有的话会通过 Object.defineProperty$router$route 变成响应式对象,
(4)、并挂载到 Vueprototype 上,
(5)、最后会往 Vue$options.components 注册两个全局组件 router-viewrouter-link

vue-router 挂载完,new VueRouter 实例化 VueRouter 有做了哪些事情呢。

对照流程图:

(1)、刚开始当然执行构造函数做一些初始化操作(流程图没体现出来)
(2)、接着会执行 createMatcher(options, routers) 根据 VueRouter 传入的 routes 数组创建 pathcomponent 的匹配规则
(3)、根据 VueRouter 传入的 mode 实例化 history 属性
(4)、定义一些钩子函数 beforeEach beforeResolve afterEach (流程图没体现出来)
(5)、定义一些路由方法 pushreplacegoback
(6)、定义 init() 方法,该方法是在Vue组件的 beforeCreat 钩子下会被调用,执行的时候会去匹配对应的 path 并找到对应的 component 再渲染到页面上(先有个印象就好)

2、路由改变是怎么触发匹配的组件更新的

前面做了这么一大波操作,那路由改变是怎么触发组件更新的呢,或者一个Vue项目初始化的时候是怎么加载路由组件的呢,具体流程是怎么样的。
如果你还没被流程图绕晕的话,再看一眼流程图的 黑色 线路。

前面我们提到了 Vue.use() 时会利用 mixinVue 混入一个 beforeCreat 钩子,注意这个钩子是全局钩子,意味着每个 Vue 组件都会调用这个钩子,包括根组件自身。

跟着黑色 线路走:

(1)、在根组件实例化后就会调用 beforeCreat 钩子,
(2)、接着会调用 route 实例的 init() 方法,init() 方法会执行 transitionTo 来改变对应的路由。
(3)、那对应的路由从哪里找呢,就是通过 match() 方法,去匹配我们上面有提到的 createMatcher 方法已经做好的 pathcomponent 的匹配规则。
(4)、通过 path 找到对应的 component 后会再执行 confirmTransition 来确认改变路由,
(5)、接着 updateRoute 跟更新路由,最后将匹配的 componentcurrent,
(6)、执行这些操作后其实是有个回调函数的,回调函数会跟新 _route 属性,该属性也是一个响应式对象,
(6)、_route 更新就会触发视图渲染,也就是调用 apprender() 函数,接着也触发了 router-view 组件的 render
(7)、router-view 组件的 render 会获取匹配的 component 渲染到视图上。

源码解析

通过流程图大致分析了 vue-router 的初始化和装载过程,以及路由跟新的流程。了解完大概思路下面进行源码层的分析就不会那么摸不着头绪了。

1、路由注册

故事的开端还是需要从路由的注册说起。

Vue.use(VueRouter)

vue-router 提供了一个 install 方法, Vue.use(VueRouter) 的时候会执行该方法。具体可看 vue 插件机制

import { install } from './install'

export default class VueRouter {
  static install: () => void;
  static version: string;
  // ...
}
VueRouter.install = install
VueRouter.version = '__VERSION__'

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

install 方法里头做了这些事情:

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  // 确保 install 调用一次
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把 Vue 赋值给全局变量给 VueRouter 用
  _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 钩子,注意,这意味着每个组件实例化时都会执行调用这个钩子
    beforeCreate () {
      // 判断是不是一个路由应用,有没有传入 router options
      if (isDef(this.$options.router)) {  // 根组件会执行
        this._routerRoot = this
        this._router = this.$options.router
        // 初始化路由
        this._router.init(this)
        // 将 _route 属性实现双向绑定,改变时会触发组件渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else { // 其他子组件会执行
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  // 能够在组件上使用  this.$router 获取 _router 实例
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  // 能够在组件上使用  this.$route 获取 _router 实例
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
  // 全局注册组件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}

2、VueRouter 实例

上面有提到 Vue 根实例会挂载 _router 属性,及 $options.router 实例,该实例就是 VueRouter 实例

 this._router = this.$options.router
const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
  ]
})

接下来分析 VueRouter 实例化做了哪些操作

export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    //...

    // 创建路由匹配对象
    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

    // 根据 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)、 createMatcher

初始化部分主要是 createMatcher 的功能, createMatcher 会创建路由匹配对象,及 pathcomponent 对应

export function createMatcher (routes: Array<RouteConfig>,router: VueRouter): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  console.log('==pathMap==', pathMap)
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  function match (raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    //...
    return _createRoute(record, location, redirectedFrom)
  }
  // ...
  return {
    match,
    addRoutes
  }
}

createMatcher 会返回一个 matchaddRoutes 方法, 调用 match 方法能根据 path 返回对应的组件。这里隐藏了一些细节的分析,直接
log pathMap 在控制台查看 createRouteMap 创建匹配的结果就好

image

可看到有这么一个结构

{
  '': {
    path: '',
    components: {}
  },
  '/foo': {
    path: '/foo',
    components: {}
  }
}

调用 match 方法能拿到对应的匹配内容了。

3、路由初始化

上面分析了 VueRouter 的实例化,那路由的初始化在哪里执行的呢,之前有提到 beforeCreate 会执行 this._router.init(this) ,这里就做了路由的初始化操作。

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

来看 init() 做了哪些事情

  init (app: any /* Vue component instance */) {
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history
    // 判断路由模式
    if (history instanceof HTML5History) {
      // 路由跳转
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      // 添加 hashchange 监听
      const setupHashListener = () => {
        history.setupListeners()
      }
      // 路由跳转
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    //注册监听事件, transitionTo 执行完会执行该回调,回调里头对组件的 _route 属性进行赋值,触发组件渲染
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

transitionTo 执行完会执行回调,回调里头去更新 _route 从而跟新视图,所以先再看下 transitionTo 做了什么。

  transitionTo (location: RawLocation,onComplete?: Function,onAbort?: Function) {
    // 获取匹配的路由信息
    const route = this.router.match(location, this.current)
    // 确认切换路由
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()

        // 执行回调
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        if (onAbort) {
          onAbort(err)
        }
        if (err && !this.ready) {
          this.ready = true
          this.readyErrorCbs.forEach(cb => {
            cb(err)
          })
        }
      }
    )
  }

transitionTo 主要是去获取匹配的路由信息,这个匹配路由信息上面已经分析过了。接着调用 confirmTransition ,执行完会调用一个回调 cb(route)
这个就会触发之前的监听事件:

history.listen(route => {
  this.apps.forEach((app) => {
    app._route = route // 更新 _route 从而触发组件更新
  })
})

4、vue-router

前面我们已经了解到了路由改变会经过一系列的操作,最终找到匹配的 component 从而跟新 route

 app._route = route // 更新 _route 从而触发组件更新

而在讲解【1、路由注册】这一步我们有提到

Vue.util.defineReactive(this, '_route', this._router.history.current)

_route 是一个响应式属性,更新后会触发相应的视图更新,并且【1、路由注册】这一步我们还提到

  // 全局注册组件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

install 注册 vue-router 时顺带注册了一个全局组件 RouterView 。接下来我们来分析下 _route 属性的改变是如何触发视图更新的,也就是

<router-view class="view"></router-view>

是如何渲染匹配的 component

router-view 源码

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    data.routerView = true
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route // 获取 _router
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // ... 

    //获取到匹配的 component
    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    // ...

    // 调用 createElement 函数 渲染匹配的组件
    return h(component, data, children)
  }
}

router-view 是一个函数组件, _router 改变触发 render 执行,render 内部获取到匹配的 component 进行渲染。

<router-view class="view"></router-view>

最后 router-view 被渲染成相应的组件。

小结

我们通过 vue-router 的挂载 -> 路由匹配 -> 组件渲染 的流程大致分析了 vue-router 的执行原理,我们隐藏和很多细节和功能没分析,比如
路由的 historyhash 模式、路由的具体匹配过程、路由守卫的功能等等。这些后续再具体分析,但了解了整体流程,接下来的细节功能就一步步拆开分析就清楚多了。

最后再放一张最开始的流程图

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant