【vue-router源码】七、router.push、router.replace源码解析

【vue-rouer源码】系列文章

  1. 【vue-router源码】一、router.install解析
  2. 【vue-router源码】二、createWebHistory、createWebHashHistory、createMemoryHistory源码解析
  3. 【vue-router源码】三、理解Vue-router中的Matcher
  4. 【vue-router源码】四、createRouter源码解析
  5. 【vue-router源码】五、router.addRoute、router.removeRoute、router.hasRoute、router.getRoutes源码分析
  6. 【vue-router源码】六、router.resolve源码解析
  7. 【vue-router源码】七、router.push、router.replace源码解析
  8. 【vue-router源码】八、router.go、router.back、router.forward源码解析
  9. 【vue-router源码】九、全局导航守卫的实现
  10. 【vue-router源码】十、isReady源码解析
  11. 【vue-router源码】十一、onBeforeRouteLeave、onBeforeRouteUpdate源码分析
  12. 【vue-router源码】十二、useRoute、useRouter、useLink源码分析
  13. 【vue-router源码】十三、RouterLink源码分析
  14. 【vue-router源码】十四、RouterView源码分析


前言

【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) ||
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MAXLZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值