likes
comments
collection
share

【vue-router源码】七、router.push、router.replace源码解析

作者站长头像
站长
· 阅读数 268

前言

【vue-router源码】系列文章将带你从0开始了解vue-router的具体实现。该系列文章源码参考vue-router v4.0.15。源码地址:https://github.com/vuejs/router阅读该文章的前提是你最好了解vue-router的基本使用,如果你没有使用过的话,可通过vue-router官网学习下。

该篇文章将分析router.pushrouter.replace的实现,通过该文章你会了解一个稍微完整的导航解析流程。

使用

使用router.push方法导航到不同的 URL。这个方法会向history栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,会回到之前的 URL。

使用router.replace方法导航到不同的 URL。这个方法会在history栈替换历史记录。

router.push('/search?name=pen')
router.push({ path: '/search', query: { name: 'pen' } })
router.push({ name: 'search', query: { name: 'pen' } })
// 以上三种方式是等效的。

router.replace('/search?name=pen')
router.replace({ path: '/search', query: { name: 'pen' } })
router.replace({ name: 'search', query: { name: 'pen' } })
// 以上三种方式是等效的。

push

push方法接收一个to参数,表示要跳转的路由,它可以是个字符串,也可以是个对象。在push方法中调用了一个pushWithRedirect函数,并返回其结果。

function push(to: RouteLocationRaw | RouteLocation) {
  return pushWithRedirect(to)
}

pushWithRedirect接收两个参数:toredirectedFrom,并返回pushWithRedirect的结果。其中to是要跳转到的路由,redirectedFrom代表to是从哪个路由重定向来的,如果多次重定向,它只是最初重定向的那个路由。

function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
  // ...
}

因为要到的to中可能存在重定向,所以pushWithRedirect中首先要处理重定向:当to中存在重定向时,递归调用pushWithRedirect

// 将to处理为规范化的路由
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
// 当前路由
const from = currentRoute.value
// 使用 History API(history.state) 保存的状态
const data: HistoryState | undefined = (to as RouteLocationOptions).state
// force代表强制触发导航,即使与当前位置相同
const force: boolean | undefined = (to as RouteLocationOptions).force
// replace代表是否替换当前历史记录
const replace = (to as RouteLocationOptions).replace === true

// 获取要重定向的记录
const shouldRedirect = handleRedirectRecord(targetLocation)

// 如果需要重定向,递归调用pushWithRedirect方法
if (shouldRedirect)
  return pushWithRedirect(
    assign(locationAsObject(shouldRedirect), {
      state: data,
      force,
      replace,
    }),
    // 重定向的根来源
    redirectedFrom || targetLocation
  )

handleRedirectRecord函数的实现:

function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
  // 找到匹配的路由,to.matched中的路由顺序是父路由在子路由前面,所以最后一个路由是我们的最终路由
  const lastMatched = to.matched[to.matched.length - 1]
  // 如果路由存在redirect
  if (lastMatched && lastMatched.redirect) {
    const { redirect } = lastMatched
    // 如果redirect是函数,需要执行函数
    let newTargetLocation =
      typeof redirect === 'function' ? redirect(to) : redirect

    // 如果newTargetLocation是string
    if (typeof newTargetLocation === 'string') {
      // 如果newTargetLocation中存在?或#,需要将newTargetLocation解析成一个LocationNormalized类型的对象
      newTargetLocation =
        newTargetLocation.includes('?') || newTargetLocation.includes('#')
          ? (newTargetLocation = locationAsObject(newTargetLocation))
          : { path: newTargetLocation }
      // 设置params为一个空对象
      newTargetLocation.params = {}
    }

    // 如果newTargetLocation中没有path和name属性,则无法找到重定向的路由,开发环境下进行提示
    if (
      __DEV__ &&
      !('path' in newTargetLocation) &&
      !('name' in newTargetLocation)
    ) {
      warn(
        `Invalid redirect found:\n${JSON.stringify(
          newTargetLocation,
          null,
          2
        )}\n when navigating to "${
          to.fullPath
        }". A redirect must contain a name or path. This will break in production.`
      )
      throw new Error('Invalid redirect')
    }

    return assign(
      {
        query: to.query,
        hash: to.hash,
        params: to.params,
      },
      newTargetLocation
    )
  }
}

处理完重定向后,接下来会检测要跳转到的路由和当前路由是否为同一个路由,如果是同一个路由并且不强制跳转,会创建一个失败函数赋给failure,然后处理滚动行为。

const toLocation = targetLocation as RouteLocationNormalized

// 设置重定向的来源
toLocation.redirectedFrom = redirectedFrom
let failure: NavigationFailure | void | undefined

// 如果要跳转到的路由与当前路由一致并且不强制跳转
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
  // 创建一个错误信息,该错误信息代表重复的导航
  failure = createRouterError<NavigationFailure>(
    ErrorTypes.NAVIGATION_DUPLICATED,
    { to: toLocation, from }
  )
  // 处理滚动行为
  handleScroll(
    from,
    from,
    true,
    false
  )
}

关于handleScroll的实现如下:首先从options中找到scrollBehavior选项,如果不是浏览器环境或不存在scrollBehavior,返回一个Promise对象。相反,获取滚动位置(根据历史记录中的position和path获取),然后在下一次DOM刷新后,执行定义的滚动行为函数,滚动行为函数执行完后,将滚动行为函数结果作为最终的滚动位置将页面滚动到指定位置。

function handleScroll(
  to: RouteLocationNormalizedLoaded,
  from: RouteLocationNormalizedLoaded,
  isPush: boolean,
  isFirstNavigation: boolean
): Promise<any> {
  const { scrollBehavior } = options
  if (!isBrowser || !scrollBehavior) return Promise.resolve()

  // 获取滚动位置
  const scrollPosition: _ScrollPositionNormalized | null =
    (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
    ((isFirstNavigation || !isPush) &&
      (history.state as HistoryState) &&
      history.state.scroll) ||
    null

  // 下一次DOM更新后触发滚动行为,滚动行为执行完后,滚动到指定位置
  return nextTick()
    .then(() => scrollBehavior(to, from, scrollPosition))
    .then(position => position && scrollToPosition(position))
    .catch(err => triggerError(err, to, from))
}

export function getScrollKey(path: string, delta: number): string {
  // history.state.position记录着当前路由在历史记录中的位置,该位置从0开始
  const position: number = history.state ? history.state.position - delta : -1
  // key值为 在历史记录中的位置+path
  return position + path
}

export function getSavedScrollPosition(key: string) {
  // 根据key值查找滚动位置
  const scroll = scrollPositions.get(key)
  // 查完后,删除对应记录
  scrollPositions.delete(key)
  return scroll
}

pushWithRedirect最后返回一个Promise。如果有failure,返回failure。如果没有failure则执行navigate(toLocation, from)

那么navigate是做什么的呢?navigate函数接收两个参数:tofrom

navigate中首先调用了一个extractChangingRecords函数,该函数的作用是将fromto所匹配到的路由分别存到三个数组中:fromto所共有的路由放入updatingRecords(正在更新的路由)、from独有的路由放入leavingRecords(正要离开的路由)、to独有的路由放入enteringRecords(正在进入的新路由)。紧接着又调用了一个extractComponentsGuards函数,用来获取组件内的beforeRouteLeave钩子,注意extractComponentsGuards函数只能获取使用beforeRouteLeave(){}方式注册的函数,对于使用onBeforeRouteLeave注册的函数需要单独处理。

const [leavingRecords, updatingRecords, enteringRecords] =
  extractChangingRecords(to, from)

guards = extractComponentsGuards(
  // 这里leavingRecords需要反转,因为matched中的顺序是父路由在子路由前,当离开时,应先离开子路由再离开父路由
  leavingRecords.reverse(),
  'beforeRouteLeave',
  to,
  from
)

// 向guards中添加使用onBeforeRouteLeave方式注册的方法
for (const record of leavingRecords) {
  record.leaveGuards.forEach(guard => {
    guards.push(guardToPromiseFn(guard, to, from))
  })
}

// 如果发生了新的导航canceledNavigationCheck可以帮助跳过后续所有的导航
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
  null,
  to,
  from
)

guards.push(canceledNavigationCheck)

extractChangingRecords的实现过程:如果tofrom配配到的路由中有公共的,说明这些路由在跳转过程中是更新操作,将其加入updatingRecords中;如果是from所匹配到独有的路由,说明要离开这些路由,将它们放入leavingRecords中;相反,如果to匹配到的路由中,from没有匹配到,说明是新的路由,将它们放入enteringRecords中。

function extractChangingRecords(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
  // 要离开的路由
  const leavingRecords: RouteRecordNormalized[] = []
  // 更新的路由
  const updatingRecords: RouteRecordNormalized[] = []
  // 要进入的新的路由(在from.matched中未出现过)
  const enteringRecords: RouteRecordNormalized[] = []

  const len = Math.max(from.matched.length, to.matched.length)
  for (let i = 0; i < len; i++) {
    const recordFrom = from.matched[i]
    if (recordFrom) {
      // 如果recordFrom在to.matched中存在,将recordFrom加入到updatingRecords,否则加入到leavingRecords中
      if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
        updatingRecords.push(recordFrom)
      else leavingRecords.push(recordFrom)
    }
    const recordTo = to.matched[i]
    if (recordTo) {
      // 如果recordTo在from.matched中找不到,说明是个新的路由,将recordTo加入到enteringRecords
      if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
        enteringRecords.push(recordTo)
      }
    }
  }

  return [leavingRecords, updatingRecords, enteringRecords]
}

extractComponentsGuards是专门用来从路由组件中提取钩子函数的。extractComponentsGuards接收四个参数:matched(从tofrom中提取出的leavingRecordsupdatingRecordsenteringRecords之一)、guardType(钩子类型,可以取的值beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave)、tofrom。返回值是一个钩子函数列表。

export function extractComponentsGuards(
  matched: RouteRecordNormalized[],
  guardType: GuardType,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
  // 声明一个数组保存钩子函数
  const guards: Array<() => Promise<void>> = []

  for (const record of matched) {
    // 遍历路由对应的组件components
    for (const name in record.components) {
      let rawComponent = record.components[name]
      // 开发环境下进行提示
      if (__DEV__) {
        // 如果组件不存在或组件不是object和function,提示不是有效的组件
        if (
          !rawComponent ||
          (typeof rawComponent !== 'object' &&
            typeof rawComponent !== 'function')
        ) {
          warn(
            `Component "${name}" in record with path "${record.path}" is not` +
              ` a valid component. Received "${String(rawComponent)}".`
          )
          // 抛出错误
          throw new Error('Invalid route component')
        } else if ('then' in rawComponent) { // 如果使用import('./xxx.vue')的方式使用组件,进行提示,并转为() => import('./xxx.vue')
          warn(
            `Component "${name}" in record with path "${record.path}" is a ` +
              `Promise instead of a function that returns a Promise. Did you ` +
              `write "import('./MyPage.vue')" instead of ` +
              `"() => import('./MyPage.vue')" ? This will break in ` +
              `production if not fixed.`
          )
          const promise = rawComponent
          rawComponent = () => promise
        } else if (
          (rawComponent as any).__asyncLoader &&
          // warn only once per component
          !(rawComponent as any).__warnedDefineAsync
        ) { // 如果使用defineAsyncComponent()方式定义的组件,进行提示
          ;(rawComponent as any).__warnedDefineAsync = true
          warn(
            `Component "${name}" in record with path "${record.path}" is defined ` +
              `using "defineAsyncComponent()". ` +
              `Write "() => import('./MyPage.vue')" instead of ` +
              `"defineAsyncComponent(() => import('./MyPage.vue'))".`
          )
        }
      }

      // 如果路由组件没有被挂载跳过update和leave钩子
      if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue

      // 如果是个路由组件
      // 路由组件需要满足:rawComponent是object || rawComponent有['displayName', 'props`、`__vccOpts`]中的任一属性
      if (isRouteComponent(rawComponent)) {
        // __vccOpts是由vue-class-component添加的
        const options: ComponentOptions =
          (rawComponent as any).__vccOpts || rawComponent
        const guard = options[guardType]
        // 向guards中添加一个异步函数
        guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
      } else {
        // 能进入这个方法的表示rawComponent是个函数;例如懒加载() => import('./xx.vue');函数式组件() => h('div', 'HomePage')
        // 注意这个的分支只发生在调用beforeRouteEnter之前,后续过程不会进行该过程。
        // 因为在调用beforeRouteEnter钩子之前,会进行异步路由组件的解析,一旦异步路由组件解析成功,会将解析后的组件挂载至对应的components[name]下
        
        // 执行rawComponent,例如懒加载() => import('./xx.vue');如果函数式组件未声明displayName也会进入此分支
        let componentPromise: Promise<
          RouteComponent | null | undefined | void
        > = (rawComponent as Lazy<RouteComponent>)()

        // 对于函数式组件需要添加一个displayName属性,如果没有,进行提示,并将componentPromise转为一个Promise
        if (__DEV__ && !('catch' in componentPromise)) {
          warn(
            `Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`
          )
          componentPromise = Promise.resolve(componentPromise as RouteComponent)
        }

        // 向guards中添加一个钩子函数,在这个钩子的执行过程中先解析异步路由组件,然后调用钩子函数
        guards.push(() =>
          componentPromise.then(resolved => {
            // 如果解析失败抛出错误
            if (!resolved)
              return Promise.reject(
                new Error(
                  `Couldn't resolve component "${name}" at "${record.path}"`
                )
              )
            // 判断解析后的组件是否为esm,如果是esm,需要取resolved.default
            const resolvedComponent = isESModule(resolved)
              ? resolved.default
              : resolved
            // 使用解析完的组件替换对应的components[name]
            record.components[name] = resolvedComponent
            const options: ComponentOptions =
              (resolvedComponent as any).__vccOpts || resolvedComponent
            // 对应的组件内的钩子
            const guard = options[guardType]
            // 钩子转promise,并执行
            return guard && guardToPromiseFn(guard, to, from, record, name)()
          })
        )
      }
    }
  }

  return guards
}

navigate函数最后会调用guards中的钩子,并在beforeRouteLeave钩子执行完后执行了一系列操作。其实在这里就体现了vue-router中钩子的执行顺序:

return (
    runGuardQueue(guards)
      .then(() => {
        // 调用全局beforeEach钩子
        guards = []
        for (const guard of beforeGuards.list()) {
          guards.push(guardToPromiseFn(guard, to, from))
        }
        guards.push(canceledNavigationCheck)

        return runGuardQueue(guards)
      })
      .then(() => {
        // 获取组件中的beforeRouteUpdate钩子,以beforeRouteUpdate() {}方式声明
        guards = extractComponentsGuards(
          updatingRecords,
          'beforeRouteUpdate',
          to,
          from
        )

        // 以onBeforeRouteUpdate注册的
        for (const record of updatingRecords) {
          record.updateGuards.forEach(guard => {
            guards.push(guardToPromiseFn(guard, to, from))
          })
        }
        guards.push(canceledNavigationCheck)

        // 调用beforeRouteUpdate钩子
        return runGuardQueue(guards)
      })
      .then(() => {
        guards = []
        for (const record of to.matched) {
          // 不在重用视图上触发beforeEnter
          // 路由配置中有beforeEnter,并且from不匹配record
          if (record.beforeEnter && !from.matched.includes(record)) {
            if (Array.isArray(record.beforeEnter)) {
              for (const beforeEnter of record.beforeEnter)
                guards.push(guardToPromiseFn(beforeEnter, to, from))
            } else {
              guards.push(guardToPromiseFn(record.beforeEnter, to, from))
            }
          }
        }
        guards.push(canceledNavigationCheck)

        // 调用路由配置中的beforeEnter
        return runGuardQueue(guards)
      })
      .then(() => {

        // 清除存在的enterCallbacks 由extractComponentsGuards添加
        to.matched.forEach(record => (record.enterCallbacks = {}))

        // 获取被激活组件中的beforeRouteEnter钩子,在之前会处理异步路由组件
        guards = extractComponentsGuards(
          enteringRecords,
          'beforeRouteEnter',
          to,
          from
        )
        guards.push(canceledNavigationCheck)

        return runGuardQueue(guards)
      })
      .then(() => {
        guards = []
        // 处理全局beforeResolve钩子
        for (const guard of beforeResolveGuards.list()) {
          guards.push(guardToPromiseFn(guard, to, from))
        }
        guards.push(canceledNavigationCheck)

        return runGuardQueue(guards)
      })
      // 捕获任何取消的导航
      .catch(err =>
        isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
          ? err
          : Promise.reject(err)
      )
  )

截止目前一个欠完整的导航的解析流程(包含钩子的执行顺序)如下 :

  1. 导航被触发
  2. 调用失活组件中的beforeRouteLeave钩子
  3. 调用全局beforeEach钩子
  4. 调用重用组件内的beforeRouteUpdate钩子
  5. 调用路由配置中的beforeEnter钩子
  6. 解析异步路由组件
  7. 调用激活组件中的beforeRouteEnter钩子
  8. 调用全局的beforeResolve钩子

你可能发现了,在每放入一个周期的钩子函数之后,都会紧跟着向guards中添加一个canceledNavigationCheck函数。这个canceledNavigationCheck的函数作用是如果在导航期间有了新的导航,则会reject一个ErrorTypes.NAVIGATION_CANCELLED错误信息。

function checkCanceledNavigationAndReject(
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
): Promise<void> {
  const error = checkCanceledNavigation(to, from)
  return error ? Promise.reject(error) : Promise.resolve()
}

function checkCanceledNavigation(
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
): NavigationFailure | void {
  if (pendingLocation !== to) {
    return createRouterError<NavigationFailure>(
      ErrorTypes.NAVIGATION_CANCELLED,
      {
        from,
        to,
      }
    )
  }
}

在向guards中放入钩子时,都使用了一个guardToPromiseFnguardToPromiseFn可以将钩子函数转为promise函数。

export function guardToPromiseFn(
  guard: NavigationGuard,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded,
  record?: RouteRecordNormalized,
  name?: string
): () => Promise<void> {
  const enterCallbackArray =
    record &&
    (record.enterCallbacks[name!] = record.enterCallbacks[name!] || [])

  return () =>
    new Promise((resolve, reject) => {
      // 这个next函数就是beforeRouteEnter中的next
      const next: NavigationGuardNext = (
        valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
      ) => {
        // 如果调用next时传入的是false,取消导航
        if (valid === false)
          reject(
            createRouterError<NavigationFailure>(
              ErrorTypes.NAVIGATION_ABORTED,
              {
                from,
                to,
              }
            )
          )
        else if (valid instanceof Error) { // 如果传入了一个Error实例
          reject(valid)
        } else if (isRouteLocation(valid)) { // 如果是个路由。可以进行重定向
          reject(
            createRouterError<NavigationRedirectError>(
              ErrorTypes.NAVIGATION_GUARD_REDIRECT,
              {
                from: to,
                to: valid,
              }
            )
          )
        } else {
          // 如果valid是个函数,会将这个函数添加到record.enterCallbacks[name]中
          // 关于record.enterCallbacks的执行时机,将会在RouterView中进行分析
          if (
            enterCallbackArray &&
            // since enterCallbackArray is truthy, both record and name also are
            record!.enterCallbacks[name!] === enterCallbackArray &&
            typeof valid === 'function'
          )
            enterCallbackArray.push(valid)
          resolve()
        }
      }

      // 调用guard,绑定this为组件实例
      const guardReturn = guard.call(
        record && record.instances[name!],
        to,
        from,
        // next应该只允许被调用一次,如果使用了多次开发环境下给出提示
        __DEV__ ? canOnlyBeCalledOnce(next, to, from) : next
      )
      // 使用Promise.resolve包装guard的返回结果,以允许异步guard
      let guardCall = Promise.resolve(guardReturn)

      // 如果guard参数小于3,guardReturn会作为next的参数
      if (guard.length < 3) guardCall = guardCall.then(next)
      // 如果guard参数大于2
      if (__DEV__ && guard.length > 2) {
        const message = `The "next" callback was never called inside of ${
          guard.name ? '"' + guard.name + '"' : ''
        }:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.`
        // guardReturn是个promise
        if (typeof guardReturn === 'object' && 'then' in guardReturn) {
          guardCall = guardCall.then(resolvedValue => {
            // 未调用next。如:
            // beforeRouteEnter(to, from ,next) {
            //  return Promise.resolve(11)
            // }
            if (!next._called) {
              warn(message)
              return Promise.reject(new Error('Invalid navigation guard'))
            }
            return resolvedValue
          })
          // TODO: test me!
        } else if (guardReturn !== undefined) {
          // 如果有返回值,并且未调用next。如
          // beforeRouteEnter(to, from ,next) {
          //  return 11
          // }
          if (!next._called) {
            warn(message)
            reject(new Error('Invalid navigation guard'))
            return
          }
        }
      }
      // 捕获错误
      guardCall.catch(err => reject(err))
    })
}

guardToPromiseFn中声明的的next方法会作为钩子函数的第三个参数。如果在使用钩子函数时,形参的数量<3,那么钩子函数的返回值会作为next函数的参数;形参数量>2时,如果钩子函数的返回值是Promise,但未调用next,会抛出错误Invalid navigation guard,如果钩子函数的返回值不为undefined,也未调用next也会抛出错误Invalid navigation guard

所以如果在使用路由钩子的过程中,如果钩子函数的形参>2,也就是你的形参中有next,你必须要调用next。如果你不想自己调用next,那么你要保证形参<2,同时钩子函数返回某个数据,这样vue-router会自动调用next。这里需要注意如果传递给next的参数是个function,那么这个function会被存入record.enterCallbacks[name]中,关于enterCallbacks的执行时机,在这里不去深究,在后续的RouterView源码分析中,你会得到你想要的答案。关于钩子函数中next的使用以下是一些示例:

beforeRouteEnter(from, to) {
    return false
}
// 等同于
beforeRouteEnter(from, to, next) {
    next(false)
}
// 不能写为如下
beforeRouteEnter(from, to, next) {
    return false
}

// 返回Promise
beforeRouteEnter(from, to) {
    return Promise.resolve(...)
}
// 返回function
beforeRouteEnter(from, to) {
    return function() { ... }
}

执行钩子列表的函数runGuardQueue,只有当前钩子执行完毕,才会执行下一个钩子:

function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
  return guards.reduce(
    (promise, guard) => promise.then(() => guard()),
    Promise.resolve()
  )
}

pushWithRedirect函数最后,在navigate执行完后并没有结束,而是又进行了以下操作:

// 首先判断之前的操作是否出错
// 如果出错,将failure使用Promise.resolve包装,进入.then
// 如果未出错,调用navigate(),navigate过程中失败,进入.catch,成功进入.then
// 注意这里catch发生在then之前,所以catch运行完,可能会继续进入then
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
  .catch((error: NavigationFailure | NavigationRedirectError) =>
    isNavigationFailure(error)
      ? 
      isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
        ? error // navigate过程中发生的重定向,进入.then
        : markAsReady(error)
      : // reject 未知的错误
      triggerError(error, toLocation, from)
  )
  .then((failure: NavigationFailure | NavigationRedirectError | void) => {
    if (failure) {
      // 如果是重定向错误
      if (
        isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
      ) {
        // 如果是循环的重定向(检测循环次数超过10次)
        if (
          __DEV__ &&
          // 重定向的位置与toLocation相同
          isSameRouteLocation(
            stringifyQuery,
            resolve(failure.to),
            toLocation
          ) &&
          redirectedFrom &&
          // 循环次数
          (redirectedFrom._count = redirectedFrom._count
            ? 
            redirectedFrom._count + 1
            : 1) > 10
        ) {
          warn(
            `Detected an infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This will break in production if not fixed.`
          )
          return Promise.reject(
            new Error('Infinite redirect in navigation guard')
          )
        }

        // 递归调用pushWithRedirect,进行重定向
        return pushWithRedirect(
          // keep options
          assign(locationAsObject(failure.to), {
            state: data,
            force,
            replace,
          }),
          // preserve the original redirectedFrom if any
          redirectedFrom || toLocation
        )
      }
    } else {
      // 如果在navigate过程中没有抛出错误信息
      failure = finalizeNavigation(
        toLocation as RouteLocationNormalizedLoaded,
        from,
        true,
        replace,
        data
      )
    }
    // 触发全局afterEach钩子
    triggerAfterEach(
      toLocation as RouteLocationNormalizedLoaded,
      from,
      failure
    )
    return failure
  })

可以发现,如果navigate过程执行顺利的话,最后会执行一个finalizeNavigation方法,然后触发全局afterEach钩子。那么我们来看下finalizeNavigation是做什么的。

function finalizeNavigation(
  toLocation: RouteLocationNormalizedLoaded,
  from: RouteLocationNormalizedLoaded,
  isPush: boolean,
  replace?: boolean,
  data?: HistoryState
): NavigationFailure | void {
  // 检查是否取消了导航
  const error = checkCanceledNavigation(toLocation, from)
  if (error) return error

  // 第一次导航
  const isFirstNavigation = from === START_LOCATION_NORMALIZED
  const state = !isBrowser ? {} : history.state

  // 仅当用户进行了push/replace并且不是初始导航时才更改 URL,因为它只是反映了 url
  if (isPush) {
    // replace为true或首次导航,使用routerHistory.replace 
    if (replace || isFirstNavigation)
      routerHistory.replace(
        toLocation.fullPath,
        assign(
          {
            // 如果是第一次导航,重用history.state中的scroll
            scroll: isFirstNavigation && state && state.scroll,
          },
          data
        )
      )
    else routerHistory.push(toLocation.fullPath, data)
  }

  // toLocation成为了当前导航
  currentRoute.value = toLocation
  // 处理滚动
  handleScroll(toLocation, from, isPush, isFirstNavigation)

  // 路由相关操作准备完毕
  markAsReady()
}

可以看出finalizeNavigation函数的作用是确认我们的导航,它主要做两件事:改变url(如果需要改变)、处理滚动行为。在最后有个markAsReady方法,我们继续看markAsReady是做什么的。

function markAsReady<E = any>(err?: E): E | void {
  // 只在ready=false时进行以下操作
  if (!ready) {
    // 如果发生错误,代表还是未准备好
    ready = !err
    // 设置监听器
    setupListeners()
    // 执行ready回调
    readyHandlers
      .list()
      .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
    // 重置ready回调列表
    readyHandlers.reset()
  }
  return err
}

markAsReady函数会标记路由的准备状态,执行通过isReady添加的回调。

截止到此,push方法也就结束了,此时一个欠完整的的导航解析流程可以更新为:

  1. 导航被触发
  2. 调用失活组件中的beforeRouteLeave钩子
  3. 调用全局beforeEach钩子
  4. 调用重用组件内的beforeRouteUpdate钩子
  5. 调用路由配置中的beforeEnter钩子
  6. 解析异步路由组件
  7. 调用激活组件中的beforeRouteEnter钩子
  8. 调用全局的beforeResolve钩子
  9. 导航被确认10.调用全局的afterEach钩子

剩余的流程,我们将在RouterView中继续进行补充。

replace

replacepush作用几乎相同,如果push时指定replace: true,那么和直接使用replace一致。

function replace(to: RouteLocationRaw | RouteLocationNormalized) {
  return push(assign(locationAsObject(to), { replace: true }))
}

这里调用了一个locationAsObject,如果tostring,会调用parseURL解析to,关于parseURL的实现可参考之前router.resolve的分析,它的主要作用是将to解析成一个含有fullPathfullPath = path + searchString + hash)、path(一个绝对路径)、queryquery对象)、hash##之后的字符串)的对象。

function locationAsObject(
  to: RouteLocationRaw | RouteLocationNormalized
): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
  return typeof to === 'string'
    ? parseURL(parseQuery, to, currentRoute.value.path)
    : assign({}, to)
}

总结

简单描述push的执行流程:先进行重定向的判断,如果需要重定向,立马指向重定向的路由;然后判断要跳转到的路由地址与from的路由地址是否相同,如果相同,在未指定force的情况下,会创建一个错误信息,并处理滚动行为;紧接着调用extractChangingRecords,将tofrom所匹配到的路由进行分组,并依此提取并执行钩子函数,如果过程中不出错的话,最后会执行finalizeNavigation方法,在finalizeNavigation调用routerHistory.reaplce/push更新历史栈,并处理滚动,最后执行markAsReady,将ready设置为true,并调用通过isReady添加的方法。

通过分析push的实现过程,我们可以初步得出了一个稍微完整的导航解析流程:

  1. 导航被触发
  2. 调用失活组件中的beforeRouteLeave钩子
  3. 调用全局beforeEach钩子
  4. 调用重用组件内的beforeRouteUpdate钩子
  5. 调用路由配置中的beforeEnter钩子
  6. 解析异步路由组件
  7. 调用激活组件中的beforeRouteEnter钩子
  8. 调用全局的beforeResolve钩子
  9. 导航被确认
  10. 调用全局的afterEach钩子

下面我们使用流程图来总结下整个push过程:

【vue-router源码】七、router.push、router.replace源码解析