vue-router原理解析

279 阅读3分钟

如何使用vue-router

阅读体验更好 & 带有思维导图的原文链接

vue项目使用配置:

// main.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import RouterDemo from './router.vue'
import { routes } from './router.js'

// 注册router插件
Vue.use(VueRouter)

// 创建router实例
const router = new VueRouter({ routes })

// 注入路由
new Vue({
    el: "#app",
    render: h => h(RouterDemo),
    router
})


// router.js
import Foo from './components/Foo.vue'
import Bar from './components/Bar.vue'
export const routes = [
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
]

vue.use()

vue通过vue.use()注册插件拓展能力,通过_installedPlugins数组维护所有注册过的插件

如果该插件已经注册过,会直接返回this即vue实例,实现链式编程

一般通过插件的install函数完成注册

// 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)
    // 执行插件中的install函数
    // 注意第一参数是vue,这样router中就可以不import vue了 可以减小包体积
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 注册完成加入_installedPlugins
    installedPlugins.push(plugin)
    return this
  }
}

vue-router的install

vue-router的install方法实现了哪些功能?

// vue-router的入口文件:src/index.js
export default class VueRouter {...}

// 实现了 install 的静态方法
// 当用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行 install 函数
VueRouter.install = install

// src/install.js
export let _Vue
export function install (Vue) {
  // 确保 install 逻辑只执行一次
  if (install.installed && _Vue === Vue) return
  install.installed = true
  _Vue = Vue

  // 利用 Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中
  Vue.mixin({
    beforeCreate () {
      // ...
    },
    destroyed () {
      // ...
    }
  })

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

思维导图

  // createRouteMap函数
  // 遍历 routes 为每一个 route 执行 addRouteRecord 方法生成一条记录
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
  })
  
// addRouteRecord 函数
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
    var record = {
      path: normalizedPath,
      regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
      components: route.components || { default: route.component },
      alias: route.alias
        ? typeof route.alias === 'string'
          ? [route.alias]
          : route.alias
        : [],
      instances: {},
      enteredCbs: {},
      name: name,
      parent: parent,
      matchAs: matchAs,
      redirect: route.redirect,
      beforeEnter: route.beforeEnter,
      meta: route.meta || {},
      props:
        route.props == null
          ? {}
          : route.components
            ? route.props
            : { default: route.props }
    };
    
    // 嵌套子路由
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
    
    // 为 pathList 和 pathMap 各添加一条记录。
    if (!pathMap[record.path]) {
       pathList.push(record.path)
       pathMap[record.path] = record
    }
 }

举例:

nameMap: {}
pathList: (2) ['/foo', '/bar']
pathMap : {
    /bar: {
        alias : []        beforeEnter : undefined         components : {
            default: {
                beforeCreate: [ƒ]
                beforeDestroy: [ƒ]
                name: "Bar"
                render: ƒ ()                staticRenderFns: []
                __file: "src/components/Bar.vue"
                _compiled: true
                [[Prototype]]: Object
            }
        }        enteredCbs : {}        instances : {}        matchAs : undefined         meta : {}        name : undefined         parent : undefined         path : "/bar"         props : {}        redirect : undefined         regex : /^/bar(?:/(?=$))?$/i         [[Prototype]]: Object
    },
    /foo: {
        alias: []
        beforeEnter: undefined
        components: {
            default: {
                    beforeCreate: [ƒ]
                    beforeDestroy: [ƒ]
                    name: "Foo"
                    render: ƒ ()                    staticRenderFns: []
                    __file: "src/components/Foo.vue"
                    _compiled: true
                    [[Prototype]]: Object
                }
        }
        enteredCbs: {}
        instances: {}
        matchAs: undefined
        meta: {}
        name: undefined
        parent: undefined
        path: "/foo"
        props: {}
        redirect: undefined
        regex: /^/foo(?:/(?=$))?$/i
        [[Prototype]]: Object
    }
} 

createRoute 函数

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
// 路由表
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query,
    params: location.params || {},
    fullPath: getFullPath(location, stringifyQuery),
    matched: record ? formatMatch(record) : []
  }
  
  return Object.freeze(route)
}

// 打平父子路由
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}

resolveQueue 函数

function resolveQueue (
  current: Array<RouteRecord>,
  next: Array<RouteRecord>
): {
  updated: Array<RouteRecord>,
  activated: Array<RouteRecord>,
  deactivated: Array<RouteRecord>
} {
  let i
  const max = Math.max(current.length, next.length)
  for (i = 0; i < max; i++) {
    if (current[i] !== next[i]) {
      break
    }
  }
  return {
    updated: next.slice(0, i),
    activated: next.slice(i),
    deactivated: current.slice(i)
  }
}

history.listen

    // index.js init 函数中
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
    
    // history base.js
  listen (cb: Function) {
    this.cb = cb
  }
  
  // confirmTransition 成功回调
  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }

路径切换

思考:路由切换的时候 先更新地址栏路由地址,然后渲染页面?

完整导航解析流程

  1. 导航被触发。
  1. 在失活的组件里调用 beforeRouteLeave 守卫。
  1. 调用全局的 beforeEach 守卫。
  1. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  1. 在路由配置里调用 beforeEnter
  1. 解析异步路由组件。
  1. 在被激活的组件里调用 beforeRouteEnter
  1. 调用全局的 beforeResolve 守卫(2.5+)。
  1. 导航被确认。
  1. 调用全局的 afterEach 钩子。
  1. 触发 DOM 更新。
  1. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

组件内的守卫

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

思维导图

runQueue

// 这是一个非常经典的异步函数队列化执行的模式
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

RouterView

思维导图

parent.$route

routerView所在组件的路由表

RouterLink

思维导图

router.resolve

解析当前link的路由信息

link颜色可自定义

关于我

欢迎关注我的公众号~ 不定期更新各种干活

微信搜索:思马斯徂