乐趣区

关于前端:vuerouter源码八routergorouterbackrouterforward源码解析

前言

【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 不同的是,一旦呈现了一些错误信息(如导航被勾销、导航时冗余的、地位谬误),须要将历史记录回退到相应地位。

退出移动版