这篇文章主要目的是用来梳理一下 Vue-router 的整个实现流程

首先,我们来看一下 Vue-router 源码的目录结构

  1. |——vue-router
  2. |——build // 构建脚本
  3. |——dist // 输出目录
  4. |——docs // 文档
  5. |——examples // 示例
  6. |——flow // 类型声明
  7. |——src // 项目源码
  8. |——components // 组件(view/link)
  9. |——history // Router 处理
  10. |——util // 工具库
  11. |——index.js // Router 入口
  12. |——install.js // Router 安装
  13. |——create-matcher.js // Route 匹配
  14. |——create-route-map.js // Route 映射

当然,我们主要关注的还是 src 目录下的文件。

router 注册

在我们开始使用 Vue-router 之前,要在主函数 main.js 里调用,也就是Vue.use(VueRouter),声明这个的目的就是利用了 Vue.js 的插件机制来安装 vue-router

当 Vue 通过 use() 来调用插件时,会调用插件的 install 方法,若插件没有 install 方法,则将插件本身作为函数来调用。

通过目录我们可以看出,install 方法是存在的,那我们首先就来看看这个文件。

  1. // src/install.js
  2. // 引入 router-view 和 router-link 组件
  3. import View from './components/view'
  4. import Link from './components/link'
  5. // export 一个私有 Vue 引用
  6. export let _Vue
  7. export function install (Vue) {
  8. // 判断是否重复安装插件
  9. if (install.installed && _Vue === Vue) return
  10. install.installed = true
  11. // 将 Vue 实例赋值给全局变量
  12. _Vue = Vue
  13. const isDef = v => v !== undefined
  14. const registerInstance = (vm, callVal) => {
  15. // 至少存在一个 VueComponent 时, _parentVnode 属性才存在
  16. let i = vm.$options._parentVnode
  17. if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
  18. i(vm, callVal)
  19. }
  20. }
  21. // 混入 beforeCreate 钩子函数,在调用该函数时,初始化路由
  22. Vue.mixin({
  23. beforeCreate () {
  24. // 判断组件是否有 router 对象,该对象只在根组件上有
  25. if (isDef(this.$options.router)) {
  26. // 将 router 的根组件指向 Vue 实例
  27. this._routerRoot = this
  28. this._router = this.$options.router
  29. // 初始化 router
  30. this._router.init(this)
  31. // 为 _route 属性实现双向绑定,触发组件渲染
  32. Vue.util.defineReactive(this, '_route', this._router.history.current)
  33. } else {
  34. // 用于 router-view 层级判断
  35. this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
  36. }
  37. registerInstance(this, this)
  38. },
  39. destroyed () {
  40. registerInstance(this)
  41. }
  42. })
  43. // 定义 Vue 原型方法 $router 和 $route 的 getter
  44. Object.defineProperty(Vue.prototype, '$router', {
  45. get () { return this._routerRoot._router }
  46. })
  47. Object.defineProperty(Vue.prototype, '$route', {
  48. get () { return this._routerRoot._route }
  49. })
  50. // 注册 router-view 和 router-link 组件
  51. Vue.component('RouterView', View)
  52. Vue.component('RouterLink', Link)
  53. const strats = Vue.config.optionMergeStrategies
  54. // use the same hook merging strategy for route hooks
  55. strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
  56. }

VueRouter 实例化

在安装过后,就要对 VueRouter 进行实例化操作。

  1. // src/index.js
  2. ...
  3. // 实例化时,主要做了两件事: 创建 matcher 对象; 创建 history 实例
  4. export default class VueRouter {
  5. static install: () => void;
  6. static version: string;
  7. app: any;
  8. apps: Array<any>;
  9. ready: boolean;
  10. readyCbs: Array<Function>;
  11. options: RouterOptions;
  12. mode: string;
  13. history: HashHistory | HTML5History | AbstractHistory;
  14. matcher: Matcher;
  15. fallback: boolean;
  16. beforeHooks: Array<?NavigationGuard>;
  17. resolveHooks: Array<?NavigationGuard>;
  18. afterHooks: Array<?AfterNavigationHook>;
  19. constructor (options: RouterOptions = {}) {
  20. // 配置路由对象
  21. this.app = null
  22. this.apps = []
  23. this.options = options
  24. this.beforeHooks = []
  25. this.resolveHooks = []
  26. this.afterHooks = []
  27. this.matcher = createMatcher(options.routes || [], this)
  28. // 对 mode 做检测 options.fallback 是新增属性,表示是否对不支持 HTML5 history 的浏览器做降级处理
  29. let mode = options.mode || 'hash'
  30. this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  31. if (this.fallback) {
  32. // 兼容不支持 history
  33. mode = 'hash'
  34. }
  35. if (!inBrowser) {
  36. // 非浏览器模式
  37. mode = 'abstract'
  38. }
  39. this.mode = mode
  40. // 根据 mode 创建 history 实例
  41. switch (mode) {
  42. case 'history':
  43. this.history = new HTML5History(this, options.base)
  44. break
  45. case 'hash':
  46. this.history = new HashHistory(this, options.base, this.fallback)
  47. break
  48. case 'abstract':
  49. this.history = new AbstractHistory(this, options.base)
  50. break
  51. default:
  52. if (process.env.NODE_ENV !== 'production') {
  53. assert(false, `invalid mode: ${mode}`)
  54. }
  55. }
  56. }
  57. // 返回匹配的 route
  58. match (
  59. raw: RawLocation,
  60. current?: Route,
  61. redirectedFrom?: Location
  62. ): Route {
  63. return this.matcher.match(raw, current, redirectedFrom)
  64. }
  65. ...
  66. }

创建路由匹配对象

  1. // src/create-matcher.js
  2. // 定义 Matcher 类型
  3. export type Matcher = {
  4. match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  5. addRoutes: (routes: Array<RouteConfig>) => void;
  6. };
  7. export function createMatcher (
  8. routes: Array<RouteConfig>,
  9. router: VueRouter
  10. ): Matcher {
  11. // 根据 routes 创建路由映射表
  12. const { pathList, pathMap, nameMap } = createRouteMap(routes)
  13. // 添加路由函数
  14. function addRoutes (routes) {
  15. createRouteMap(routes, pathList, pathMap, nameMap)
  16. }
  17. // 路由匹配
  18. function match (
  19. raw: RawLocation,
  20. currentRoute?: Route,
  21. redirectedFrom?: Location
  22. ): Route {
  23. const location = normalizeLocation(raw, currentRoute, false, router)
  24. const { name } = location
  25. // 如果 name 存在的话,就去 name map 中去找到这条路由记录
  26. if (name) {
  27. const record = nameMap[name]
  28. if (process.env.NODE_ENV !== 'production') {
  29. warn(record, `Route with name '${name}' does not exist`)
  30. }
  31. // 如果没有这条路由记录就去创建一条路由对象
  32. if (!record) return _createRoute(null, location)
  33. const paramNames = record.regex.keys
  34. .filter(key => !key.optional)
  35. .map(key => key.name)
  36. if (typeof location.params !== 'object') {
  37. location.params = {}
  38. }
  39. if (currentRoute && typeof currentRoute.params === 'object') {
  40. for (const key in currentRoute.params) {
  41. if (!(key in location.params) && paramNames.indexOf(key) > -1) {
  42. location.params[key] = currentRoute.params[key]
  43. }
  44. }
  45. }
  46. if (record) {
  47. location.path = fillParams(record.path, location.params, `named route "${name}"`)
  48. return _createRoute(record, location, redirectedFrom)
  49. }
  50. } else if (location.path) {
  51. location.params = {}
  52. for (let i = 0; i < pathList.length; i++) {
  53. const path = pathList[i]
  54. const record = pathMap[path]
  55. if (matchRoute(record.regex, location.path, location.params)) {
  56. return _createRoute(record, location, redirectedFrom)
  57. }
  58. }
  59. }
  60. // no match
  61. return _createRoute(null, location)
  62. }
  63. ...
  64. // 根据不同的条件去创建路由对象;
  65. function _createRoute (
  66. record: ?RouteRecord,
  67. location: Location,
  68. redirectedFrom?: Location
  69. ): Route {
  70. if (record && record.redirect) {
  71. return redirect(record, redirectedFrom || location)
  72. }
  73. if (record && record.matchAs) {
  74. return alias(record, location, record.matchAs)
  75. }
  76. return createRoute(record, location, redirectedFrom, router)
  77. }
  78. // 返回 matcher 对象
  79. return {
  80. match,
  81. addRoutes
  82. }
  83. }

根据源码我们可以看出,createMatcher 就是根据传入的 routes 生成一个 map 表,并且返回 match 函数以及一个可以增加路由配置项 addRoutes 函数。

我们继续看 route-map 的生成

  1. // src/create-route-map.js
  2. export function createRouteMap (
  3. routes: Array<RouteConfig>,
  4. oldPathList?: Array<string>,
  5. oldPathMap?: Dictionary<RouteRecord>,
  6. oldNameMap?: Dictionary<RouteRecord>
  7. ): {
  8. pathList: Array<string>;
  9. pathMap: Dictionary<RouteRecord>;
  10. nameMap: Dictionary<RouteRecord>;
  11. } {
  12. // 创建映射列表
  13. // the path list is used to control path matching priority
  14. const pathList: Array<string> = oldPathList || []
  15. // $flow-disable-line
  16. const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  17. // $flow-disable-line
  18. const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  19. // 遍历路由配置,为每个配置添加路由记录
  20. routes.forEach(route => {
  21. addRouteRecord(pathList, pathMap, nameMap, route)
  22. })
  23. // ensure wildcard routes are always at the end 通配符一直保持在最后
  24. for (let i = 0, l = pathList.length; i < l; i++) {
  25. if (pathList[i] === '*') {
  26. pathList.push(pathList.splice(i, 1)[0])
  27. l--
  28. i--
  29. }
  30. }
  31. return {
  32. pathList,
  33. pathMap,
  34. nameMap
  35. }
  36. }
  37. // 添加路由记录
  38. function addRouteRecord (
  39. pathList: Array<string>,
  40. pathMap: Dictionary<RouteRecord>,
  41. nameMap: Dictionary<RouteRecord>,
  42. route: RouteConfig,
  43. parent?: RouteRecord,
  44. matchAs?: string
  45. ) {
  46. const { path, name } = route
  47. if (process.env.NODE_ENV !== 'production') {
  48. assert(path != null, `"path" is required in a route configuration.`)
  49. assert(
  50. typeof route.component !== 'string',
  51. `route config "component" for path: ${String(path || name)} cannot be a ` +
  52. `string id. Use an actual component instead.`
  53. )
  54. }
  55. const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  56. // 序列化 path 用 / 替换
  57. const normalizedPath = normalizePath(
  58. path,
  59. parent,
  60. pathToRegexpOptions.strict
  61. )
  62. // 对路径进行正则匹配是否区分大小写
  63. if (typeof route.caseSensitive === 'boolean') {
  64. pathToRegexpOptions.sensitive = route.caseSensitive
  65. }
  66. // 创建一个路由记录对象
  67. const record: RouteRecord = {
  68. path: normalizedPath, // 路径
  69. regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), //转化为匹配数组
  70. components: route.components || { default: route.component }, // 关联数组
  71. instances: {}, // 实例
  72. name, // 名称
  73. parent, // 父级 router
  74. matchAs,
  75. redirect: route.redirect, // 跳转
  76. beforeEnter: route.beforeEnter, // deforeEnter 钩子函数
  77. meta: route.meta || {}, // 附加参数
  78. props: route.props == null // prop 属性
  79. ? {}
  80. : route.components
  81. ? route.props
  82. : { default: route.props }
  83. }
  84. // 递归子路由
  85. if (route.children) {
  86. // Warn if route is named, does not redirect and has a default child route.
  87. // If users navigate to this route by name, the default child will
  88. // not be rendered (GH Issue #629)
  89. if (process.env.NODE_ENV !== 'production') {
  90. if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
  91. warn(
  92. false,
  93. `Named Route '${route.name}' has a default child route. ` +
  94. `When navigating to this named route (:to="{name: '${route.name}'"), ` +
  95. `the default child route will not be rendered. Remove the name from ` +
  96. `this route and use the name of the default child route for named ` +
  97. `links instead.`
  98. )
  99. }
  100. }
  101. route.children.forEach(child => {
  102. const childMatchAs = matchAs
  103. ? cleanPath(`${matchAs}/${child.path}`)
  104. : undefined
  105. addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
  106. })
  107. }
  108. // 别名
  109. if (route.alias !== undefined) {
  110. const aliases = Array.isArray(route.alias)
  111. ? route.alias
  112. : [route.alias]
  113. aliases.forEach(alias => {
  114. const aliasRoute = {
  115. path: alias,
  116. children: route.children
  117. }
  118. addRouteRecord(
  119. pathList,
  120. pathMap,
  121. nameMap,
  122. aliasRoute,
  123. parent,
  124. record.path || '/' // matchAs
  125. )
  126. })
  127. }
  128. // 按路径存储
  129. if (!pathMap[record.path]) {
  130. pathList.push(record.path)
  131. pathMap[record.path] = record
  132. }
  133. // 处理命名路由,按照名字存储
  134. if (name) {
  135. if (!nameMap[name]) {
  136. nameMap[name] = record
  137. } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
  138. warn(
  139. false,
  140. `Duplicate named routes definition: ` +
  141. `{ name: "${name}", path: "${record.path}" }`
  142. )
  143. }
  144. }
  145. }

从上述代码可以看出, create-route-map.js 的主要功能是根据用户的 routes 配置的 pathalias以及 name 来生成对应的路由记录, 方便后续匹配对应。

History 实例化

vueRouter 提供了 HTML5History、HashHistory、AbstractHistory 三种方式,根据不同的 mode 和实际环境去实例化 History

  1. // src/history/base.js
  2. export class History {
  3. router: Router; // router 对象
  4. base: string; // 基准路径
  5. current: Route; // 当前路径
  6. pending: ?Route;
  7. cb: (r: Route) => void;
  8. ready: boolean;
  9. readyCbs: Array<Function>;
  10. readyErrorCbs: Array<Function>;
  11. errorCbs: Array<Function>;
  12. // 子类
  13. // implemented by sub-classes
  14. +go: (n: number) => void;
  15. +push: (loc: RawLocation) => void;
  16. +replace: (loc: RawLocation) => void;
  17. +ensureURL: (push?: boolean) => void;
  18. +getCurrentLocation: () => string;
  19. constructor (router: Router, base: ?string) {
  20. this.router = router
  21. this.base = normalizeBase(base) // 返回基准路径
  22. // start with a route object that stands for "nowhere"
  23. this.current = START // 当前 route
  24. this.pending = null
  25. this.ready = false
  26. this.readyCbs = []
  27. this.readyErrorCbs = []
  28. this.errorCbs = []
  29. }
  30. ...
  31. // 路由化操作
  32. transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  33. const route = this.router.match(location, this.current) // 找到匹配路由
  34. this.confirmTransition(route, () => { // 确认是否转化
  35. this.updateRoute(route) // 更新 route
  36. onComplete && onComplete(route)
  37. this.ensureURL()
  38. // fire ready cbs once
  39. if (!this.ready) {
  40. this.ready = true
  41. this.readyCbs.forEach(cb => { cb(route) })
  42. }
  43. }, err => {
  44. if (onAbort) {
  45. onAbort(err)
  46. }
  47. if (err && !this.ready) {
  48. this.ready = true
  49. this.readyErrorCbs.forEach(cb => { cb(err) })
  50. }
  51. })
  52. }
  53. // 确认是否转化路由
  54. confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  55. const current = this.current
  56. const abort = err => {
  57. if (isError(err)) {
  58. if (this.errorCbs.length) {
  59. this.errorCbs.forEach(cb => { cb(err) })
  60. } else {
  61. warn(false, 'uncaught error during route navigation:')
  62. console.error(err)
  63. }
  64. }
  65. onAbort && onAbort(err)
  66. }
  67. // 判断如果前后是一个路由,则不发生变化
  68. if (
  69. isSameRoute(route, current) &&
  70. // in the case the route map has been dynamically appended to
  71. route.matched.length === current.matched.length
  72. ) {
  73. this.ensureURL()
  74. return abort()
  75. }
  76. ...
  77. updateRoute (route: Route) {
  78. const prev = this.current
  79. this.current = route
  80. this.cb && this.cb(route)
  81. this.router.afterHooks.forEach(hook => {
  82. hook && hook(route, prev)
  83. })
  84. }
  85. }

在基础的挂载和各种实例都弄完之后,我们就可以从 init 开始入手了

init()

  1. // src/index.js
  2. init (app: any /* Vue component instance */) {
  3. process.env.NODE_ENV !== 'production' && assert(
  4. install.installed,
  5. `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
  6. `before creating root instance.`
  7. )
  8. // 从 install 知道 这个 app 是我们实例化的 Vue 实例
  9. this.apps.push(app)
  10. // main app already initialized.
  11. if (this.app) {
  12. return
  13. }
  14. // 将 vueRouter 内部的 app 指向 Vue 实例
  15. this.app = app
  16. const history = this.history
  17. // 对 HTML5History 和 HashHistory 进行特殊处理
  18. if (history instanceof HTML5History) {
  19. history.transitionTo(history.getCurrentLocation())
  20. } else if (history instanceof HashHistory) {
  21. // 监听路由变化
  22. const setupHashListener = () => {
  23. history.setupListeners()
  24. }
  25. history.transitionTo(
  26. history.getCurrentLocation(),
  27. setupHashListener,
  28. setupHashListener
  29. )
  30. }
  31. // 设置路由改变的时候监听
  32. history.listen(route => {
  33. this.apps.forEach((app) => {
  34. app._route = route
  35. })
  36. })
  37. }

我们这里看出,对于 HTML5History 和 HashHistory 进行了不同的处理,因为此时需要根据浏览器地址栏里的 path 或者 hash 来匹配对应的路由。尽管有些不同,但是都调用了 transitionTo 方法,让我们来看一下这个方法。

  1. // src/history/base.js
  2. transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  3. // location 为当前路由
  4. // 这里调用了 match 方法来获取匹配的路由对象,this.current 指我们保存的当前状态的对象
  5. const route = this.router.match(location, this.current)
  6. this.confirmTransition(route, () => {
  7. // 更新当前对象
  8. this.updateRoute(route)
  9. onComplete && onComplete(route)
  10. // 调用子类方法,用来更新 URL
  11. this.ensureURL()
  12. // fire ready cbs once
  13. // 调用成功后的ready的回调函数
  14. if (!this.ready) {
  15. this.ready = true
  16. this.readyCbs.forEach(cb => { cb(route) })
  17. }
  18. }, err => {
  19. if (onAbort) {
  20. onAbort(err)
  21. }
  22. // 调用失败的err回调函数;
  23. if (err && !this.ready) {
  24. this.ready = true
  25. this.readyErrorCbs.forEach(cb => { cb(err) })
  26. }
  27. })
  28. }
  29. // 确认跳转
  30. confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  31. const current = this.current
  32. const abort = err => {
  33. if (isError(err)) {
  34. if (this.errorCbs.length) {
  35. this.errorCbs.forEach(cb => { cb(err) })
  36. } else {
  37. warn(false, 'uncaught error during route navigation:')
  38. console.error(err)
  39. }
  40. }
  41. onAbort && onAbort(err)
  42. }
  43. if (
  44. // 如果是同一个路由就不跳转
  45. isSameRoute(route, current) &&
  46. // in the case the route map has been dynamically appended to
  47. route.matched.length === current.matched.length
  48. ) {
  49. // 调用子类的方法更新url
  50. this.ensureURL()
  51. return abort()
  52. }
  53. const {
  54. updated,
  55. deactivated,
  56. activated
  57. } = resolveQueue(this.current.matched, route.matched)
  58. const queue: Array<?NavigationGuard> = [].concat(
  59. // in-component leave guards
  60. extractLeaveGuards(deactivated),
  61. // global before hooks
  62. this.router.beforeHooks,
  63. // in-component update hooks
  64. extractUpdateHooks(updated),
  65. // in-config enter guards
  66. activated.map(m => m.beforeEnter),
  67. // async components
  68. resolveAsyncComponents(activated)
  69. )
  70. this.pending = route
  71. // 每一个队列执行的 iterator 函数
  72. const iterator = (hook: NavigationGuard, next) => {
  73. if (this.pending !== route) {
  74. return abort()
  75. }
  76. try {
  77. hook(route, current, (to: any) => {
  78. if (to === false || isError(to)) {
  79. // next(false) -> abort navigation, ensure current URL
  80. this.ensureURL(true)
  81. abort(to)
  82. } else if (
  83. typeof to === 'string' ||
  84. (typeof to === 'object' && (
  85. typeof to.path === 'string' ||
  86. typeof to.name === 'string'
  87. ))
  88. ) {
  89. // next('/') or next({ path: '/' }) -> redirect
  90. abort()
  91. if (typeof to === 'object' && to.replace) {
  92. this.replace(to)
  93. } else {
  94. this.push(to)
  95. }
  96. } else {
  97. // confirm transition and pass on the value
  98. next(to)
  99. }
  100. })
  101. } catch (e) {
  102. abort(e)
  103. }
  104. }
  105. // 执行各种钩子队列
  106. runQueue(queue, iterator, () => {
  107. const postEnterCbs = []
  108. const isValid = () => this.current === route
  109. // wait until async components are resolved before
  110. // extracting in-component enter guards
  111. const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
  112. const queue = enterGuards.concat(this.router.resolveHooks)
  113. runQueue(queue, iterator, () => {
  114. if (this.pending !== route) {
  115. return abort()
  116. }
  117. this.pending = null
  118. onComplete(route)
  119. if (this.router.app) {
  120. this.router.app.$nextTick(() => {
  121. postEnterCbs.forEach(cb => { cb() })
  122. })
  123. }
  124. })
  125. })
  126. }

其实这里说白了就是各种钩子函数来回秀操作,要注意的就是每个 router 对象都会有一个 matchd 属性,这个属性包含了一个路由记录。

在这里大多数博客都会说一下 src/index.js 的一个小尾巴,我们也不例外

  1. history.listen(route => {
  2. this.apps.forEach((app) => {
  3. app._route = route
  4. })
  5. })

这里更新的 _route 的值,这样就可以去通过 render 进行组件重新渲染。

Vue-router 的大致流程就是这些,还有一些 utils 和几种不同的 history 的具体实现还没有讲到,在这里就不一一详解了,还是推荐结合着源码去理解 Vue-router 真正在做什么,这样理解的也能更深入。