history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法
前一节我们分析了 matcher 的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事情,接下来我们来完整分析一下 transitionTo 的实现,它的定义在 src/history/base.js 中

  1. // transitionTo实际上就是在切换this.current
  2. transitionTo (
  3. location: RawLocation, // 目标
  4. onComplete?: Function, // 当前路径
  5. onAbort?: Function
  6. ) {
  7. let route
  8. // catch redirect option https://github.com/vuejs/vue-router/issues/3201
  9. try {
  10. // 匹配到目标的路径
  11. route = this.router.match(location, this.current)
  12. } catch (e) {
  13. this.errorCbs.forEach(cb => {
  14. cb(e)
  15. })
  16. // Exception should still be thrown
  17. throw e
  18. }
  19. // 拿到新的路径
  20. const prev = this.current
  21. // confirmTransition去做真正的切换
  22. this.confirmTransition(
  23. route,
  24. () => {
  25. this.updateRoute(route)
  26. onComplete && onComplete(route)
  27. this.ensureURL()
  28. this.router.afterHooks.forEach(hook => {
  29. hook && hook(route, prev)
  30. })
  31. // fire ready cbs once
  32. if (!this.ready) {
  33. this.ready = true
  34. this.readyCbs.forEach(cb => {
  35. cb(route)
  36. })
  37. }
  38. },
  39. err => {
  40. if (onAbort) {
  41. onAbort(err)
  42. }
  43. if (err && !this.ready) {
  44. // Initial redirection should not mark the history as ready yet
  45. // because it's triggered by the redirection instead
  46. // https://github.com/vuejs/vue-router/issues/3225
  47. // https://github.com/vuejs/vue-router/issues/3331
  48. if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
  49. this.ready = true
  50. this.readyErrorCbs.forEach(cb => {
  51. cb(err)
  52. })
  53. }
  54. }
  55. }
  56. )
  57. }

this.current是history维护的当前路径,它的初始值是在history的构造函数中初始化的

  1. this.current = START

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

  1. // the starting route that represents the initial state
  2. // 创建一个初始的Route
  3. export const START = createRoute(null, {
  4. path: '/'
  5. })

confirmTransition方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数,先来看一下它的定义

  1. confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  2. const current = this.current
  3. this.pending = route
  4. const abort = err => {
  5. // changed after adding errors with
  6. // https://github.com/vuejs/vue-router/pull/3047 before that change,
  7. // redirect and aborted navigation would produce an err == null
  8. if (!isNavigationFailure(err) && isError(err)) {
  9. if (this.errorCbs.length) {
  10. this.errorCbs.forEach(cb => {
  11. cb(err)
  12. })
  13. } else {
  14. if (process.env.NODE_ENV !== 'production') {
  15. warn(false, 'uncaught error during route navigation:')
  16. }
  17. console.error(err)
  18. }
  19. }
  20. onAbort && onAbort(err)
  21. }
  22. const lastRouteIndex = route.matched.length - 1
  23. const lastCurrentIndex = current.matched.length - 1
  24. if (
  25. isSameRoute(route, current) && // 判断计算后的route和current是相同路径
  26. // in the case the route map has been dynamically appended to
  27. lastRouteIndex === lastCurrentIndex &&
  28. route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
  29. ) {
  30. this.ensureURL()
  31. if (route.hash) {
  32. handleScroll(this.router, current, route, false)
  33. }
  34. return abort(createNavigationDuplicatedError(current, route))
  35. }
  36. // 解析出3个队列
  37. const { updated, deactivated, activated } = resolveQueue(
  38. this.current.matched,
  39. route.matched // 是一个RouteRecord的数组
  40. )
  41. // 1.导航守卫
  42. // 路径变换,执行一系列的钩子函数
  43. // 构造一个队列,实际上是个数组
  44. const queue: Array<?NavigationGuard> = [].concat(
  45. // in-component leave guards
  46. extractLeaveGuards(deactivated),
  47. // global before hooks
  48. this.router.beforeHooks,
  49. // in-component update hooks
  50. extractUpdateHooks(updated),
  51. // in-config enter guards
  52. activated.map(m => m.beforeEnter),
  53. // async components
  54. resolveAsyncComponents(activated)
  55. )
  56. // 定义一个迭代器函数
  57. const iterator = (hook: NavigationGuard, next) => {
  58. if (this.pending !== route) {
  59. return abort(createNavigationCancelledError(current, route))
  60. }
  61. try {
  62. // 去执行每一个导航守卫hook,传入route、current和匿名函数 对应 to from next
  63. hook(route, current, (to: any) => {
  64. // 根据条件执行abort或next,只有执行next时才会前进到下一个导航守卫钩子函数中
  65. // 这也就是为什么官方文档会说只有执行 next 方法来 resolve 这个钩子函数
  66. if (to === false) {
  67. // next(false) -> abort navigation, ensure current URL
  68. this.ensureURL(true)
  69. abort(createNavigationAbortedError(current, route))
  70. } else if (isError(to)) {
  71. this.ensureURL(true)
  72. abort(to)
  73. } else if (
  74. typeof to === 'string' ||
  75. (typeof to === 'object' &&
  76. (typeof to.path === 'string' || typeof to.name === 'string'))
  77. ) {
  78. // next('/') or next({ path: '/' }) -> redirect
  79. abort(createNavigationRedirectedError(current, route))
  80. if (typeof to === 'object' && to.replace) {
  81. this.replace(to)
  82. } else {
  83. this.push(to)
  84. }
  85. } else {
  86. // confirm transition and pass on the value
  87. next(to)
  88. }
  89. })
  90. } catch (e) {
  91. abort(e)
  92. }
  93. }
  94. // 执行这个队列
  95. runQueue(queue, iterator, () => {
  96. // wait until async components are resolved before
  97. // extracting in-component enter guards
  98. const enterGuards = extractEnterGuards(activated)
  99. const queue = enterGuards.concat(this.router.resolveHooks)
  100. runQueue(queue, iterator, () => {
  101. if (this.pending !== route) {
  102. return abort(createNavigationCancelledError(current, route))
  103. }
  104. this.pending = null
  105. onComplete(route)
  106. if (this.router.app) {
  107. this.router.app.$nextTick(() => {
  108. handleRouteEntered(route)
  109. })
  110. }
  111. })
  112. })
  113. }

resolveQueue

  1. function resolveQueue (
  2. current: Array<RouteRecord>,
  3. next: Array<RouteRecord>
  4. ): {
  5. updated: Array<RouteRecord>,
  6. activated: Array<RouteRecord>,
  7. deactivated: Array<RouteRecord>
  8. } {
  9. let i
  10. const max = Math.max(current.length, next.length)
  11. for (i = 0; i < max; i++) {
  12. if (current[i] !== next[i]) {
  13. break
  14. }
  15. }
  16. return {
  17. updated: next.slice(0, i),
  18. activated: next.slice(i),
  19. deactivated: current.slice(i)
  20. }
  21. }

由于路径是由 current 变向 route,那么就遍历对比 2 边的 RouteRecord,找到一个不一样的位置 i,那么 next 中从 0 到 i 的 RouteRecord 是两边都一样,则为 updated 的部分;从 i 到最后的 RouteRecord 是 next 独有的,为 activated 的部分;而 current 中从 i 到最后的 RouteRecord 则没有了,为 deactivated 的部分

导航守卫

官方的说法叫导航守卫,实际上就是发生在路由路径切换的时候,执行的一系列钩子函数
runQueue定义在 src/util/async.js 中

  1. // 非常经典的异步函数队列化执行的模式
  2. export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  3. // queue是一个NavigationGuard类型的数组
  4. // 每次根据index从queue中取一个guard,然后执行fn函数,并且把guard作为参数传入
  5. const step = index => {
  6. if (index >= queue.length) {
  7. cb()
  8. } else {
  9. if (queue[index]) {
  10. // 第二个参数是一个函数,当这个函数执行时再递归执行step函数,前进到下一个
  11. // fn就是iterator函数
  12. fn(queue[index], () => {
  13. step(index + 1)
  14. })
  15. } else {
  16. step(index + 1)
  17. }
  18. }
  19. }
  20. step(0)
  21. }

fn 就是刚才的 iterator 函数
queue 是怎么构造的

  1. const queue: Array<?NavigationGuard> = [].concat(
  2. // in-component leave guards
  3. extractLeaveGuards(deactivated),
  4. // global before hooks
  5. this.router.beforeHooks,
  6. // in-component update hooks
  7. extractUpdateHooks(updated),
  8. // in-config enter guards
  9. activated.map(m => m.beforeEnter),
  10. // async components
  11. resolveAsyncComponents(activated)
  12. )

按照顺序如下:

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

接下来分别介绍这 5 步的实现

第一步

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

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

它内部调用了 extractGuards 的通用方法,可以从 RouteRecord 数组中提取各个阶段的守卫

  1. function extractGuards (
  2. records: Array<RouteRecord>,
  3. name: string,
  4. bind: Function,
  5. reverse?: boolean
  6. ): Array<?Function> {
  7. const guards = flatMapComponents(records, (def, instance, match, key) => {
  8. // 获取组件中的对应name的导航守卫
  9. const guard = extractGuard(def, name)
  10. if (guard) {
  11. return Array.isArray(guard)
  12. ? guard.map(guard => bind(guard, instance, match, key)) // 调用bind方法把组件的实例instance作为函数执行的上下文绑定到guard上,bind对应的是bindGuard
  13. : bind(guard, instance, match, key)
  14. }
  15. })
  16. return flatten(reverse ? guards.reverse() : guards)
  17. }

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

  1. // flatMapComponents返回一个数组,数组的元素是从matched里获取到所有组件的key,然后返回fn函数执行的结果
  2. export function flatMapComponents (
  3. matched: Array<RouteRecord>,
  4. fn: Function
  5. ): Array<?Function> {
  6. // flatten把二维数组拍平成一维数组
  7. return flatten(matched.map(m => {
  8. return Object.keys(m.components).map(key => fn(
  9. m.components[key],
  10. m.instances[key],
  11. m, key
  12. ))
  13. }))
  14. }
  15. export function flatten (arr: Array<any>): Array<any> {
  16. return Array.prototype.concat.apply([], arr)
  17. }

extractGuard

  1. function extractGuard (
  2. def: Object | Function,
  3. key: string
  4. ): NavigationGuard | Array<NavigationGuard> {
  5. if (typeof def !== 'function') {
  6. // extend now so that global mixins are applied.
  7. def = _Vue.extend(def)
  8. }
  9. return def.options[key]
  10. }

bindGuard

  1. function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
  2. if (instance) {
  3. return function boundRouteGuard () {
  4. return guard.apply(instance, arguments)
  5. }
  6. }
  7. }

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

第二步

this.router.beforeHooks,在VueRouter 类中定义了 beforeEach 方法,在 src/index.js 中

  1. beforeEach (fn: Function): Function {
  2. return registerHook(this.beforeHooks, fn)
  3. }
  4. function registerHook (list: Array<any>, fn: Function): Function {
  5. list.push(fn)
  6. return () => {
  7. const i = list.indexOf(fn)
  8. if (i > -1) list.splice(i, 1)
  9. }
  10. }

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

第三步

执行了 extractUpdateHooks(updated),来看一下 extractUpdateHooks 的定义

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

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

第四步

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

第五步

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

  1. export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  2. // 返回的是一个导航守卫函数,有标准的to、from、next参数
  3. return (to, from, next) => {
  4. let hasAsync = false
  5. let pending = 0
  6. let error = null
  7. flatMapComponents(matched, (def, _, match, key) => {
  8. // if it's a function and doesn't have cid attached,
  9. // assume it's an async component resolve function.
  10. // we are not using Vue's default async resolving mechanism because
  11. // we want to halt the navigation until the incoming component has been
  12. // resolved.
  13. if (typeof def === 'function' && def.cid === undefined) {
  14. hasAsync = true
  15. pending++
  16. const resolve = once(resolvedDef => {
  17. if (isESModule(resolvedDef)) {
  18. resolvedDef = resolvedDef.default
  19. }
  20. // save resolved on async factory in case it's used elsewhere
  21. def.resolved = typeof resolvedDef === 'function'
  22. ? resolvedDef
  23. : _Vue.extend(resolvedDef)
  24. match.components[key] = resolvedDef
  25. pending--
  26. if (pending <= 0) {
  27. next()
  28. }
  29. })
  30. const reject = once(reason => {
  31. const msg = `Failed to resolve async component ${key}: ${reason}`
  32. process.env.NODE_ENV !== 'production' && warn(false, msg)
  33. if (!error) {
  34. error = isError(reason)
  35. ? reason
  36. : new Error(msg)
  37. next(error)
  38. }
  39. })
  40. let res
  41. try {
  42. res = def(resolve, reject)
  43. } catch (e) {
  44. reject(e)
  45. }
  46. if (res) {
  47. if (typeof res.then === 'function') {
  48. res.then(resolve, reject)
  49. } else {
  50. // new syntax in Vue 2.3
  51. const comp = res.component
  52. if (comp && typeof comp.then === 'function') {
  53. comp.then(resolve, reject)
  54. }
  55. }
  56. }
  57. }
  58. })
  59. if (!hasAsync) next()
  60. }
  61. }

使用 flatMapComponents 方法从 matched 中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析 Vue 加载异步组件很类似,加载成功后会执行 match.components[key] = resolvedDef 把解析好的异步组件放到对应的 components 上,并且执行 next 函数
这样在 resolveAsyncComponents(activated) 解析完所有激活的异步组件后,就可以拿到这一次所有激活的组件
这样在做完这 5 步后又做了一些事情

  1. runQueue(queue, iterator, () => {
  2. // wait until async components are resolved before
  3. // extracting in-component enter guards
  4. const enterGuards = extractEnterGuards(activated)
  5. const queue = enterGuards.concat(this.router.resolveHooks)
  6. runQueue(queue, iterator, () => {
  7. if (this.pending !== route) {
  8. return abort(createNavigationCancelledError(current, route))
  9. }
  10. this.pending = null
  11. onComplete(route)
  12. if (this.router.app) {
  13. this.router.app.$nextTick(() => {
  14. handleRouteEntered(route)
  15. })
  16. }
  17. })
  18. })

第六步

在被激活的组件里调用 beforeRouteEnter

  1. const enterGuards = extractEnterGuards(activated)
  2. function extractEnterGuards (
  3. activated: Array<RouteRecord>
  4. ): Array<?Function> {
  5. return extractGuards(
  6. activated,
  7. 'beforeRouteEnter',
  8. (guard, _, match, key) => {
  9. return bindEnterGuard(guard, match, key)
  10. }
  11. )
  12. }
  13. function bindEnterGuard (
  14. guard: NavigationGuard,
  15. match: RouteRecord,
  16. key: string
  17. ): NavigationGuard {
  18. return function routeEnterGuard (to, from, next) {
  19. return guard(to, from, cb => {
  20. if (typeof cb === 'function') {
  21. if (!match.enteredCbs[key]) {
  22. match.enteredCbs[key] = []
  23. }
  24. match.enteredCbs[key].push(cb)
  25. }
  26. next(cb)
  27. })
  28. }
  29. }

extractEnterGuards 函数的实现也是利用了 extractGuards 方法提取组件中的 beforeRouteEnter 导航钩子函数
beforeRouteEnter 钩子函数中是拿不到组件实例的,因为当守卫执行前,组件实例还没被创建,但是我们可以通过传一个回调给 next 来访问组件实例
在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数

  1. beforeRouteEnter (to, from, next) {
  2. next(vm => {
  3. // 通过 `vm` 访问组件实例
  4. })
  5. }

在 bindEnterGuard 函数中,返回的是 routeEnterGuard 函数,所以在执行 iterator 中的 hook 函数的时候,就相当于执行 routeEnterGuard 函数,那么就会执行我们定义的导航守卫 guard 函数,并且当这个回调函数执行的时候,首先执行 next 函数 rersolve 当前导航钩子,然后把回调函数的参数,它也是一个回调函数之后收集起来
然后在最后会执行

  1. if (this.router.app) {
  2. this.router.app.$nextTick(() => {
  3. handleRouteEntered(route)
  4. })
  5. }
  1. export function handleRouteEntered (route: Route) {
  2. for (let i = 0; i < route.matched.length; i++) {
  3. const record = route.matched[i]
  4. for (const name in record.instances) {
  5. const instance = record.instances[name]
  6. const cbs = record.enteredCbs[name]
  7. if (!instance || !cbs) continue
  8. delete record.enteredCbs[name]
  9. for (let i = 0; i < cbs.length; i++) {
  10. if (!instance._isBeingDestroyed) cbs[i](instance)
  11. }
  12. }
  13. }
  14. }

在根路由组件重新渲染后,执行handleRouteEntered
因为考虑到一些了路由组件被套 transition 組件在一些缓动模式下不一定能拿到实例,所以用一个轮询方法不断去判断,直到能获取到组件实例,再去调用 cb,并把组件实例作为参数传入,这就是在回调函数中能拿到组件实例的原因

第七步

调用全局的 beforeResolve 守卫
获取 this.router.resolveHooks,这个和 this.router.beforeHooks 的获取类似,在 VueRouter 类中定义了 beforeResolve 方法

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

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

第八步

调用全局的 afterEach 钩子
在最后执行了 onComplete(route) 后,会执行 this.updateRoute(route) 方法

  1. updateRoute (route: Route) {
  2. this.current = route
  3. this.cb && this.cb(route)
  4. }

同样在 VueRouter 类中定义了 afterEach 方法

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

当用户使用 router.afterEach 注册了一个全局守卫,就会往 router.afterHooks 添加一个钩子函数,这样 this.router.afterHooks 获取的就是用户注册的全局 afterHooks 守卫
那么至此把所有导航守卫的执行分析完毕了,知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是 url 发生变化,一个是组件发生变化

url

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

  1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. // $flow-disable-line
  3. if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
  4. return new Promise((resolve, reject) => {
  5. this.history.push(location, resolve, reject)
  6. })
  7. } else {
  8. this.history.push(location, onComplete, onAbort)
  9. }
  10. }

this.history.push 函数,这个函数是子类实现的,不同模式下该函数的实现略有不同
平时使用比较多的 hash 模式该函数的实现,在 src/history/hash.js 中

  1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. const { current: fromRoute } = this
  3. // 做路径切换
  4. this.transitionTo(
  5. location,
  6. route => {
  7. pushHash(route.fullPath)
  8. handleScroll(this.router, route, fromRoute, false)
  9. onComplete && onComplete(route)
  10. },
  11. onAbort
  12. )
  13. }

在切换完成的回调函数中,执行 pushHash 函数

  1. function pushHash (path) {
  2. // 如果支持则获取当前完整的url,执行pushState
  3. if (supportsPushState) {
  4. pushState(getUrl(path))
  5. } else {
  6. window.location.hash = path
  7. }
  8. }

supportsPushState定义在 src/util/push-state.js 中

  1. export const supportsPushState =
  2. inBrowser &&
  3. (function () {
  4. const ua = window.navigator.userAgent
  5. if (
  6. (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
  7. ua.indexOf('Mobile Safari') !== -1 &&
  8. ua.indexOf('Chrome') === -1 &&
  9. ua.indexOf('Windows Phone') === -1
  10. ) {
  11. return false
  12. }
  13. return window.history && typeof window.history.pushState === 'function'
  14. })()

pushState

  1. export function pushState (url?: string, replace?: boolean) {
  2. saveScrollPosition()
  3. // try...catch the pushState call to get around Safari
  4. // DOM Exception 18 where it limits to 100 pushState calls
  5. const history = window.history
  6. try {
  7. // 调用浏览器原生的history的replaceState或者pushState接口,更新浏览器的url地址,并把当前url压入历史栈中
  8. if (replace) {
  9. // preserve existing history state as it could be overriden by the user
  10. const stateCopy = extend({}, history.state)
  11. stateCopy.key = getStateKey()
  12. history.replaceState(stateCopy, '', url)
  13. } else {
  14. history.pushState({ key: setStateKey(genStateKey()) }, '', url)
  15. }
  16. } catch (e) {
  17. window.location[replace ? 'replace' : 'assign'](url)
  18. }
  19. }

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

  1. // this is delayed until the app mounts
  2. // to avoid the hashchange listener being fired too early
  3. setupListeners () {
  4. if (this.listeners.length > 0) {
  5. return
  6. }
  7. const router = this.router
  8. const expectScroll = router.options.scrollBehavior
  9. const supportsScroll = supportsPushState && expectScroll
  10. if (supportsScroll) {
  11. this.listeners.push(setupScroll())
  12. }
  13. const handleRoutingEvent = () => {
  14. const current = this.current
  15. if (!ensureSlash()) {
  16. return
  17. }
  18. this.transitionTo(getHash(), route => {
  19. if (supportsScroll) {
  20. handleScroll(this.router, route, current, true)
  21. }
  22. if (!supportsPushState) {
  23. replaceHash(route.fullPath)
  24. }
  25. })
  26. }
  27. const eventType = supportsPushState ? 'popstate' : 'hashchange'
  28. window.addEventListener(
  29. eventType,
  30. handleRoutingEvent
  31. )
  32. this.listeners.push(() => {
  33. window.removeEventListener(eventType, handleRoutingEvent)
  34. })
  35. }

当点击浏览器返回按钮的时候,如果已经有 url 被压入历史栈,则会触发 popstate 事件,然后拿到当前要跳转的 hash,执行 transtionTo 方法做一次路径转换
在使用 Vue-Router 开发项目的时候,打开调试页面 http://localhost:8080 后会自动把 url 修改为 http://localhost:8080/#/,这是怎么做到呢
原来在实例化 HashHistory 的时候,构造函数会执行 ensureSlash() 方法

  1. function ensureSlash () {
  2. var path = getHash();
  3. if (path.charAt(0) === '/') {
  4. return true
  5. }
  6. // path为空,执行replaceHash
  7. replaceHash('/' + path);
  8. return false
  9. }
  10. function getHash () {
  11. // We can't use window.location.hash here because it's not
  12. // consistent across browsers - Firefox will pre-decode it!
  13. var href = window.location.href;
  14. var index = href.indexOf('#');
  15. // empty path
  16. if (index < 0) { return '' }
  17. href = href.slice(index + 1);
  18. return href
  19. }
  20. function getUrl (path) {
  21. var href = window.location.href;
  22. var i = href.indexOf('#');
  23. var base = i >= 0 ? href.slice(0, i) : href;
  24. return (base + "#" + path)
  25. }
  26. function replaceHash (path) {
  27. if (supportsPushState) {
  28. // 内部会执行一次getUrl,计算出新的url为http://localhost:8080/#/
  29. replaceState(getUrl(path));
  30. } else {
  31. window.location.replace(getUrl(path));
  32. }
  33. }
  34. function replaceState (url) {
  35. // 最终会执行pushState,这就是url会改变的原因
  36. pushState(url, true);
  37. }

组件

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

  1. // <router-view>是一个functional组件,它的渲染也是依赖render函数
  2. export default {
  3. name: 'RouterView',
  4. functional: true,
  5. props: {
  6. name: {
  7. type: String,
  8. default: 'default'
  9. }
  10. },
  11. render (_, { props, children, parent, data }) {
  12. // used by devtools to display a router-view badge
  13. data.routerView = true
  14. // directly use parent context's createElement() function
  15. // so that components rendered by router-view can resolve named slots
  16. const h = parent.$createElement
  17. const name = props.name
  18. // 1.获取当前的路径
  19. const route = parent.$route
  20. const cache = parent._routerViewCache || (parent._routerViewCache = {})
  21. // determine current view depth, also check to see if the tree
  22. // has been toggled inactive but kept-alive.
  23. let depth = 0
  24. let inactive = false
  25. while (parent && parent._routerRoot !== parent) {
  26. const vnodeData = parent.$vnode ? parent.$vnode.data : {}
  27. if (vnodeData.routerView) {
  28. depth++
  29. }
  30. if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
  31. inactive = true
  32. }
  33. parent = parent.$parent
  34. }
  35. data.routerViewDepth = depth
  36. // render previous view if the tree is inactive and kept-alive
  37. if (inactive) {
  38. const cachedData = cache[name]
  39. const cachedComponent = cachedData && cachedData.component
  40. if (cachedComponent) {
  41. // #2301
  42. // pass props
  43. if (cachedData.configProps) {
  44. fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
  45. }
  46. return h(cachedComponent, data, children)
  47. } else {
  48. // render previous empty view
  49. return h()
  50. }
  51. }
  52. const matched = route.matched[depth]
  53. const component = matched && matched.components[name]
  54. // render empty node if no matched route or no config component
  55. if (!matched || !component) {
  56. cache[name] = null
  57. return h()
  58. }
  59. // cache component
  60. cache[name] = { component }
  61. // attach instance registration hook
  62. // this will be called in the instance's injected lifecycle hooks
  63. data.registerRouteInstance = (vm, val) => {
  64. // val could be undefined for unregistration
  65. const current = matched.instances[name]
  66. if (
  67. (val && current !== vm) ||
  68. (!val && current === vm)
  69. ) {
  70. matched.instances[name] = val
  71. }
  72. }
  73. // also register instance in prepatch hook
  74. // in case the same component instance is reused across different routes
  75. ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
  76. matched.instances[name] = vnode.componentInstance
  77. }
  78. // register instance in init hook
  79. // in case kept-alive component be actived when routes changed
  80. data.hook.init = (vnode) => {
  81. if (vnode.data.keepAlive &&
  82. vnode.componentInstance &&
  83. vnode.componentInstance !== matched.instances[name]
  84. ) {
  85. matched.instances[name] = vnode.componentInstance
  86. }
  87. // if the route transition has already been confirmed then we weren't
  88. // able to call the cbs during confirmation as the component was not
  89. // registered yet, so we call it here.
  90. handleRouteEntered(route)
  91. }
  92. const configProps = matched.props && matched.props[name]
  93. // save route and configProps in cache
  94. if (configProps) {
  95. extend(cache[name], {
  96. route,
  97. configProps
  98. })
  99. fillPropsinData(component, data, route, configProps)
  100. }
  101. return h(component, data, children)
  102. }
  103. }

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

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

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

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

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

  1. listen (cb: Function) {
  2. this.cb = cb
  3. }

然后在 updateRoute 的时候执行 this.cb

  1. updateRoute (route: Route) {
  2. this.current = route
  3. this.cb && this.cb(route)
  4. }

也就是执行 transitionTo 方法最后执行 updateRoute 的时候会执行回调,然后会更新 this.apps 保存的组件实例的 _route 值,this.apps 数组保存的实例的特点都是在初始化的时候传入了 router 配置项,一般的场景数组只会保存根 Vue 实例,因为是在 new Vue 传入了 router 实例
$route 是定义在 Vue.prototype 上
每个组件实例访问 $route 属性,就是访问根实例的 _route,也就是当前的路由线路

是支持嵌套的,回到 render 函数,其中定义了 depth 的概念,它表示 嵌套的深度。每个 在渲染的时候,执行如下逻辑

  1. // used by devtools to display a router-view badge
  2. data.routerView = true
  3. // ...
  4. // parent._routerRoot表示是根Vue实例 循环是从当前得<router-view>的父节点向上找,一直找到根Vue实例
  5. while (parent && parent._routerRoot !== parent) {
  6. const vnodeData = parent.$vnode ? parent.$vnode.data : {}
  7. // 碰到父节点也是<router-view>时说明<router-view>有嵌套 depth++
  8. if (vnodeData.routerView) {
  9. depth++
  10. }
  11. if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
  12. inactive = true
  13. }
  14. parent = parent.$parent
  15. }
  16. // ...
  17. // 遍历完成后根据当前线路匹配的路径和depth找到相应的RouteRecord,进而找到该渲染的组件
  18. const matched = route.matched[depth]
  19. const component = matched && matched.components[name]
  20. // ...
  21. // attach instance registration hook
  22. // this will be called in the instance's injected lifecycle hooks
  23. // 定义一个注册路由实例的方法
  24. data.registerRouteInstance = (vm, val) => {
  25. // val could be undefined for unregistration
  26. const current = matched.instances[name]
  27. if (
  28. (val && current !== vm) ||
  29. (!val && current === vm)
  30. ) {
  31. matched.instances[name] = val
  32. }
  33. }
  34. // ...
  35. // 根据component渲染出对应的组件vnode
  36. return h(component, data, children)

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

  1. const registerInstance = (vm, callVal) => {
  2. let i = vm.$options._parentVnode
  3. if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
  4. i(vm, callVal)
  5. }
  6. }
  7. Vue.mixin({
  8. beforeCreate () {
  9. if (isDef(this.$options.router)) {
  10. this._routerRoot = this
  11. this._router = this.$options.router
  12. this._router.init(this)
  13. Vue.util.defineReactive(this, '_route', this._router.history.current)
  14. } else {
  15. this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
  16. }
  17. registerInstance(this, this)
  18. },
  19. destroyed () {
  20. registerInstance(this)
  21. }
  22. })

在混入的 beforeCreate 钩子函数中,会执行 registerInstance 方法,进而执行 render 函数中定义的 registerRouteInstance 方法,从而给 matched.instances[name] 赋值当前组件的 vm 实例
那么当执行 transitionTo 来更改路由线路后,组件是如何重新渲染的呢?在混入的 beforeCreate 钩子函数中有这么一段逻辑

  1. Vue.mixin({
  2. beforeCreate () {
  3. if (isDef(this.$options.router)) {
  4. this._routerRoot = this
  5. this._router = this.$options.router
  6. this._router.init(this)
  7. Vue.util.defineReactive(this, '_route', this._router.history.current)
  8. } else {
  9. this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
  10. }
  11. registerInstance(this, this)
  12. },
  13. })

由于我们把根 Vue 实例的 _route 属性定义成响应式的,在每个 执行 render 函数的时候,都会访问 parent.$route,如之前分析会访问 this._routerRoot._route,触发了它的 getter,相当于 对它有依赖,然后再执行完 transitionTo 后,修改 app._route 的时候,又触发了setter,因此会通知 的渲染 watcher 更新,重新渲染组件

Vue-Router 还内置了另一个组件 , 它支持用户在具有路由功能的应用中(点击)导航。 通过 to 属性指定目标地址,默认渲染成带有正确链接的 标签,可以通过配置 tag 属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名
比起写死的
会好一些,理由如下:

  1. 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动
  2. 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面
  3. 当在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写(基路径)了

分析它的实现,它的定义在 src/components/link.js 中

  1. export default {
  2. name: 'RouterLink',
  3. props: {
  4. to: {
  5. type: toTypes,
  6. required: true
  7. },
  8. tag: {
  9. type: String,
  10. default: 'a'
  11. },
  12. custom: Boolean,
  13. exact: Boolean,
  14. exactPath: Boolean,
  15. append: Boolean,
  16. replace: Boolean,
  17. activeClass: String,
  18. exactActiveClass: String,
  19. ariaCurrentValue: {
  20. type: String,
  21. default: 'page'
  22. },
  23. event: {
  24. type: eventTypes,
  25. default: 'click'
  26. }
  27. },
  28. // 渲染
  29. render (h: Function) {
  30. // 路由解析
  31. const router = this.$router
  32. const current = this.$route
  33. const { location, route, href } = router.resolve(
  34. this.to,
  35. current,
  36. this.append
  37. )
  38. // 对exactActiveClass和activeClass做处理
  39. const classes = {}
  40. const globalActiveClass = router.options.linkActiveClass
  41. const globalExactActiveClass = router.options.linkExactActiveClass
  42. // Support global empty active class
  43. const activeClassFallback =
  44. globalActiveClass == null ? 'router-link-active' : globalActiveClass
  45. const exactActiveClassFallback =
  46. globalExactActiveClass == null
  47. ? 'router-link-exact-active'
  48. : globalExactActiveClass
  49. const activeClass =
  50. this.activeClass == null ? activeClassFallback : this.activeClass
  51. const exactActiveClass =
  52. this.exactActiveClass == null
  53. ? exactActiveClassFallback
  54. : this.exactActiveClass
  55. const compareTarget = route.redirectedFrom
  56. ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
  57. : route
  58. // 当配置 exact 为 true 的时候,只有当目标路径和当前路径完全匹配的时候,会添加 exactActiveClass;而当目标路径包含当前路径的时候,会添加 activeClass
  59. classes[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath)
  60. classes[activeClass] = this.exact || this.exactPath
  61. ? classes[exactActiveClass]
  62. : isIncludedRoute(current, compareTarget)
  63. const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
  64. // 创建一个守卫函数
  65. const handler = e => {
  66. if (guardEvent(e)) {
  67. if (this.replace) {
  68. router.replace(location, noop)
  69. } else {
  70. router.push(location, noop)
  71. }
  72. }
  73. }
  74. const on = { click: guardEvent }
  75. if (Array.isArray(this.event)) {
  76. this.event.forEach(e => {
  77. on[e] = handler
  78. })
  79. } else {
  80. on[this.event] = handler
  81. }
  82. const data: any = { class: classes }
  83. const scopedSlot =
  84. !this.$scopedSlots.$hasNormal &&
  85. this.$scopedSlots.default &&
  86. this.$scopedSlots.default({
  87. href,
  88. route,
  89. navigate: handler,
  90. isActive: classes[activeClass],
  91. isExactActive: classes[exactActiveClass]
  92. })
  93. if (scopedSlot) {
  94. if (process.env.NODE_ENV !== 'production' && !this.custom) {
  95. !warnedCustomSlot && warn(false, 'In Vue Router 4, the v-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link v-slot="{ navigate, href }" custom></router-link>\n')
  96. warnedCustomSlot = true
  97. }
  98. if (scopedSlot.length === 1) {
  99. return scopedSlot[0]
  100. } else if (scopedSlot.length > 1 || !scopedSlot.length) {
  101. if (process.env.NODE_ENV !== 'production') {
  102. warn(
  103. false,
  104. `<router-link> with to="${
  105. this.to
  106. }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
  107. )
  108. }
  109. return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
  110. }
  111. }
  112. if (process.env.NODE_ENV !== 'production') {
  113. if ('tag' in this.$options.propsData && !warnedTagProp) {
  114. warn(
  115. false,
  116. `<router-link>'s tag prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
  117. )
  118. warnedTagProp = true
  119. }
  120. if ('event' in this.$options.propsData && !warnedEventProp) {
  121. warn(
  122. false,
  123. `<router-link>'s event prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
  124. )
  125. warnedEventProp = true
  126. }
  127. }
  128. if (this.tag === 'a') {
  129. data.on = on
  130. data.attrs = { href, 'aria-current': ariaCurrentValue }
  131. } else {
  132. // find the first <a> child and apply listener and href
  133. const a = findAnchor(this.$slots.default)
  134. if (a) {
  135. // in case the <a> is a static node
  136. a.isStatic = false
  137. const aData = (a.data = extend({}, a.data))
  138. aData.on = aData.on || {}
  139. // transform existing events in both objects into arrays so we can push later
  140. for (const event in aData.on) {
  141. const handler = aData.on[event]
  142. if (event in on) {
  143. aData.on[event] = Array.isArray(handler) ? handler : [handler]
  144. }
  145. }
  146. // append new listeners for router-link
  147. for (const event in on) {
  148. if (event in aData.on) {
  149. // on[event] is always a function
  150. aData.on[event].push(on[event])
  151. } else {
  152. aData.on[event] = handler
  153. }
  154. }
  155. const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
  156. aAttrs.href = href
  157. aAttrs['aria-current'] = ariaCurrentValue
  158. } else {
  159. // doesn't have <a> child, apply listener to self
  160. data.on = on
  161. }
  162. }
  163. return h(this.tag, data, this.$slots.default)
  164. }
  165. }

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

  1. resolve (
  2. to: RawLocation,
  3. current?: Route,
  4. append?: boolean
  5. ): {
  6. location: Location,
  7. route: Route,
  8. href: string,
  9. // for backwards compat
  10. normalizedTo: Location,
  11. resolved: Route
  12. } {
  13. current = current || this.history.current
  14. // 规范化生成目标location
  15. const location = normalizeLocation(to, current, append, this)
  16. // 根据location和match通过this.match方法计算生成目标路径route
  17. const route = this.match(location, current)
  18. // 根据base、fullPath和this.mode通过createHref方法计算出最终跳转的href
  19. const fullPath = route.redirectedFrom || route.fullPath
  20. const base = this.history.base
  21. const href = createHref(base, fullPath, this.mode)
  22. return {
  23. location,
  24. route,
  25. href,
  26. // for backwards compat
  27. normalizedTo: location,
  28. resolved: route
  29. }
  30. }
  31. function createHref (base: string, fullPath: string, mode) {
  32. var path = mode === 'hash' ? '#' + fullPath : fullPath
  33. return base ? cleanPath(base + '/' + path) : path
  34. }

接着创建了一个守卫函数

  1. const handler = e => {
  2. if (guardEvent(e)) {
  3. if (this.replace) {
  4. router.replace(location, noop)
  5. } else {
  6. router.push(location, noop)
  7. }
  8. }
  9. }
  10. function guardEvent (e) {
  11. // don't redirect with control keys
  12. if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  13. // don't redirect when preventDefault called
  14. if (e.defaultPrevented) return
  15. // don't redirect on right click
  16. if (e.button !== undefined && e.button !== 0) return
  17. // don't redirect if `target="_blank"`
  18. if (e.currentTarget && e.currentTarget.getAttribute) {
  19. const target = e.currentTarget.getAttribute('target')
  20. if (/\b_blank\b/i.test(target)) return
  21. }
  22. // this may be a Weex event which doesn't have this method
  23. if (e.preventDefault) {
  24. e.preventDefault()
  25. }
  26. return true
  27. }
  28. const on = { click: guardEvent }
  29. if (Array.isArray(this.event)) {
  30. this.event.forEach(e => {
  31. on[e] = handler
  32. })
  33. } else {
  34. on[this.event] = handler
  35. }

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

  1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  2. // $flow-disable-line
  3. if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
  4. return new Promise((resolve, reject) => {
  5. this.history.push(location, resolve, reject)
  6. })
  7. } else {
  8. this.history.push(location, onComplete, onAbort)
  9. }
  10. }
  11. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  12. // $flow-disable-line
  13. if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
  14. return new Promise((resolve, reject) => {
  15. this.history.replace(location, resolve, reject)
  16. })
  17. } else {
  18. this.history.replace(location, onComplete, onAbort)
  19. }
  20. }

实际上就是执行了 history 的 push 和 replace 方法做路由跳转
最后判断当前 tag 是否是
标签, 默认会渲染成 标签,当然也可以修改 tag 的 prop 渲染成其他节点,这种情况下会尝试找它子元素的 标签,如果有则把事件绑定到 标签上并添加 href 属性,否则绑定到外层元素本身

路径变化是路由中最重要的功能,记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据