关于前端:vuerouter源码七routerpushrouterreplace源码解析

37次阅读

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

前言

【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 过程:

正文完
 0