关于vue.js:vuerouter源码分析

60次阅读

共计 45625 个字符,预计需要花费 115 分钟才能阅读完成。

后端路由

路由的概念最开始是在后端呈现的,浏览器发送 URL,服务器接管到浏览器的申请时,通过解析不同的 URL 去拼接须要的 HTML 或模板,而后将后果返回到浏览器进行渲染。
服务器端路由有利有弊,益处是安全性更高,更严格控制页面展现。然而减少了服务器负荷,并且须要 reload 页面,用户体验不佳。

前端路由

前端路由保障只有一个 HTML 页面,在用户交互时不刷新和跳转页面的同时,为 SPA 中的每个视图展现模式匹配一个非凡的 url。在刷新、后退、后退和 SEO 时均通过这个非凡的 url 来实现。
前端路由实现形式

hash 模式

hash 值的变动不会导致浏览器向服务器发送申请,浏览器不发出请求,也就不会刷新页面。另外每次 hash 值的变动,还会触发 hashchange 事件,通过这个事件咱们能够晓得 hash 值产生了哪些变动,来实现更新页面局部内容的操作,浏览器的后退后退也能对其进行管制。所以在 H5 的 history 模式呈现之前,根本都是应用 hash 模式来实现前端路由。

history 模式

HTML5 引入了 history.pushState() 和 history.replaceState() 办法,他们别离能够增加和批改历史记录条目。这些办法通常与 window.onpopstate 配合应用。
history.pushState() 和 history.replaceState() 区别在于:

  • history.pushState() 在保留现有历史记录的同时,将 url 退出到历史记录中。
  • history.replaceState() 会将历史记录中的以后页面历史替换为 url。

因为 history.pushState() 和 history.replaceState() 能够扭转 url 时,不会刷新页面,所以在 HTML5 中的 history 具备了实现前端路由的能力。

比照

hash 长处:

  • 兼容性更好,能够兼容到 IE8
  • hash 的变动会在浏览器的 history 中减少一条记录,能够实现浏览器的后退和后退性能

hash 毛病:

  • 多了一个 #,url 整体不够好看
  • 会导致锚点性能生效
  • 雷同 hash 值不会触发动作将记录退出到历史栈中,而 pushState 能够

Vue-router 的实现形式

vue-router 依据不同的门路映射到不同的视图,它的能力非常弱小,反对 hash、history、abstract 三种路由形式,提供了 router-link 和 router-view 两种组件,还提供了简略的路由配置和一系列好用的 API。

路由注册

Vue 设计上就是一个渐进式的 JavaScript 框架,自身的外围是解决视图渲染问题,其它的能力就通过插件的形式来解决。Vue-router 就是官网保护的路由插件,在介绍它的注册实现之前,先来剖析下 Vue 通用插件注册原理。

Vue.use

Vue 提供了 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
  }
}

Vue.use 接管一个 plugin 参数,并且保护了一个_installedPlugins 数组,它存储所有注册过的 plugin;接着又会判断 plugin 有没有注册 install 办法,如果有就调用该办法,并且该办法执行的第一个参数是 Vue;最初把 plugin 存储到 installPlugins 中。

能够看到 Vue 提供的插件注册机制很简略,每个插件须要实现一个动态的 install 办法,当咱们执行 Vue.use 注册插件的时候,就会执行这个 install 办法,并且在这个 install 办法的第一个参数咱们能够拿到 Vue 对象,这样的益处就是作为插件的编写方不须要额定再 import Vue 了。

路由装置

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
}

当用户执行 Vue.use(VueRouter) 的时候,实际上就是执行 install 函数,为了确保 install 逻辑只执行一次,用了 install.installed 变量做已装置的标记位。另外用一个全局的_Vue 来接管参数 Vue,因为作为 Vue 插件对 Vue 对象是有依赖的,但又不能去独自 import Vue,因为那样会减少包体积,所以就通过这种形式拿到 Vue 对象。
Vue-router 装置最重要的一步就是利用 Vue.mixin 去把 beforeCreate 和 destoryed 钩子函数注入到每一个组件中。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
  }
}

它的实现实际上非常简单,就是把要混入的对象通过 mergeOptions 合并到 Vue 的 options 中,因为每个组件的构造函数都会在 extend 阶段合并 Vue.options 到本身的 options 中,所以也就相当于每个组件都定义了 mixin 定义的选项。

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

对于 beforeCreate 和 destroyed 钩子函数,它们都会执行 registerInstance 办法,这个办法的作用咱们也是之后会介绍。

接着给 Vue 原型上定义了 $router 和 $route 2 个属性的 get 办法,这就是为什么咱们能够在组件实例上能够拜访 this.$router 以及 this.$route,它们的作用之后介绍。

接着又通过 Vue.component 办法定义了全局的 <router-link> 和 <router-view> 2 个组件,这也是为什么咱们在写模板的时候能够应用这两个标签,它们的作用也是之后介绍。

最初定义了路由中的钩子函数的合并策略,和一般的钩子函数一样。

总结

那么到此为止,咱们剖析了 Vue-Router 的装置过程,Vue 编写插件的时候通常要提供动态的 install 办法,咱们通过 Vue.use(plugin) 时候,就是在执行 install 办法。Vue-Router 的 install 办法会给每一个组件注入 beforeCreate 和 destoryed 钩子函数,在 beforeCreate 做一些公有属性定义和路由初始化工作,下一节咱们就来剖析一下 VueRouter 对象的实现和它的初始化工作。

VueRouter 对象

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

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

构造函数定义了一些属性,其中 this.app 示意根 Vue 实例,this.apps 保留持有 $options.router 属性的 Vue 实例,this.options 保留传入的路由配置,this.beforeHooks、this.resolveHooks、this.afterHooks 示意一些钩子函数,咱们之后会介绍,this.matcher 示意路由匹配器,咱们之后会介绍,this.fallback 示意在浏览器不反对 history.pushState 的状况下,依据传入的 fallback 配置参数,决定是否回退到 hash 模式,this.mode 示意路由创立的模式,this.history 示意路由历史的具体的实现实例,它是依据 this.mode 的不同实现不同,它有 History 基类,而后不同的 history 实现都是继承 History。

实例化 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})
  })
}

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

咱们先不焦急去看 transitionTo 的具体实现,先看第一行代码,它调用了 this.router.match 函数:

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

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

总结

通过这一节的剖析,咱们大抵对 VueRouter 类有了大抵理解,晓得了它的一些属性和办法,同时理解到在组件的初始化阶段,执行到 beforeCreate 钩子函数的时候会执行 router.init 办法,而后又会执行 history.transitionTo 办法做路由过渡,进而引出了 matcher 的概念,接下来咱们先钻研一下 matcher 的相干实现。

matcher

matcher 相干的实现都在 src/create-matcher.js 中,咱们先来看一下 matcher 的数据结构:

export type Matcher = {match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
};

Matcher 返回了 2 个办法,match 和 addRoutes,在上一节咱们接触到了 match 办法,顾名思义它是做匹配,那么匹配的是什么,在介绍之前,咱们先理解路由中重要的 2 个概念,Loaction 和 Route,它们的数据结构定义在 flow/declarations.js 中。

  • Location
declare type Location = {
  _normalized?: boolean;
  name?: string;
  path?: string;
  hash?: string;
  query?: Dictionary<string>;
  params?: Dictionary<string>;
  append?: boolean;
  replace?: boolean;
}

Vue-Router 中定义的 Location 数据结构和浏览器提供的 window.location 局部构造有点相似,它们都是对 url 的结构化形容。举个例子:/abc?foo=bar&baz=qux#hello,它的 path 是 /abc,query 是 {foo:’bar’,baz:’qux’}。Location 的其余属性咱们之后会介绍。

  • Route
declare type Route = {
  path: string;
  name: ?string;
  hash: string;
  query: Dictionary<string>;
  params: Dictionary<string>;
  fullPath: string;
  matched: Array<RouteRecord>;
  redirectedFrom?: string;
  meta?: any;
}

Route 示意的是路由中的一条线路,它除了形容了相似 Loctaion 的 path、query、hash 这些概念,还有 matched 示意匹配到的所有的 RouteRecord。Route 的其余属性咱们之后会介绍。

createMatcher

在理解了 Location 和 Route 后,咱们来看一下 matcher 的创立过程:

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {const { pathList, pathMap, nameMap} = createRouteMap(routes)

  function addRoutes (routes) {createRouteMap(routes, pathList, pathMap, nameMap)
  }

  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {const location = normalizeLocation(raw, currentRoute, false, router)
    const {name} = location

    if (name) {const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {warn(record, `Route with name '${name}' does not exist`)
      }
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      if (typeof location.params !== 'object') {location.params = {}
      }

      if (currentRoute && typeof currentRoute.params === 'object') {for (const key in currentRoute.params) {if (!(key in location.params) && paramNames.indexOf(key) > -1) {location.params[key] = currentRoute.params[key]
          }
        }
      }

      if (record) {location.path = fillParams(record.path, location.params, `named route "${name}"`)
        return _createRoute(record, location, redirectedFrom)
      }
    } else if (location.path) {location.params = {}
      for (let i = 0; i < pathList.length; i++) {const path = pathList[i]
        const record = pathMap[path]
        if (matchRoute(record.regex, location.path, location.params)) {return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    return _createRoute(null, location)
  }

  // ...

  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {if (record && record.redirect) {return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom, router)
  }

  return {
    match,
    addRoutes
  }
}

createMatcher 接管 2 个参数,一个是 router,它是咱们 new VueRouter 返回的实例,一个是 routes,它是用户定义的路由配置,来看一下咱们之前举的例子中的配置:

const Foo = {template: '<div>foo</div>'}
const Bar = {template: '<div>bar</div>'}

const routes = [{ path: '/foo', component: Foo},
  {path: '/bar', component: Bar}
]

createMathcer 首先执行的逻辑是 const {pathList, pathMap, nameMap} = createRouteMap(routes) 创立一个路由映射表,createRouteMap 的定义在 src/create-route-map 中:

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>;
  pathMap: Dictionary<RouteRecord>;
  nameMap: Dictionary<RouteRecord>;
} {const pathList: Array<string> = oldPathList || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)

  routes.forEach(route => {addRouteRecord(pathList, pathMap, nameMap, route)
  })

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

createRouteMap 函数的指标是把用户的路由配置转换成一张路由映射表,它蕴含 3 个局部,pathList 存储所有的 path,pathMap 示意一个 path 到 RouteRecord 的映射关系,而 nameMap 示意 name 到 RouteRecord 的映射关系。那么 RouteRecord 到底是什么,先来看一下它的数据结构:

declare type RouteRecord = {
  path: string;
  regex: RouteRegExp;
  components: Dictionary<any>;
  instances: Dictionary<any>;
  name: ?string;
  parent: ?RouteRecord;
  redirect: ?RedirectOption;
  matchAs: ?string;
  beforeEnter: ?NavigationGuard;
  meta: any;
  props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}

它的创立是通过遍历 routes 为每一个 route 执行 addRouteRecord 办法生成一条记录,来看一下它的定义:

function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {const { path, name} = route
  if (process.env.NODE_ENV !== 'production') {assert(path != null, `"path" is required in a route configuration.`)
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(path || name)} cannot be a ` +
      `string id. Use an actual component instead.`
    )
  }

  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )

  if (typeof route.caseSensitive === 'boolean') {pathToRegexpOptions.sensitive = route.caseSensitive}

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

  if (route.children) {if (process.env.NODE_ENV !== 'production') {if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
          `When navigating to this named route (:to="{name:'${route.name}'"), ` +
          `the default child route will not be rendered. Remove the name from ` +
          `this route and use the name of the default child route for named ` +
          `links instead.`
        )
      }
    }
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }

  if (route.alias !== undefined) {const aliases = Array.isArray(route.alias)
      ? route.alias
      : [route.alias]

    aliases.forEach(alias => {
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/'
      )
    })
  }

  if (!pathMap[record.path]) {pathList.push(record.path)
    pathMap[record.path] = record
  }

  if (name) {if (!nameMap[name]) {nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
        `{name: "${name}", path: "${record.path}" }`
      )
    }
  }
}

咱们只看几个要害逻辑,首先创立 RouteRecord 的代码如下:

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

这里要留神几个点,path 是规范化后的门路,它会依据 parent 的 path 做计算;regex 是一个正则表达式的扩大,它利用了 path-to-regexp 这个工具库,把 path 解析成一个正则表达式的扩大,举个例子:

var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]

components 是一个对象,通常咱们在配置中写的 component 实际上这里会被转换成 {components: route.component};instances 示意组件的实例,也是一个对象类型;parent 示意父的 RouteRecord,因为咱们配置的时候有时候会配置子路由,所以整个 RouteRecord 也就是一个树型构造。

if (route.children) {
  // ...
  route.children.forEach(child => {
  const childMatchAs = matchAs
    ? cleanPath(`${matchAs}/${child.path}`)
    : undefined
  addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}

如果配置了 children,那么递归执行 addRouteRecord 办法,并把以后的 record 作为 parent 传入,通过这样的深度遍历,咱们就能够拿到一个 route 下的残缺记录。

if (!pathMap[record.path]) {pathList.push(record.path)
  pathMap[record.path] = record
}

为 pathList 和 pathMap 各增加一条记录。

if (name) {if (!nameMap[name]) {nameMap[name] = record
  }
  // ...
}

如果咱们在路由配置中配置了 name,则给 nameMap 增加一条记录。

因为 pathList、pathMap、nameMap 都是援用类型,所以在遍历整个 routes 过程中去执行 addRouteRecord 办法,会一直给他们增加数据。那么通过整个 createRouteMap 办法的执行,咱们失去的就是 pathList、pathMap 和 nameMap。其中 pathList 是为了记录路由配置中的所有 path,而 pathMap 和 nameMap 都是为了通过 path 和 name 能疾速查到对应的 RouteRecord。

再回到 createMatcher 函数,接下来就定义了一系列办法,最初返回了一个对象。

也就是说,matcher 是一个对象,它对外裸露了 match 和 addRoutes 办法。

addRoutes

addRoutes 办法的作用是动静增加路由配置,因为在理论开发中有些场景是不能提前把路由写死的,须要依据一些条件动静增加路由,所以 Vue-Router 也提供了这一接口:

function addRoutes (routes) {createRouteMap(routes, pathList, pathMap, nameMap)
}

addRoutes 的办法非常简略,再次调用 createRouteMap 即可,传入新的 routes 配置,因为 pathList、pathMap、nameMap 都是援用类型,执行 addRoutes 后会批改它们的值。

match

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {const location = normalizeLocation(raw, currentRoute, false, router)
  const {name} = location

  if (name) {const record = nameMap[name]
    if (process.env.NODE_ENV !== 'production') {warn(record, `Route with name '${name}' does not exist`)
    }
    if (!record) return _createRoute(null, location)
    const paramNames = record.regex.keys
      .filter(key => !key.optional)
      .map(key => key.name)

    if (typeof location.params !== 'object') {location.params = {}
    }

    if (currentRoute && typeof currentRoute.params === 'object') {for (const key in currentRoute.params) {if (!(key in location.params) && paramNames.indexOf(key) > -1) {location.params[key] = currentRoute.params[key]
        }
      }
    }

    if (record) {location.path = fillParams(record.path, location.params, `named route "${name}"`)
      return _createRoute(record, location, redirectedFrom)
    }
  } else if (location.path) {location.params = {}
    for (let i = 0; i < pathList.length; i++) {const path = pathList[i]
      const record = pathMap[path]
      if (matchRoute(record.regex, location.path, location.params)) {return _createRoute(record, location, redirectedFrom)
      }
    }
  }
  
  return _createRoute(null, location)
}

match 办法接管 3 个参数,其中 raw 是 RawLocation 类型,它能够是一个 url 字符串,也能够是一个 Location 对象;currentRoute 是 Route 类型,它示意以后的门路;redirectedFrom 和重定向相干,这里先疏忽。match 办法返回的是一个门路,它的作用是依据传入的 raw 和以后的门路 currentRoute 计算出一个新的门路并返回。

首先执行了 normalizeLocation,它的定义在 src/util/location.js 中:

export function normalizeLocation (
  raw: RawLocation,
  current: ?Route,
  append: ?boolean,
  router: ?VueRouter
): Location {let next: Location = typeof raw === 'string' ? { path: raw} : raw
  if (next.name || next._normalized) {return next}

  if (!next.path && next.params && current) {next = assign({}, next)
    next._normalized = true
    const params: any = assign(assign({}, current.params), next.params)
    if (current.name) {
      next.name = current.name
      next.params = params
    } else if (current.matched.length) {const rawPath = current.matched[current.matched.length - 1].path
      next.path = fillParams(rawPath, params, `path ${current.path}`)
    } else if (process.env.NODE_ENV !== 'production') {warn(false, `relative params navigation requires a current route.`)
    }
    return next
  }

  const parsedPath = parsePath(next.path || '')
  const basePath = (current && current.path) || '/'
  const path = parsedPath.path
    ? resolvePath(parsedPath.path, basePath, append || next.append)
    : basePath

  const query = resolveQuery(
    parsedPath.query,
    next.query,
    router && router.options.parseQuery
  )

  let hash = next.hash || parsedPath.hash
  if (hash && hash.charAt(0) !== '#') {hash = `#${hash}`
  }

  return {
    _normalized: true,
    path,
    query,
    hash
  }
}

normalizeLocation 办法的作用是依据 raw,current 计算出新的 location,它次要解决了 raw 的两种状况,一种是有 params 且没有 path,一种是有 path 的,对于第一种状况,如果 current 有 name,则计算出的 location 也有 name。

计算出新的 location 后,对 location 的 name 和 path 的两种状况做了解决。

  • name

有 name 的状况下就依据 nameMap 匹配到 record,它就是一个 RouterRecord 对象,如果 record 不存在,则匹配失败,返回一个空门路;而后拿到 record 对应的 paramNames,再比照 currentRoute 中的 params,把交加局部的 params 增加到 location 中,而后在通过 fillParams 办法依据 record.path 和 location.path 计算出 location.path,最初调用 _createRoute(record, location, redirectedFrom) 去生成一条新门路,该办法咱们之后会介绍。

  • path

通过 name 咱们能够很快的找到 record,然而通过 path 并不能,因为咱们计算后的 location.path 是一个实在门路,而 record 中的 path 可能会有 param,因而须要对所有的 pathList 做程序遍历,而后通过 matchRoute 办法依据 record.regex、location.path、location.params 匹配,如果匹配到则也通过 _createRoute(record, location, redirectedFrom) 去生成一条新门路。因为是程序遍历,所以咱们书写路由配置要留神门路的程序,因为写在后面的会优先尝试匹配。

最初咱们来看一下 _createRoute 的实现:

function _createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: Location
): Route {if (record && record.redirect) {return redirect(record, redirectedFrom || location)
  }
  if (record && record.matchAs) {return alias(record, location, record.matchAs)
  }
  return createRoute(record, location, redirectedFrom, router)
}

咱们先不思考 record.redirect 和 record.matchAs 的状况,最终会调用 createRoute 办法,它的定义在 src/uitl/route.js 中:

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery

  let query: any = location.query || {}
  try {query = clone(query)
  } catch (e) {}

  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) : []}
  if (redirectedFrom) {route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  return Object.freeze(route)
}

createRoute 能够依据 record 和 location 创立进去,最终返回的是一条 Route 门路,咱们之前也介绍过它的数据结构。在 Vue-Router 中,所有的 Route 最终都会通过 createRoute 函数创立,并且它最初是不能够被内部批改的。Route 对象中有一个十分重要属性是 matched,它通过 formatMatch(record) 计算而来:

function formatMatch (record: ?RouteRecord): Array<RouteRecord> {const res = []
  while (record) {res.unshift(record)
    record = record.parent
  }
  return res
}

能够看它是通过 record 循环向上找 parent,直到找到最外层,并把所有的 record 都 push 到一个数组中,最终返回的就是 record 的数组,它记录了一条线路上的所有 record。matched 属性十分有用,它为之后渲染组件提供了根据。

总结

那么到此,matcher 相干的主流程的剖析就完结了,咱们理解了 Location、Route、RouteRecord 等概念。并通过 matcher 的 match 办法,咱们会找到匹配的门路 Route,这个对 Route 的切换,组件的渲染都有十分重要的指导意义。下一节咱们会回到 transitionTo 办法,看一看门路的切换都做了哪些事件。

门路切换

history.transitionTo 是 Vue-Router 中十分重要的办法,当咱们切换路由线路的时候,就会执行到该办法,前一节咱们剖析了 matcher 的相干实现,晓得它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事件,接下来咱们来残缺剖析一下 transitionTo 的实现,它的定义在 src/history/base.js 中:

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 首先依据指标 location 和以后门路 this.current 执行 this.router.match 办法去匹配到指标的门路。这里 this.current 是 history 保护的以后门路,它的初始值是在 history 的构造函数中初始化的:

this.current = START

START 的定义在 src/util/route.js 中:

export const START = createRoute(null, {path: '/'})

这样就创立了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current,稍后咱们会看到。

拿到新的门路后,那么接下来就会执行 confirmTransition 办法去做真正的切换,因为这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有胜利回调函数和失败回调函数,先来看一下它的定义:

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  const current = this.current
  const abort = err => {if (isError(err)) {if (this.errorCbs.length) {this.errorCbs.forEach(cb => { cb(err) })
      } else {warn(false, 'uncaught error during route navigation:')
        console.error(err)
      }
    }
    onAbort && onAbort(err)
  }
  if (isSameRoute(route, current) &&
    route.matched.length === current.matched.length
  ) {this.ensureURL()
    return abort()}

  const {
    updated,
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)

  const queue: Array<?NavigationGuard> = [].concat(extractLeaveGuards(deactivated),
    this.router.beforeHooks,
    extractUpdateHooks(updated),
    activated.map(m => m.beforeEnter),
    resolveAsyncComponents(activated)
  )

  this.pending = route
  const iterator = (hook: NavigationGuard, next) => {if (this.pending !== route) {return abort()
    }
    try {hook(route, current, (to: any) => {if (to === false || isError(to)) {this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' && (
            typeof to.path === 'string' ||
            typeof to.name === 'string'
          ))
        ) {abort()
          if (typeof to === 'object' && to.replace) {this.replace(to)
          } else {this.push(to)
          }
        } else {next(to)
        }
      })
    } catch (e) {abort(e)
    }
  }

  runQueue(queue, iterator, () => {const postEnterCbs = []
    const isValid = () => this.current === route
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    const queue = enterGuards.concat(this.router.resolveHooks)
    runQueue(queue, iterator, () => {if (this.pending !== route) {return abort()
      }
      this.pending = null
      onComplete(route)
      if (this.router.app) {this.router.app.$nextTick(() => {postEnterCbs.forEach(cb => { cb() })
        })
      }
    })
  })
}

首先定义了 abort 函数,而后判断如果满足计算后的 route 和 current 是雷同门路的话,则间接调用 this.ensureUrl 和 abort,ensureUrl 这个函数咱们之后会介绍。

接着又依据 current.matched 和 route.matched 执行了 resolveQueue 办法解析出 3 个队列:

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

因为 route.matched 是一个 RouteRecord 的数组,因为门路是由 current 变向 route,那么就遍历比照 2 边的 RouteRecord,找到一个不一样的地位 i,那么 next 中从 0 到 i 的 RouteRecord 是两边都一样,则为 updated 的局部;从 i 到最初的 RouteRecord 是 next 独有的,为 activated 的局部;而 current 中从 i 到最初的 RouteRecord 则没有了,为 deactivated 的局部。

拿到 updated、activated、deactivated 3 个 ReouteRecord 数组后,接下来就是门路变换后的一个重要局部,执行一系列的钩子函数。

导航守卫

官网的说法叫导航守卫,实际上就是产生在路由门路切换的时候,执行的一系列钩子函数。

咱们先从整体上看一下这些钩子函数执行的逻辑,首先结构一个队列 queue,它实际上是一个数组;而后再定义一个迭代器函数 iterator;最初再执行 runQueue 办法来执行这个队列。咱们先来看一下 runQueue 的定义,在 src/util/async.js 中:

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

这是一个十分经典的异步函数队列化执行的模式,queue 是一个 NavigationGuard 类型的数组,咱们定义了 step 函数,每次依据 index 从 queue 中取一个 guard,而后执行 fn 函数,并且把 guard 作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行 step 函数,后退到下一个,留神这里的 fn 就是咱们方才的 iterator 函数,那么咱们再回到 iterator 函数的定义:

const iterator = (hook: NavigationGuard, next) => {if (this.pending !== route) {return abort()
  }
  try {hook(route, current, (to: any) => {if (to === false || isError(to)) {this.ensureURL(true)
        abort(to)
      } else if (
        typeof to === 'string' ||
        (typeof to === 'object' && (
          typeof to.path === 'string' ||
          typeof to.name === 'string'
        ))
      ) {abort()
        if (typeof to === 'object' && to.replace) {this.replace(to)
        } else {this.push(to)
        }
      } else {next(to)
      }
    })
  } catch (e) {abort(e)
  }
}

iterator 函数逻辑很简略,它就是去执行每一个 导航守卫 hook,并传入 route、current 和匿名函数,这些参数对应文档中的 to、from、next,当执行了匿名函数,会依据一些条件执行 abort 或 next,只有执行 next 的时候,才会后退到下一个导航守卫钩子函数中,这也就是为什么官网文档会说只有执行 next 办法来 resolve 这个钩子函数。

那么最初咱们来看 queue 是怎么结构的:

const queue: Array<?NavigationGuard> = [].concat(extractLeaveGuards(deactivated),
  this.router.beforeHooks,
  extractUpdateHooks(updated),
  activated.map(m => m.beforeEnter),
  resolveAsyncComponents(activated)
)

依照程序如下:

  1. 在失活的组件里调用来到守卫。
  2. 调用全局的 beforeEach 守卫。
  3. 在重用的组件里调用 beforeRouteUpdate 守卫
  4. 在激活的路由配置里调用 beforeEnter。
  5. 解析异步路由组件。

接下来咱们来别离介绍这 5 步的实现。

第一步是通过执行 extractLeaveGuards(deactivated),先来看一下 extractLeaveGuards 的定义:

function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

它外部调用了 extractGuards 的通用办法,能够从 RouteRecord 数组中提取各个阶段的守卫:

function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {const guards = flatMapComponents(records, (def, instance, match, key) => {const guard = extractGuard(def, name)
    if (guard) {return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  return flatten(reverse ? guards.reverse() : guards)
}

这里用到了 flatMapComponents 办法去从 records 中获取所有的导航,它的定义在 src/util/resolve-components.js 中:

export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
  return flatten(matched.map(m => {return Object.keys(m.components).map(key => fn(m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}

export function flatten (arr: Array<any>): Array<any> {return Array.prototype.concat.apply([], arr)
}

flatMapComponents 的作用就是返回一个数组,数组的元素是从 matched 里获取到所有组件的 key,而后返回 fn 函数执行的后果,flatten 作用是把二维数组拍平成一维数组。

那么对于 extractGuards 中 flatMapComponents 的调用,执行每个 fn 的时候,通过 extractGuard(def, name) 获取到组件中对应 name 的导航守卫:

function extractGuard (
  def: Object | Function,
  key: string
): NavigationGuard | Array<NavigationGuard> {if (typeof def !== 'function') {def = _Vue.extend(def)
  }
  return def.options[key]
}

获取到 guard 后,还会调用 bind 办法把组件的实例 instance 作为函数执行的上下文绑定到 guard 上,bind 办法的对应的是 bindGuard:

function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {if (instance) {return function boundRouteGuard () {return guard.apply(instance, arguments)
    }
  }
}

那么对于 extractLeaveGuards(deactivated) 而言,获取到的就是所有失活组件中定义的 beforeRouteLeave 钩子函数。

第二步是 this.router.beforeHooks,在咱们的 VueRouter 类中定义了 beforeEach 办法,在 src/index.js 中:

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

function registerHook (list: Array<any>, fn: Function): Function {list.push(fn)
  return () => {const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

当用户应用 router.beforeEach 注册了一个全局守卫,就会往 router.beforeHooks 增加一个钩子函数,这样 this.router.beforeHooks 获取的就是用户注册的全局 beforeEach 守卫。

第三步执行了 extractUpdateHooks(updated),来看一下 extractUpdateHooks 的定义:

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

和 extractLeaveGuards(deactivated) 相似,extractUpdateHooks(updated) 获取到的就是所有重用的组件中定义的 beforeRouteUpdate 钩子函数。

第四步是执行 activated.map(m => m.beforeEnter),获取的是在激活的路由配置中定义的 beforeEnter 函数。

第五步是执行 resolveAsyncComponents(activated) 解析异步组件,先来看一下 resolveAsyncComponents 的定义,在 src/util/resolve-components.js 中:

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null

    flatMapComponents(matched, (def, _, match, key) => {if (typeof def === 'function' && def.cid === undefined) {
        hasAsync = true
        pending++

        const resolve = once(resolvedDef => {if (isESModule(resolvedDef)) {resolvedDef = resolvedDef.default}
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {next()
          }
        })

        const reject = once(reason => {const msg = `Failed to resolve async component ${key}: ${reason}`
          process.env.NODE_ENV !== 'production' && warn(false, msg)
          if (!error) {error = isError(reason)
              ? reason
              : new Error(msg)
            next(error)
          }
        })

        let res
        try {res = def(resolve, reject)
        } catch (e) {reject(e)
        }
        if (res) {if (typeof res.then === 'function') {res.then(resolve, reject)
          } else {
            const comp = res.component
            if (comp && typeof comp.then === 'function') {comp.then(resolve, reject)
            }
          }
        }
      }
    })

    if (!hasAsync) next()}
}

resolveAsyncComponents 返回的是一个导航守卫函数,有规范的 to、from、next 参数。它的外部实现很简略,利用了 flatMapComponents 办法从 matched 中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和咱们之前剖析 Vue 加载异步组件很相似,加载胜利后会执行 match.components[key] = resolvedDef 把解析好的异步组件放到对应的 components 上,并且执行 next 函数。

这样在 resolveAsyncComponents(activated) 解析完所有激活的异步组件后,咱们就能够拿到这一次所有激活的组件。这样咱们在做完这 5 步后又做了一些事件:

runQueue(queue, iterator, () => {const postEnterCbs = []
  const isValid = () => this.current === route
  const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
  const queue = enterGuards.concat(this.router.resolveHooks)
  runQueue(queue, iterator, () => {if (this.pending !== route) {return abort()
    }
    this.pending = null
    onComplete(route)
    if (this.router.app) {this.router.app.$nextTick(() => {postEnterCbs.forEach(cb => { cb() })
      })
    }
  })
})
  1. 在被激活的组件里调用 beforeRouteEnter。
  2. 调用全局的 beforeResolve 守卫。
  3. 调用全局的 afterEach 钩子。

对于第六步有这些相干的逻辑:

const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)

function extractEnterGuards (
  activated: Array<RouteRecord>,
  cbs: Array<Function>,
  isValid: () => boolean): Array<?Function> {return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {return bindEnterGuard(guard, match, key, cbs, isValid)
  })
}

function bindEnterGuard (
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>,
  isValid: () => boolean): NavigationGuard {return function routeEnterGuard (to, from, next) {
    return guard(to, from, cb => {next(cb)
      if (typeof cb === 'function') {cbs.push(() => {poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}

function poll (
  cb: any,
  instances: Object,
  key: string,
  isValid: () => boolean) {if (instances[key]) {cb(instances[key])
  } else if (isValid()) {setTimeout(() => {poll(cb, instances, key, isValid)
    }, 16)
  }
}

extractEnterGuards 函数的实现也是利用了 extractGuards 办法提取组件中的 beforeRouteEnter 导航钩子函数,和之前不同的是 bind 办法的不同。文档中特意强调了 beforeRouteEnter 钩子函数中是拿不到组件实例的,因为当守卫执行前,组件实例还没被创立,然而咱们能够通过传一个回调给 next 来拜访组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调办法的参数:

beforeRouteEnter (to, from, next) {
  next(vm => {// 通过 `vm` 拜访组件实例})
}

来看一下这是怎么实现的。

在 bindEnterGuard 函数中,返回的是 routeEnterGuard 函数,所以在执行 iterator 中的 hook 函数的时候,就相当于执行 routeEnterGuard 函数,那么就会执行咱们定义的导航守卫 guard 函数,并且当这个回调函数执行的时候,首先执行 next 函数 rersolve 以后导航钩子,而后把回调函数的参数,它也是一个回调函数用 cbs 收集起来,其实就是收集到里面定义的 postEnterCbs 中,而后在最初会执行:

if (this.router.app) {this.router.app.$nextTick(() => {postEnterCbs.forEach(cb => { cb() })
  })
}

在根路由组件从新渲染后,遍历 postEnterCbs 执行回调,每一个回调执行的时候,其实是执行 poll(cb, match.instances, key, isValid) 办法,因为思考到一些了路由组件被套 transition 組件在一些缓动模式下不肯定能拿到实例,所以用一个轮询办法一直去判断,直到能获取到组件实例,再去调用 cb,并把组件实例作为参数传入,这就是咱们在回调函数中能拿到组件实例的起因。

第七步是获取 this.router.resolveHooks,这个和 this.router.beforeHooks 的获取相似,在咱们的 VueRouter 类中定义了 beforeResolve 办法:

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

当用户应用 router.beforeResolve 注册了一个全局守卫,就会往 router.resolveHooks 增加一个钩子函数,这样 this.router.resolveHooks 获取的就是用户注册的全局 beforeResolve 守卫。

第八步是在最初执行了 onComplete(route) 后,会执行 this.updateRoute(route) 办法:

updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  this.router.afterHooks.forEach(hook => {hook && hook(route, prev)
  })
}

同样在咱们的 VueRouter 类中定义了 afterEach 办法:

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

当用户应用 router.afterEach 注册了一个全局守卫,就会往 router.afterHooks 增加一个钩子函数,这样 this.router.afterHooks 获取的就是用户注册的全局 afterHooks 守卫。

那么至此咱们把所有导航守卫的执行剖析结束了,咱们晓得路由切换除了执行这些钩子函数,从表象上有 2 个中央会发生变化,一个是 url 发生变化,一个是组件发生变化。接下来咱们别离介绍这两块的实现原理。

url

当咱们点击 router-link 的时候,实际上最终会执行 router.push,如下:

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

this.history.push 函数,这个函数是子类实现的,不同模式下该函数的实现略有不同,咱们来看一下平时应用比拟多的 hash 模式该函数的实现,在 src/history/hash.js 中:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {const { current: fromRoute} = this
  this.transitionTo(location, route => {pushHash(route.fullPath)
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

push 函数会先执行 this.transitionTo 做门路切换,在切换实现的回调函数中,执行 pushHash 函数:

function pushHash (path) {if (supportsPushState) {pushState(getUrl(path))
  } else {window.location.hash = path}
}
supportsPushState 的定义在 src/util/push-state.js 中:export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {return false}

  return window.history && 'pushState' in window.history
})()

如果反对的话,则获取以后残缺的 url,执行 pushState 办法:

export function pushState (url?: string, replace?: boolean) {saveScrollPosition()
  const history = window.history
  try {if (replace) {history.replaceState({ key: _key}, '', url)
    } else {_key = genKey()
      history.pushState({key: _key}, '', url)
    }
  } catch (e) {window.location[replace ? 'replace' : 'assign'](url)
  }
}

pushState 会调用浏览器原生的 history 的 pushState 接口或者 replaceState 接口,更新浏览器的 url 地址,并把以后 url 压入历史栈中。

而后在 history 的初始化中,会设置一个监听器,监听历史栈的变动:

setupListeners () {
  const router = this.router
  const expectScroll = router.options.scrollBehavior
  const supportsScroll = supportsPushState && expectScroll

  if (supportsScroll) {setupScroll()
  }

  window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
    const current = this.current
    if (!ensureSlash()) {return}
    this.transitionTo(getHash(), route => {if (supportsScroll) {handleScroll(this.router, route, current, true)
      }
      if (!supportsPushState) {replaceHash(route.fullPath)
      }
    })
  })
}

当点击浏览器返回按钮的时候,如果曾经有 url 被压入历史栈,则会触发 popstate 事件,而后拿到以后要跳转的 hash,执行 transtionTo 办法做一次门路转换。

在应用 Vue-Router 开发我的项目的时候,关上调试页面 http://localhost:8080 后会主动把 url 批改为 http://localhost:8080/#/,这是怎么做到呢?原来在实例化 HashHistory 的时候,构造函数会执行 ensureSlash() 办法:

function ensureSlash (): boolean {const path = getHash()
  if (path.charAt(0) === '/') {return true}
  replaceHash('/' + path)
  return false
}

export function getHash (): string {
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  const href = window.location.href
  const index = href.indexOf('#')
  return index === -1 ? '' : href.slice(index + 1)
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function replaceHash (path) {if (supportsPushState) {replaceState(getUrl(path))
  } else {window.location.replace(getUrl(path))
  }
}

export function replaceState (url?: string) {pushState(url, true)
}

这个时候 path 为空,所以执行 replaceHash(‘/’ + path),而后外部会执行一次 getUrl,计算出来的新的 url 为 http://localhost:8080/#/,最终会执行 pushState(url, true),这就是 url 会扭转的起因。

组件

路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件,它的定义在 src/components/view.js 中。

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
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {if (parent.$vnode && parent.$vnode.data.routerView) {depth++}
      if (parent._inactive) {inactive = true}
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    if (inactive) {return h(cache[name], data, children)
    }

    const matched = route.matched[depth]
    if (!matched) {cache[name] = null
      return h()}

    const component = cache[name] = matched.components[name]
   
    data.registerRouteInstance = (vm, val) => {const current = matched.instances[name]
      if ((val && current !== vm) ||
        (!val && current === vm)
      ) {matched.instances[name] = val
      }
    }
    
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {matched.instances[name] = vnode.componentInstance
    }

    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {propsToPass = data.props = extend({}, propsToPass)
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {if (!component.props || !(key in component.props)) {attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }

    return h(component, data, children)
  }
}

<router-view> 是一个 functional 组件,它的渲染也是依赖 render 函数,那么 <router-view> 具体应该渲染什么组件呢,首先获取以后的门路:

const route = parent.$route

咱们之前剖析过,在 src/install.js 中,咱们给 Vue 的原型上定义了 $route:

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

而后在 VueRouter 的实例执行 router.init 办法的时候,会执行如下逻辑,定义在 src/index.js 中:

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

而 history.listen 办法定义在 src/history/base.js 中:

listen (cb: Function) {this.cb = cb}

而后在 updateRoute 的时候执行 this.cb:

updateRoute (route: Route) {
  //. ..
  this.current = route
  this.cb && this.cb(route)
  // ...
}

也就是咱们执行 transitionTo 办法最初执行 updateRoute 的时候会执行回调,而后会更新 this.apps 保留的组件实例的 _route 值,this.apps 数组保留的实例的特点都是在初始化的时候传入了 router 配置项,个别的场景数组只会保留根 Vue 实例,因为咱们是在 new Vue 传入了 router 实例。$route 是定义在 Vue.prototype 上。每个组件实例拜访 $route 属性,就是拜访根实例的 _route,也就是以后的路由线路。

<router-view> 是反对嵌套的,回到 render 函数,其中定义了 depth 的概念,它示意 <router-view> 嵌套的深度。每个 <router-view> 在渲染的时候,执行如下逻辑:

data.routerView = true
// ...
while (parent && parent._routerRoot !== parent) {if (parent.$vnode && parent.$vnode.data.routerView) {depth++}
  if (parent._inactive) {inactive = true}
  parent = parent.$parent
}

const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]

parent._routerRoot 示意的是根 Vue 实例,那么这个循环就是从以后的 <router-view> 的父节点向上找,始终找到根 Vue 实例,在这个过程,如果碰到了父节点也是 <router-view> 的时候,阐明 <router-view> 有嵌套的状况,depth++。遍历实现后,依据以后线路匹配的门路和 depth 找到对应的 RouteRecord,进而找到该渲染的组件。

除了找到了应该渲染的组件,还定义了一个注册路由实例的办法:

data.registerRouteInstance = (vm, val) => {const current = matched.instances[name]
  if ((val && current !== vm) ||
    (!val && current === vm)
  ) {matched.instances[name] = val
  }
}

给 vnode 的 data 定义了 registerRouteInstance 办法,在 src/install.js 中,咱们会调用该办法去注册路由的实例:

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 () {
    // ...
    registerInstance(this, this)
  },
  destroyed () {registerInstance(this)
  }
})

在混入的 beforeCreate 钩子函数中,会执行 registerInstance 办法,进而执行 render 函数中定义的 registerRouteInstance 办法,从而给 matched.instances[name] 赋值以后组件的 vm 实例。

render 函数的最初依据 component 渲染出对应的组件 vonde:

return h(component, data, children)

那么当咱们执行 transitionTo 来更改路由线路后,组件是如何从新渲染的呢?在咱们混入的 beforeCreate 钩子函数中有这么一段逻辑:

Vue.mixin({beforeCreate () {if (isDef(this.$options.router)) {Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
    // ...
  }
})

因为咱们把根 Vue 实例的 _route 属性定义成响应式的,咱们在每个 <router-view> 执行 render 函数的时候,都会拜访 parent.$route,如咱们之前剖析会拜访 this._routerRoot._route,触发了它的 getter,相当于 <router-view> 对它有依赖,而后再执行完 transitionTo 后,批改 app._route 的时候,又触发了 setter,因而会告诉 <router-view> 的渲染 watcher 更新,从新渲染组件。

Vue-Router 还内置了另一个组件 <router-link>,它反对用户在具备路由性能的利用中(点击)导航。通过 to 属性指定指标地址,默认渲染成带有正确链接的 标签,能够通过配置 tag 属性生成别的标签。另外,当指标路由胜利激活时,链接元素主动设置一个示意激活的 CSS 类名。

<router-link> 比起写死的 a 标签会好一些,理由如下:

无论是 HTML5 history 模式还是 hash 模式,它的体现行为统一,所以,当你要切换路由模式,或者在 IE9 降级应用 hash 模式,毋庸作任何变动。

在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再从新加载页面。

当你在 HTML5 history 模式下应用 base 选项之后,所有的 to 属性都不须要写(基门路)了。

那么接下来咱们就来剖析它的实现,它的定义在 src/components/link.js 中:

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route
    const {location, route, href} = router.resolve(this.to, current, this.append)

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const handler = e => {if (guardEvent(e)) {if (this.replace) {router.replace(location)
        } else {router.push(location)
        }
      }
    }

    const on = {click: guardEvent}
    if (Array.isArray(this.event)) {this.event.forEach(e => { on[e] = handler })
    } else {on[this.event] = handler
    }

    const data: any = {class: classes}

    if (this.tag === 'a') {
      data.on = on
      data.attrs = {href}
    } else {const a = findAnchor(this.$slots.default)
      if (a) {
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {data.on = on}
    }

    return h(this.tag, data, this.$slots.default)
  }
}

<router-link> 标签的渲染也是基于 render 函数,它首先做了路由解析:

const router = this.$router
const current = this.$route
const {location, route, href} = router.resolve(this.to, current, this.append)

router.resolve 是 VueRouter 的实例办法,它的定义在 src/index.js 中:

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

function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}

它先标准生成指标 location,再依据 location 和 match 通过 this.match 办法计算生成指标门路 route,而后再依据 base、fullPath 和 this.mode 通过 createHref 办法计算出最终跳转的 href。

解析完 router 取得指标 location、route、href 后,接下来对 exactActiveClass 和 activeClass 做解决,当配置 exact 为 true 的时候,只有当指标门路和以后门路齐全匹配的时候,会增加 exactActiveClass;而当指标门路蕴含以后门路的时候,会增加 activeClass。

接着创立了一个守卫函数:

const handler = e => {if (guardEvent(e)) {if (this.replace) {router.replace(location)
    } else {router.push(location)
    }
  }
}

function guardEvent (e) {if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  if (e.defaultPrevented) return
  if (e.button !== undefined && e.button !== 0) return 
  if (e.currentTarget && e.currentTarget.getAttribute) {const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
  if (e.preventDefault) {e.preventDefault()
  }
  return true
}

const on = {click: guardEvent}
  if (Array.isArray(this.event)) {this.event.forEach(e => { on[e] = handler })
  } else {on[this.event] = handler
  }

最终会监听点击事件或者其它能够通过 prop 传入的事件类型,执行 hanlder 函数,最终执行 router.push 或者 router.replace 函数,它们的定义在 src/index.js 中:

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

实际上就是执行了 history 的 push 和 replace 办法做路由跳转。

最初判断以后 tag 是否是 标签,<router-link> 默认会渲染成 标签,当然咱们也能够批改 tag 的 prop 渲染成其余节点,这种状况下会尝试找它子元素的 标签,如果有则把事件绑定到 标签上并增加 href 属性,否则绑定到外层元素自身。

总结

门路变动是路由中最重要的性能,咱们要记住以下内容:路由始终会保护以后的线路,路由切换的时候会把以后线路切换到指标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换结束后会把指标线路更新替换以后线路,这样就会作为下一次的门路切换的根据。

正文完
 0