Vue-Router源码分析:push

542 阅读5分钟

我会把整个源码过一遍,整理出来并实现一个简易版的vue-router。 背源码不是目的,通过对源码的学习,理解设计思想,提升coding的能力才是关键。

1.基本使用

//https://router.vuejs.org/zh/guide/essentials/navigation.html
// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

2. 源码分析

下面来看看源码中的具体实现:

function push(to: RouteLocationRaw) {
  return pushWithRedirect(to)
}

RouteLocationRaw类型参考: router.vuejs.org/zh/api/#rou…

进入pushWithRedirect :

const targetLocation: RouteLocation = (pendingLocation = resolve(to))
const shouldRedirect = handleRedirectRecord(targetLocation)

if (shouldRedirect)
  return pushWithRedirect(
    assign(locationAsObject(shouldRedirect), {
      state:
        typeof shouldRedirect === 'object'
          ? assign({}, data, shouldRedirect.state)
          : data,
      force,
      replace,
    }),
    redirectedFrom || targetLocation
  )
function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {

  //resolve方法用于解析 URL 和路由配置之间的映射关系
  const Redirect: RouteLocation = (pendingLocation = resolve(to))
  const from = currentRoute.value
  const data: HistoryState | undefined = (to as RouteLocationOptions).state
  
  //force是即使跳转路由相同也会跳转
  const force: boolean | undefined = (to as RouteLocationOptions).force
  
  //如果replace是true,那么即使force为true,也不会添加一条历史记录
  //如果replace为false,force为true,那么依然会跳转,并且添加一条记录
  const replace = (to as RouteLocationOptions).replace === true
  
  //这个方法主要就是判断要跳转的路由有没有redirect
  const shouldRedirect = handleRedirectRecord(targetLocation)
  
  //如果是重定向的路由, 就把重定向的目标路由传入这个方法继续调用,完成后直接返回
  if (shouldRedirect)
    return pushWithRedirect(
      assign(locationAsObject(shouldRedirect), {
        state:
          typeof shouldRedirect === 'object'
            ? assign({}, data, shouldRedirect.state)
            : data,
        force,
        replace,
      }),
      redirectedFrom || targetLocation
    )

  // 到这重定向的路由已经处理完了
  const toLocation = targetLocation as RouteLocationNormalized

  toLocation.redirectedFrom = redirectedFrom
  let failure: NavigationFailure | void | undefined
   
  //如果force为false(路由相同是不跳转)并且当前路由和跳转路由相同
  if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
  //跳转失败
    failure = createRouterError<NavigationFailure>(
      ErrorTypes.NAVIGATION_DUPLICATED,
      { to: toLocation, from }
    )
    // 触发滚动
    handleScroll(
      from,
      from,
      //是否通过push触发
      true,
      //不可能是第一次导航,因为最开始的定位是不能通过手动导航到的
      false
    )
  }

  //跳转到指定的路由
  return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
    .catch((error: NavigationFailure | NavigationRedirectError) =>
      isNavigationFailure(error)
        ? isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
        ? error: markAsReady(error)
        : triggerError(error, toLocation, from)
    )
    .then((failure: NavigationFailure | NavigationRedirectError | void) => {
      //处理跳转失败的情况, 这里的具体失败情况处理可以参照文档:
      //https://router.vuejs.org/zh/guide/advanced/navigation-failures.html
      if (failure) {
        if (
          isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
        ) {
          if (
            __DEV__ &&
            // 重定向到已经到达的相同位置
            isSameRouteLocation(
              stringifyQuery,
              resolve(failure.to),
              toLocation
            ) &&
            // 重复执行,如果30次后依然会进入这个判断,就报错
            redirectedFrom &&
            (redirectedFrom._count = 
            redirectedFrom._count ? redirectedFrom._count + 1 : 1) > 30
          ) {
            warn(
              `Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This might break in production if not fixed.`
            )
            return Promise.reject(
              new Error('Infinite redirect in navigation guard')
            )
          }

          //如果跳转失败就继续递归调用,并且允许重定向替换记录
          return pushWithRedirect(
            assign(
              {replace},
              locationAsObject(failure.to),
              {
                state:
                  typeof failure.to === 'object'
                    ? assign({}, data, failure.to.state)
                    : data,
                force,
              }
            ),
            redirectedFrom || toLocation
          )
        }
      } else {
        // 如果这个失败是不能继续递归的, 就触发路由守卫
        failure = finalizeNavigation(
          toLocation as RouteLocationNormalizedLoaded,
          from,
          true,
          replace,
          data
        )
      }
      triggerAfterEach(
        toLocation as RouteLocationNormalizedLoaded,
        from,
        failure
      )
      return failure
    })
}

resolve方法:

function resolve(
  rawLocation: Readonly<RouteLocationRaw>,
  currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string } {

  currentLocation = assign({}, currentLocation || currentRoute.value)
  
  //如果传入的是字符串,如: router.push('/login')
  if (typeof rawLocation === 'string') {
    // 该对象包含了标准化后的 `fullpath` 和 `path` 属性,以及其他属性(如 `hash`、`query`)。
    const locationNormalized = parseURL(
      parseQuery,
      rawLocation,
      currentLocation.path
    )
    
    //字符串和routes匹配的结果
    const matchedRoute = matcher.resolve(
      { path: locationNormalized.path },
      currentLocation
    )

    //routerHistory.createHref会删除#之前的任意字符
    const href = routerHistory.createHref(locationNormalized.fullPath)
    if (__DEV__) {
    //如果开发环境下的location是//开头,就报警告
      if (href.startsWith('//'))
        warn(
          `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
        )
      else if (!matchedRoute.matched.length) {
      //没匹配到相对应的路由
        warn(`No match found for location with path "${rawLocation}"`)
      }
    }

    // 
    return assign(locationNormalized, matchedRoute, {
      params: decodeParams(matchedRoute.params),
      hash: decode(locationNormalized.hash),
      redirectedFrom: undefined,
      href,
    })
  }

  let matcherLocation: MatcherLocationRaw

  //如果传入的是一个对象,并且有path属性
  if ('path' in rawLocation) {
    if (
      __DEV__ &&
      'params' in rawLocation &&
      !('name' in rawLocation) &&
      // @ts-expect-error: the type is never
      Object.keys(rawLocation.params).length
    ) {
    //如果有params, 建议用name匹配
      warn(
        `Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`
      )
    }
    //更新
    matcherLocation = assign({}, rawLocation, {
      path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
    })
  } else {

    //删除无效的params
    const targetParams = assign({}, rawLocation.params)
    for (const key in targetParams) {
      if (targetParams[key] == null) {
        delete targetParams[key]
      }
    }
    
    //对params编码
    matcherLocation = assign({}, rawLocation, {
      params: encodeParams(rawLocation.params),
    })
    
    
    // 对当前location的params解码
    currentLocation.params = encodeParams(currentLocation.params)
  }

  //和routes匹配
  const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
  const hash = rawLocation.hash || ''

  if (__DEV__ && hash && !hash.startsWith('#')) {
    warn(
      `A `hash` should always start with the character "#". Replace "${hash}" with "#${hash}".`
    )
  }

  //下面的操作和上面基本类似
  matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))

  const fullPath = stringifyURL(
    stringifyQuery,
    assign({}, rawLocation, {
      hash: encodeHash(hash),
      path: matchedRoute.path,
    })
  )

  const href = routerHistory.createHref(fullPath)
  if (__DEV__) {
    if (href.startsWith('//')) {
      warn(
        `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
      )
    } else if (!matchedRoute.matched.length) {
      warn(
        `No match found for location with path "${
          'path' in rawLocation ? rawLocation.path : rawLocation
        }"`
      )
    }
  }

  return assign(
    {
    //返回完整的路径
      fullPath,
      hash,
      query:
        //stringifyQuery: routes里的query,
        //originalStringifyQuery: url拼接的query
        //normalizeQuery:将query中数组的每一项或其他值转换为字符串
        
        //如果query中不存在对象,则直接标准化处理,否则不处理直接返回
        stringifyQuery === originalStringifyQuery
          ? normalizeQuery(rawLocation.query)
          : ((rawLocation.query || {}) as LocationQuery),
    },
    matchedRoute,
    {
      redirectedFrom: undefined,
      href,
    }
  )
}

handleScroll方法:处理滚动行为

参照文档:router.vuejs.org/zh/guide/ad…

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

  return nextTick()
    .then(() => scrollBehavior(to, from, scrollPosition))
    .then(position => position && scrollToPosition(position))
    .catch(err => triggerError(err, to, from))
}

navigate 方法: 跳转的方法

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
  let guards: Lazy<any>[]

//通过 from.matched, to.matched 两个数组的操作获取差异的特征
  const [leavingRecords, updatingRecords, enteringRecords] =
    extractChangingRecords(to, from)

  // 提取组件内的·beforeRouteLeave·钩子函数
  guards = extractComponentsGuards(
    leavingRecords.reverse(),
    'beforeRouteLeave',
    to,
    from
  )

  for (const record of leavingRecords) {
    record.leaveGuards.forEach(guard => {
      guards.push(guardToPromiseFn(guard, to, from))
    })
  }

  //如果有新的导航触发,那么取消并跳过现在的路由守卫,可能为null
  const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
    null,
    to,
    from
  )

  guards.push(canceledNavigationCheck)

  // 按顺序执行所有的beforeRouteLeave(promise)
  return (
    runGuardQueue(guards)
      .then(() => {
        // check global guards beforeEach
        guards = []
        for (const guard of beforeGuards.list()) {
          guards.push(guardToPromiseFn(guard, to, from))
        }
        guards.push(canceledNavigationCheck)

        return runGuardQueue(guards)
      })
      .then(() => {
        //收集并执行 ·beforeRouteUpdate· 的钩子(顺序执行)
        // check in components beforeRouteUpdate
        guards = extractComponentsGuards(
          updatingRecords,
          'beforeRouteUpdate',
          to,
          from
        )

        for (const record of updatingRecords) {
          record.updateGuards.forEach(guard => {
            guards.push(guardToPromiseFn(guard, to, from))
          })
        }
        guards.push(canceledNavigationCheck)

        // run the queue of per route beforeEnter guards
        return runGuardQueue(guards)
      })
      .then(() => {
        // 收集并顺序执行beforeEnter钩子
        // 在进入特定于此记录的守卫之前。如果记录有`重定向`属性,则beforeEnter无效
        guards = []
        for (const record of to.matched) {
        //如果有循环跳转则不重复触发
          if (record.beforeEnter && !from.matched.includes(record)) {
            if (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)

        return runGuardQueue(guards)
      })
      .then(() => {
        to.matched.forEach(record => (record.enterCallbacks = {}))

        // 收集组件内的beforeRouteEnter钩子并顺序执行
        guards = extractComponentsGuards(
          enteringRecords,
          'beforeRouteEnter',
          to,
          from
        )
        guards.push(canceledNavigationCheck)

        return runGuardQueue(guards)
      })
      .then(() => {
        //收集全局的beforeResolve钩子(导航即将被解析之前执行的导航守卫)
        guards = []
        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)
      )
  )
}

这个方法在源码中这里的注释应该是有问题的😄

image.png

3.总结

  1. 首先通过参数找到对应的route
  2. 判断这个路由是否有 重定向 属性,如果有,则继续递归调用
  3. 判断即将前往的路由和当前路由是否相同,如果相同,则记录下来,并且触发滚动行为(是否滚动在这个行为中进行详细的判断处理)
  4. 最后就是跳转操作,所有的钩子函数会按照顺序触发,可以在官网了解到更详细的组件激活的流程。这里的具体操作在源码的 navigationGuards.ts