美文网首页【vue-rouer源码】
【vue-router源码】八、router.go、router

【vue-router源码】八、router.go、router

作者: MAXLZ | 来源:发表于2022-06-10 08:25 被阅读0次

    前言

    【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的执行流程:

    go.png

    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不同的是,一旦出现了一些错误信息(如导航被取消、导航时冗余的、位置错误),需要将历史记录回退到相应位置。

    相关文章

      网友评论

        本文标题:【vue-router源码】八、router.go、router

        本文链接:https://www.haomeiwen.com/subject/wqnmmrtx.html