前言

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

本篇文章将介绍router.gorouter.backrouter.forward的实现。

应用

go函数容许你在历史中后退或后退,指定步数如果>0,代表后退;<0代表后退。

router.go(-2)router.back()// 等同于router.go(-1)router.forward()// 等同于router.go(1)

go

go接管一个参数delta,示意绝对以后页面,挪动多少步,正数示意后退,负数示意后退。

const go = (delta: number) => routerHistory.go(delta)

routerHistory.go中会调用history.go,进而触发popstate监听函数。如果你看过之前createWebHistory的解析,你会晓得在createWebHistory中通过useHistoryListeners创立historyListeners时,会注册一个popstate监听函数,这个监听函数在调用history.go后就会触发。

// 文件地位:src/history/html5.ts useHistoryListeners办法window.addEventListener('popstate', popStateHandler)const popStateHandler: PopStateListener = ({ state,}: {  state: StateEntry | null}) => {  // 以后location,字符串  const to = createCurrentLocation(base, location)  const from: HistoryLocation = currentLocation.value  const fromState: StateEntry = historyState.value  let delta = 0  // 如果不存在state  // 对于为什么state可能为空,可参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event  if (state) {    currentLocation.value = to    historyState.value = state    // 如果暂停监听了,并且暂停时的状态是from,间接return    if (pauseState && pauseState === from) {      pauseState = null      return    }    // 计算挪动的步数    delta = fromState ? state.position - fromState.position : 0  } else {    replace(to)  }  // 循环调用监听函数  listeners.forEach(listener => {    listener(currentLocation.value, from, {      delta,      type: NavigationType.pop,      direction: delta        ? delta > 0          ? NavigationDirection.forward          : NavigationDirection.back        : NavigationDirection.unknown,    })  })}

能够看到,在监听函数最初会循环调用listeners中的listener,那么listener是什么?什么时候被增加的呢?

在后面文章介绍install的实现时,其中有一步十分重要的操作就是要依据地址栏的url进行第一次跳转。而个跳转是通过调用push办法实现的,因为push会调用pushWidthRedirect办法,在pushWidthRedirect中的最初会执行finalizeNavigation(不思考两头reject谬误)。而在finalizeNavigation中的最初会调用一个markAsReady办法。

function markAsReady<E = any>(err?: E): E | void {  if (!ready) {    // still not ready if an error happened    ready = !err    setupListeners()    readyHandlers      .list()      .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))    readyHandlers.reset()  }  return err}

markAsReady中调用了setupListeners的一个办法。在这个办法中会调用routerHistory.listen()增加一个函数。

let removeHistoryListener: undefined | null | (() => void)function setupListeners() {  // 如果有removeHistoryListener,阐明曾经增加过listener  if (removeHistoryListener) return  // 调用routerHistory.listen增加监听函数,routerHistory.listen返回一个删除这个listener函数  removeHistoryListener = routerHistory.listen((to, _from, info) => {    const toLocation = resolve(to) as RouteLocationNormalized    // 确定是否存在重定向    const shouldRedirect = handleRedirectRecord(toLocation)    if (shouldRedirect) {      pushWithRedirect(        assign(shouldRedirect, { replace: true }),        toLocation      ).catch(noop)      return    }    pendingLocation = toLocation    const from = currentRoute.value    // 保留from滚动地位    if (isBrowser) {      saveScrollPosition(        getScrollKey(from.fullPath, info.delta),        computeScrollPosition()      )    }        navigate(toLocation, from)      .catch((error: NavigationFailure | NavigationRedirectError) => {        // 导航被勾销        if (          isNavigationFailure(            error,            ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED          )        ) {          return error        }        // 在钩子中进行了重定向        if (          isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)        ) {          pushWithRedirect(            (error as NavigationRedirectError).to,            toLocation          )            .then(failure => {              // 钩子中的重定向过程中如果导航被勾销或导航冗余,回退一步              if (                isNavigationFailure(                  failure,                  ErrorTypes.NAVIGATION_ABORTED |                    ErrorTypes.NAVIGATION_DUPLICATED                ) &&                !info.delta &&                info.type === NavigationType.pop              ) {                routerHistory.go(-1, false)              }            })            .catch(noop)          return Promise.reject()        }        // 复原历史记录,,但不触发监听        if (info.delta) routerHistory.go(-info.delta, false)        // 无奈辨认的谬误,交给全局谬误处理器        return triggerError(error, toLocation, from)      })      .then((failure: NavigationFailure | void) => {        failure =          failure ||          finalizeNavigation(            toLocation as RouteLocationNormalizedLoaded,            from,            false          )                if (failure) {          // 如果存在错误信息,回到原始地位,但不触发监听          if (info.delta) {            routerHistory.go(-info.delta, false)          } else if (            info.type === NavigationType.pop &&            isNavigationFailure(              failure,              ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED            )          ) { // 谬误类型时导航被勾销或冗余,回退历史记录,但不触发监听            routerHistory.go(-1, false)          }        }        // 触发全局afterEach钩子        triggerAfterEach(          toLocation as RouteLocationNormalizedLoaded,          from,          failure        )      })      .catch(noop)  })}

能够看到这个监听函数和push的过程十分相似,与push不同的是,在触发监听时,一旦呈现了一些错误信息(如导航被勾销、导航时冗余的、地位谬误),须要将历史记录回退到相应地位。

go的执行流程:

back

back,回退一个历史记录,相当于go(-1)

const router = {  // ...  back: () => go(-1),  // ...}

forward

forward,后退一个历史记录,相当于go(1)

const router = {  // ...  forward: () => go(1),  // ...}

总结

gobackforward办法最终通过调用history.go办法,触发popstate事件(popstate中的监听函数在第一次路由跳转时被增加),而在popstate事件中的过程和push的过程是十分相似的,与push不同的是,一旦呈现了一些错误信息(如导航被勾销、导航时冗余的、地位谬误),须要将历史记录回退到相应地位。