前言

【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 = redirectedFromlet 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}// 返回PromisebeforeRouteEnter(from, to) {    return Promise.resolve(...)}// 返回functionbeforeRouteEnter(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运行完,可能会持续进入thenreturn (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过程: