• 路径切换
    • 导航守卫
    • url
    • 组件
    • 总结

    路径切换

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

    1. transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    2. const route = this.router.match(location, this.current)
    3. this.confirmTransition(route, () => {
    4. this.updateRoute(route)
    5. onComplete && onComplete(route)
    6. this.ensureURL()
    7. if (!this.ready) {
    8. this.ready = true
    9. this.readyCbs.forEach(cb => { cb(route) })
    10. }
    11. }, err => {
    12. if (onAbort) {
    13. onAbort(err)
    14. }
    15. if (err && !this.ready) {
    16. this.ready = true
    17. this.readyErrorCbs.forEach(cb => { cb(err) })
    18. }
    19. })
    20. }

    transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径。这里 this.currenthistory 维护的当前路径,它的初始值是在 history 的构造函数中初始化的:

    1. this.current = START

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

    1. export const START = createRoute(null, {
    2. path: '/'
    3. })

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

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

    1. confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    2. const current = this.current
    3. const abort = err => {
    4. if (isError(err)) {
    5. if (this.errorCbs.length) {
    6. this.errorCbs.forEach(cb => { cb(err) })
    7. } else {
    8. warn(false, 'uncaught error during route navigation:')
    9. console.error(err)
    10. }
    11. }
    12. onAbort && onAbort(err)
    13. }
    14. if (
    15. isSameRoute(route, current) &&
    16. route.matched.length === current.matched.length
    17. ) {
    18. this.ensureURL()
    19. return abort()
    20. }
    21. const {
    22. updated,
    23. deactivated,
    24. activated
    25. } = resolveQueue(this.current.matched, route.matched)
    26. const queue: Array<?NavigationGuard> = [].concat(
    27. extractLeaveGuards(deactivated),
    28. this.router.beforeHooks,
    29. extractUpdateHooks(updated),
    30. activated.map(m => m.beforeEnter),
    31. resolveAsyncComponents(activated)
    32. )
    33. this.pending = route
    34. const iterator = (hook: NavigationGuard, next) => {
    35. if (this.pending !== route) {
    36. return abort()
    37. }
    38. try {
    39. hook(route, current, (to: any) => {
    40. if (to === false || isError(to)) {
    41. this.ensureURL(true)
    42. abort(to)
    43. } else if (
    44. typeof to === 'string' ||
    45. (typeof to === 'object' && (
    46. typeof to.path === 'string' ||
    47. typeof to.name === 'string'
    48. ))
    49. ) {
    50. abort()
    51. if (typeof to === 'object' && to.replace) {
    52. this.replace(to)
    53. } else {
    54. this.push(to)
    55. }
    56. } else {
    57. next(to)
    58. }
    59. })
    60. } catch (e) {
    61. abort(e)
    62. }
    63. }
    64. runQueue(queue, iterator, () => {
    65. const postEnterCbs = []
    66. const isValid = () => this.current === route
    67. const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    68. const queue = enterGuards.concat(this.router.resolveHooks)
    69. runQueue(queue, iterator, () => {
    70. if (this.pending !== route) {
    71. return abort()
    72. }
    73. this.pending = null
    74. onComplete(route)
    75. if (this.router.app) {
    76. this.router.app.$nextTick(() => {
    77. postEnterCbs.forEach(cb => { cb() })
    78. })
    79. }
    80. })
    81. })
    82. }

    首先定义了 abort 函数,然后判断如果满足计算后的 routecurrent 是相同路径的话,则直接调用 this.ensureUrlabortensureUrl 这个函数我们之后会介绍。

    接着又根据 current.matchedroute.matched 执行了 resolveQueue 方法解析出 3 个队列:

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

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

    拿到 updatedactivateddeactivated 3 个 ReouteRecord 数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数。

    导航守卫

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

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

    1. export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
    2. const step = index => {
    3. if (index >= queue.length) {
    4. cb()
    5. } else {
    6. if (queue[index]) {
    7. fn(queue[index], () => {
    8. step(index + 1)
    9. })
    10. } else {
    11. step(index + 1)
    12. }
    13. }
    14. }
    15. step(0)
    16. }

    这是一个非常经典的异步函数队列化执行的模式, queue 是一个 NavigationGuard 类型的数组,我们定义了 step 函数,每次根据 indexqueue 中取一个 guard,然后执行 fn 函数,并且把 guard 作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行 step 函数,前进到下一个,注意这里的 fn 就是我们刚才的 iterator 函数,那么我们再回到 iterator 函数的定义:

    1. const iterator = (hook: NavigationGuard, next) => {
    2. if (this.pending !== route) {
    3. return abort()
    4. }
    5. try {
    6. hook(route, current, (to: any) => {
    7. if (to === false || isError(to)) {
    8. this.ensureURL(true)
    9. abort(to)
    10. } else if (
    11. typeof to === 'string' ||
    12. (typeof to === 'object' && (
    13. typeof to.path === 'string' ||
    14. typeof to.name === 'string'
    15. ))
    16. ) {
    17. abort()
    18. if (typeof to === 'object' && to.replace) {
    19. this.replace(to)
    20. } else {
    21. this.push(to)
    22. }
    23. } else {
    24. next(to)
    25. }
    26. })
    27. } catch (e) {
    28. abort(e)
    29. }
    30. }

    iterator 函数逻辑很简单,它就是去执行每一个 导航守卫 hook,并传入 routecurrent 和匿名函数,这些参数对应文档中的 tofromnext,当执行了匿名函数,会根据一些条件执行 abortnext,只有执行 next 的时候,才会前进到下一个导航守卫钩子函数中,这也就是为什么官方文档会说只有执行 next 方法来 resolve 这个钩子函数。

    那么最后我们来看 queue 是怎么构造的:

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

    按照顺序如下:

    • 在失活的组件里调用离开守卫。

    • 调用全局的 beforeEach 守卫。

    • 在重用的组件里调用 beforeRouteUpdate 守卫

    • 在激活的路由配置里调用 beforeEnter

    • 解析异步路由组件。

    接下来我们来分别介绍这 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. const guard = extractGuard(def, name)
    9. if (guard) {
    10. return Array.isArray(guard)
    11. ? guard.map(guard => bind(guard, instance, match, key))
    12. : bind(guard, instance, match, key)
    13. }
    14. })
    15. return flatten(reverse ? guards.reverse() : guards)
    16. }

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

    1. export function flatMapComponents (
    2. matched: Array<RouteRecord>,
    3. fn: Function
    4. ): Array<?Function> {
    5. return flatten(matched.map(m => {
    6. return Object.keys(m.components).map(key => fn(
    7. m.components[key],
    8. m.instances[key],
    9. m, key
    10. ))
    11. }))
    12. }
    13. export function flatten (arr: Array<any>): Array<any> {
    14. return Array.prototype.concat.apply([], arr)
    15. }

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

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

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

    获取到 guard 后,还会调用 bind 方法把组件的实例 instance 作为函数执行的上下文绑定到 guard 上,bind 方法的对应的是 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. return (to, from, next) => {
    3. let hasAsync = false
    4. let pending = 0
    5. let error = null
    6. flatMapComponents(matched, (def, _, match, key) => {
    7. if (typeof def === 'function' && def.cid === undefined) {
    8. hasAsync = true
    9. pending++
    10. const resolve = once(resolvedDef => {
    11. if (isESModule(resolvedDef)) {
    12. resolvedDef = resolvedDef.default
    13. }
    14. def.resolved = typeof resolvedDef === 'function'
    15. ? resolvedDef
    16. : _Vue.extend(resolvedDef)
    17. match.components[key] = resolvedDef
    18. pending--
    19. if (pending <= 0) {
    20. next()
    21. }
    22. })
    23. const reject = once(reason => {
    24. const msg = `Failed to resolve async component ${key}: ${reason}`
    25. process.env.NODE_ENV !== 'production' && warn(false, msg)
    26. if (!error) {
    27. error = isError(reason)
    28. ? reason
    29. : new Error(msg)
    30. next(error)
    31. }
    32. })
    33. let res
    34. try {
    35. res = def(resolve, reject)
    36. } catch (e) {
    37. reject(e)
    38. }
    39. if (res) {
    40. if (typeof res.then === 'function') {
    41. res.then(resolve, reject)
    42. } else {
    43. const comp = res.component
    44. if (comp && typeof comp.then === 'function') {
    45. comp.then(resolve, reject)
    46. }
    47. }
    48. }
    49. }
    50. })
    51. if (!hasAsync) next()
    52. }
    53. }

    resolveAsyncComponents 返回的是一个导航守卫函数,有标准的 tofromnext 参数。它的内部实现很简单,利用了 flatMapComponents 方法从 matched 中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析 Vue 加载异步组件很类似,加载成功后会执行 match.components[key] = resolvedDef 把解析好的异步组件放到对应的 components 上,并且执行 next 函数。

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

    1. runQueue(queue, iterator, () => {
    2. const postEnterCbs = []
    3. const isValid = () => this.current === route
    4. const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    5. const queue = enterGuards.concat(this.router.resolveHooks)
    6. runQueue(queue, iterator, () => {
    7. if (this.pending !== route) {
    8. return abort()
    9. }
    10. this.pending = null
    11. onComplete(route)
    12. if (this.router.app) {
    13. this.router.app.$nextTick(() => {
    14. postEnterCbs.forEach(cb => { cb() })
    15. })
    16. }
    17. })
    18. })
    • 在被激活的组件里调用 beforeRouteEnter

    • 调用全局的 beforeResolve 守卫。

    • 调用全局的 afterEach 钩子。

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

    1. const postEnterCbs = []
    2. const isValid = () => this.current === route
    3. const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    4. function extractEnterGuards (
    5. activated: Array<RouteRecord>,
    6. cbs: Array<Function>,
    7. isValid: () => boolean
    8. ): Array<?Function> {
    9. return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
    10. return bindEnterGuard(guard, match, key, cbs, isValid)
    11. })
    12. }
    13. function bindEnterGuard (
    14. guard: NavigationGuard,
    15. match: RouteRecord,
    16. key: string,
    17. cbs: Array<Function>,
    18. isValid: () => boolean
    19. ): NavigationGuard {
    20. return function routeEnterGuard (to, from, next) {
    21. return guard(to, from, cb => {
    22. next(cb)
    23. if (typeof cb === 'function') {
    24. cbs.push(() => {
    25. poll(cb, match.instances, key, isValid)
    26. })
    27. }
    28. })
    29. }
    30. }
    31. function poll (
    32. cb: any,
    33. instances: Object,
    34. key: string,
    35. isValid: () => boolean
    36. ) {
    37. if (instances[key]) {
    38. cb(instances[key])
    39. } else if (isValid()) {
    40. setTimeout(() => {
    41. poll(cb, instances, key, isValid)
    42. }, 16)
    43. }
    44. }

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

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

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

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

    1. if (this.router.app) {
    2. this.router.app.$nextTick(() => {
    3. postEnterCbs.forEach(cb => { cb() })
    4. })
    5. }

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

    第七步是获取 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 守卫。

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

    1. updateRoute (route: Route) {
    2. const prev = this.current
    3. this.current = route
    4. this.cb && this.cb(route)
    5. this.router.afterHooks.forEach(hook => {
    6. hook && hook(route, prev)
    7. })
    8. }

    同样在我们的 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. this.history.push(location, onComplete, onAbort)
    3. }

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

    1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    2. const { current: fromRoute } = this
    3. this.transitionTo(location, route => {
    4. pushHash(route.fullPath)
    5. handleScroll(this.router, route, fromRoute, false)
    6. onComplete && onComplete(route)
    7. }, onAbort)
    8. }

    push 函数会先执行 this.transitionTo 做路径切换,在切换完成的回调函数中,执行 pushHash 函数:

    1. function pushHash (path) {
    2. if (supportsPushState) {
    3. pushState(getUrl(path))
    4. } else {
    5. window.location.hash = path
    6. }
    7. }

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

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

    如果支持的话,则获取当前完整的 url,执行 pushState 方法:

    1. export function pushState (url?: string, replace?: boolean) {
    2. saveScrollPosition()
    3. const history = window.history
    4. try {
    5. if (replace) {
    6. history.replaceState({ key: _key }, '', url)
    7. } else {
    8. _key = genKey()
    9. history.pushState({ key: _key }, '', url)
    10. }
    11. } catch (e) {
    12. window.location[replace ? 'replace' : 'assign'](url)
    13. }
    14. }

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

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

    1. setupListeners () {
    2. const router = this.router
    3. const expectScroll = router.options.scrollBehavior
    4. const supportsScroll = supportsPushState && expectScroll
    5. if (supportsScroll) {
    6. setupScroll()
    7. }
    8. window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
    9. const current = this.current
    10. if (!ensureSlash()) {
    11. return
    12. }
    13. this.transitionTo(getHash(), route => {
    14. if (supportsScroll) {
    15. handleScroll(this.router, route, current, true)
    16. }
    17. if (!supportsPushState) {
    18. replaceHash(route.fullPath)
    19. }
    20. })
    21. })
    22. }

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

    同学们在使用 Vue-Router 开发项目的时候,打开调试页面 http://localhost:8080 后会自动把 url 修改为 http://localhost:8080/#/,这是怎么做到呢?原来在实例化 HashHistory 的时候,构造函数会执行 ensureSlash() 方法:

    1. function ensureSlash (): boolean {
    2. const path = getHash()
    3. if (path.charAt(0) === '/') {
    4. return true
    5. }
    6. replaceHash('/' + path)
    7. return false
    8. }
    9. export function getHash (): string {
    10. // We can't use window.location.hash here because it's not
    11. // consistent across browsers - Firefox will pre-decode it!
    12. const href = window.location.href
    13. const index = href.indexOf('#')
    14. return index === -1 ? '' : href.slice(index + 1)
    15. }
    16. function getUrl (path) {
    17. const href = window.location.href
    18. const i = href.indexOf('#')
    19. const base = i >= 0 ? href.slice(0, i) : href
    20. return `${base}#${path}`
    21. }
    22. function replaceHash (path) {
    23. if (supportsPushState) {
    24. replaceState(getUrl(path))
    25. } else {
    26. window.location.replace(getUrl(path))
    27. }
    28. }
    29. export function replaceState (url?: string) {
    30. pushState(url, true)
    31. }

    这个时候 path 为空,所以执行 replaceHash('/' + path),然后内部会执行一次 getUrl,计算出来的新的 urlhttp://localhost:8080/#/,最终会执行 pushState(url, true),这就是 url 会改变的原因。

    组件

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

    1. export default {
    2. name: 'RouterView',
    3. functional: true,
    4. props: {
    5. name: {
    6. type: String,
    7. default: 'default'
    8. }
    9. },
    10. render (_, { props, children, parent, data }) {
    11. data.routerView = true
    12. const h = parent.$createElement
    13. const name = props.name
    14. const route = parent.$route
    15. const cache = parent._routerViewCache || (parent._routerViewCache = {})
    16. let depth = 0
    17. let inactive = false
    18. while (parent && parent._routerRoot !== parent) {
    19. if (parent.$vnode && parent.$vnode.data.routerView) {
    20. depth++
    21. }
    22. if (parent._inactive) {
    23. inactive = true
    24. }
    25. parent = parent.$parent
    26. }
    27. data.routerViewDepth = depth
    28. if (inactive) {
    29. return h(cache[name], data, children)
    30. }
    31. const matched = route.matched[depth]
    32. if (!matched) {
    33. cache[name] = null
    34. return h()
    35. }
    36. const component = cache[name] = matched.components[name]
    37. data.registerRouteInstance = (vm, val) => {
    38. const current = matched.instances[name]
    39. if (
    40. (val && current !== vm) ||
    41. (!val && current === vm)
    42. ) {
    43. matched.instances[name] = val
    44. }
    45. }
    46. ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
    47. matched.instances[name] = vnode.componentInstance
    48. }
    49. let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    50. if (propsToPass) {
    51. propsToPass = data.props = extend({}, propsToPass)
    52. const attrs = data.attrs = data.attrs || {}
    53. for (const key in propsToPass) {
    54. if (!component.props || !(key in component.props)) {
    55. attrs[key] = propsToPass[key]
    56. delete propsToPass[key]
    57. }
    58. }
    59. }
    60. return h(component, data, children)
    61. }
    62. }

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

    1. const route = parent.$route

    我们之前分析过,在 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. //. ..
    3. this.current = route
    4. this.cb && this.cb(route)
    5. // ...
    6. }

    也就是我们执行 transitionTo 方法最后执行 updateRoute 的时候会执行回调,然后会更新所有组件实例的 _route 值,所以说 $route 对应的就是当前的路由线路。

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

    1. data.routerView = true
    2. // ...
    3. while (parent && parent._routerRoot !== parent) {
    4. if (parent.$vnode && parent.$vnode.data.routerView) {
    5. depth++
    6. }
    7. if (parent._inactive) {
    8. inactive = true
    9. }
    10. parent = parent.$parent
    11. }
    12. const matched = route.matched[depth]
    13. // ...
    14. const component = cache[name] = matched.components[name]

    parent._routerRoot 表示的是根 Vue 实例,那么这个循环就是从当前的 <router-view> 的父节点向上找,一直找到根 Vue 实例,在这个过程,如果碰到了父节点也是 <router-view> 的时候,说明 <router-view> 有嵌套的情况,depth++。遍历完成后,根据当前线路匹配的路径和 depth 找到对应的 RouteRecord,进而找到该渲染的组件。

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

    1. data.registerRouteInstance = (vm, val) => {
    2. const current = matched.instances[name]
    3. if (
    4. (val && current !== vm) ||
    5. (!val && current === vm)
    6. ) {
    7. matched.instances[name] = val
    8. }
    9. }

    vnodedata 定义了 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. // ...
    10. registerInstance(this, this)
    11. },
    12. destroyed () {
    13. registerInstance(this)
    14. }
    15. })

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

    render 函数的最后根据 component 渲染出对应的组件 vonde

    1. return h(component, data, children)

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

    1. Vue.mixin({
    2. beforeCreate () {
    3. if (isDef(this.$options.router)) {
    4. Vue.util.defineReactive(this, '_route', this._router.history.current)
    5. }
    6. // ...
    7. }
    8. })

    由于我们把根 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 属性指定目标地址,默认渲染成带有正确链接的 <a> 标签,可以通过配置 tag 属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。

    <router-link> 比起写死的 <a href="…"> 会好一些,理由如下:

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

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

    当你在 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. exact: Boolean,
    13. append: Boolean,
    14. replace: Boolean,
    15. activeClass: String,
    16. exactActiveClass: String,
    17. event: {
    18. type: eventTypes,
    19. default: 'click'
    20. }
    21. },
    22. render (h: Function) {
    23. const router = this.$router
    24. const current = this.$route
    25. const { location, route, href } = router.resolve(this.to, current, this.append)
    26. const classes = {}
    27. const globalActiveClass = router.options.linkActiveClass
    28. const globalExactActiveClass = router.options.linkExactActiveClass
    29. const activeClassFallback = globalActiveClass == null
    30. ? 'router-link-active'
    31. : globalActiveClass
    32. const exactActiveClassFallback = globalExactActiveClass == null
    33. ? 'router-link-exact-active'
    34. : globalExactActiveClass
    35. const activeClass = this.activeClass == null
    36. ? activeClassFallback
    37. : this.activeClass
    38. const exactActiveClass = this.exactActiveClass == null
    39. ? exactActiveClassFallback
    40. : this.exactActiveClass
    41. const compareTarget = location.path
    42. ? createRoute(null, location, null, router)
    43. : route
    44. classes[exactActiveClass] = isSameRoute(current, compareTarget)
    45. classes[activeClass] = this.exact
    46. ? classes[exactActiveClass]
    47. : isIncludedRoute(current, compareTarget)
    48. const handler = e => {
    49. if (guardEvent(e)) {
    50. if (this.replace) {
    51. router.replace(location)
    52. } else {
    53. router.push(location)
    54. }
    55. }
    56. }
    57. const on = { click: guardEvent }
    58. if (Array.isArray(this.event)) {
    59. this.event.forEach(e => { on[e] = handler })
    60. } else {
    61. on[this.event] = handler
    62. }
    63. const data: any = {
    64. class: classes
    65. }
    66. if (this.tag === 'a') {
    67. data.on = on
    68. data.attrs = { href }
    69. } else {
    70. const a = findAnchor(this.$slots.default)
    71. if (a) {
    72. a.isStatic = false
    73. const extend = _Vue.util.extend
    74. const aData = a.data = extend({}, a.data)
    75. aData.on = on
    76. const aAttrs = a.data.attrs = extend({}, a.data.attrs)
    77. aAttrs.href = href
    78. } else {
    79. data.on = on
    80. }
    81. }
    82. return h(this.tag, data, this.$slots.default)
    83. }
    84. }

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

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

    router.resolveVueRouter 的实例方法,它的定义在 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. normalizedTo: Location,
    10. resolved: Route
    11. } {
    12. const location = normalizeLocation(
    13. to,
    14. current || this.history.current,
    15. append,
    16. this
    17. )
    18. const route = this.match(location, current)
    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. normalizedTo: location,
    27. resolved: route
    28. }
    29. }
    30. function createHref (base: string, fullPath: string, mode) {
    31. var path = mode === 'hash' ? '#' + fullPath : fullPath
    32. return base ? cleanPath(base + '/' + path) : path
    33. }

    它先规范生成目标 location,再根据 locationmatch 通过 this.match 方法计算生成目标路径 route,然后再根据 basefullPaththis.mode 通过 createHref 方法计算出最终跳转的 href

    解析完 router 获得目标 locationroutehref 后,接下来对 exactActiveClassactiveClass 做处理,当配置 exact 为 true 的时候,只有当目标路径和当前路径完全匹配的时候,会添加 exactActiveClass;而当目标路径包含当前路径的时候,会添加 activeClass

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

    1. const handler = e => {
    2. if (guardEvent(e)) {
    3. if (this.replace) {
    4. router.replace(location)
    5. } else {
    6. router.push(location)
    7. }
    8. }
    9. }
    10. function guardEvent (e) {
    11. if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
    12. if (e.defaultPrevented) return
    13. if (e.button !== undefined && e.button !== 0) return
    14. if (e.currentTarget && e.currentTarget.getAttribute) {
    15. const target = e.currentTarget.getAttribute('target')
    16. if (/\b_blank\b/i.test(target)) return
    17. }
    18. if (e.preventDefault) {
    19. e.preventDefault()
    20. }
    21. return true
    22. }
    23. const on = { click: guardEvent }
    24. if (Array.isArray(this.event)) {
    25. this.event.forEach(e => { on[e] = handler })
    26. } else {
    27. on[this.event] = handler
    28. }

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

    1. push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    2. this.history.push(location, onComplete, onAbort)
    3. }
    4. replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    5. this.history.replace(location, onComplete, onAbort)
    6. }

    实际上就是执行了 historypushreplace 方法做路由跳转。

    最后判断当前 tag 是否是 <a> 标签,<router-link> 默认会渲染成 <a> 标签,当然我们也可以修改 tagprop 渲染成其他节点,这种情况下会尝试找它子元素的 <a> 标签,如果有则把事件绑定到 <a> 标签上并添加 href 属性,否则绑定到外层元素本身。

    总结

    那么至此我们把路由的 transitionTo 的主体过程分析完毕了,其他一些分支比如重定向、别名、滚动行为等同学们可以自行再去分析。

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

    原文: https://ustbhuangyi.github.io/vue-analysis/vue-router/transition-to.html