源码阅读--vue-router-next核心源码大纲梳理

前言

  • 本文仅做大纲整理,方便日后回顾。所以很少贴代码(贴上太多代码看着乱遭)。
  • 本次阅读源码原因:项目需要多窗口

一、源码大纲

名词解释

  • 当前路由信息(currentRouter):当前匹配到的路由–this.$route
  • 路由实例(router):this.$router

第一部分:创建

  • 入口
  // src/router.ts
  /**
 * 创建一个路由实例,且包含install注册方法
 * 参数options中必须包含routes数组与history管理实例
 * @param options - {@link RouterOptions}
 * @return router
 */
  export function createRouter(options: RouterOptions): Router {

 }
  • 创建做了这些事:
  1. 分析路由树配置信息routes,得到matcher:储存路由匹配规则,负责匹配path工作。
  2. 保存历史管理容器。
  3. 创建路由守卫队列;当前路由信息容器(reactive响应式,方便viewRouter组件监听)
  4. 创键操作路由方法,主要有:新增、删除、重定向(pushWithRedirect)、事件处理(事件注册、navigate、triggerAfterEach)

第二部分:注册到vue(vue.use) src/router.ts: install

1. 注册组件: RouterLink、RouterView
2. 提供外部访问变量:currentRoute =>$route; router实例 => app.config.globalProperties.$router
3. 调用push方法初始化页面
4. 将路由实例(router)、当前路由信息(currentRouter)注入到app
5. 包装app.unmount方法,触发时销毁自身

第三部分:使用

1. 导航方式:routerLink、push、replace
2. 导航触发后调用:重定向入口(pushWithRedirect)
3. 渲染组件:routerView,
       监听当前路由信息
       渲染组件(组件搬运工):匹配路由,将得到的组件返回

二、执行路由变更

1.起点

  • 该方法为处理路由变更的入口。一般由push和repleace调用
  // /src/router.ts
  function pushWithRedirect(
    to: RouteLocationRaw | RouteLocation,
    redirectedFrom?: RouteLocation
  ): Promise<NavigationFailure | void | undefined> {
    console.log('重定向路由');
    // 1. 将to标准化成路由描述对象,更新pendingLocation
    const targetLocation: RouteLocation = (pendingLocation = resolve(to))
    // 2. 当前路由信息转给from
    const from = currentRoute.value
    // 3. 获取路由配置信息(数据、?、更新当前路由)
    const data: HistoryState | undefined = (to as RouteLocationOptions).state
    const force: boolean | undefined = (to as RouteLocationOptions).force
    // to could be a string where `replace` is a function
    const replace = (to as RouteLocationOptions).replace === true

    // 4. 是否需要重定向
    const shouldRedirect = handleRedirectRecord(targetLocation)

    // 需要重定向重新调用此方法
    if (shouldRedirect)
      return pushWithRedirect(
        assign(shouldRedirect, { state: data, force, replace }),
        // keep original redirectedFrom if it exists
        redirectedFrom || targetLocation
      )

    // if it was a redirect we already called `pushWithRedirect` above
    const toLocation = targetLocation as RouteLocationNormalized

    // 5. 记录重定向来源(如果有的话)
    toLocation.redirectedFrom = redirectedFrom
    let failure: NavigationFailure | void | undefined

    // 6. from与to是否相同
    if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
      failure = createRouterError<NavigationFailure>(
        ErrorTypes.NAVIGATION_DUPLICATED,
        { to: toLocation, from }
      )
      // trigger scroll to allow scrolling to the same anchor
      handleScroll(
        from,
        from,
        // this is a push, the only way for it to be triggered from a
        // history.listen is with a redirect, which makes it become a push
        true,
        // This cannot be the first navigation because the initial location
        // cannot be manually navigated to
        false
      )
    }

    // 7. 如果未失败,正常调用navigate进行导航
    return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
      .catch((error: NavigationFailure | NavigationRedirectError) =>
        isNavigationFailure(error)
          ? error
          : // reject any unknown error
            triggerError(error)
      )
      .then((failure: NavigationFailure | NavigationRedirectError | void) => {
        if (failure) {
          if (
            isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
          ) {
            if (
              __DEV__ &&
              // we are redirecting to the same location we were already at
              isSameRouteLocation(
                stringifyQuery,
                resolve(failure.to),
                toLocation
              ) &&
              // and we have done it a couple of times
              redirectedFrom &&
              // @ts-ignore
              (redirectedFrom._count = redirectedFrom._count
                ? // @ts-ignore
                  redirectedFrom._count + 1
                : 1) > 10
            ) {
              warn(
                `Detected an infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This will break in production if not fixed.`
              )
              return Promise.reject(
                new Error('Infinite redirect in navigation guard')
              )
            }

            return pushWithRedirect(
              // keep options
              assign(locationAsObject(failure.to), {
                state: data,
                force,
                replace,
              }),
              // preserve the original redirectedFrom if any
              redirectedFrom || toLocation
            )
          }
        } else {
          // 8. 开始执行导航
          // if we fail we don't finalize the navigation
          failure = finalizeNavigation(
            toLocation as RouteLocationNormalizedLoaded,
            from,
            true,
            replace,
            data
          )
        }
        // 9. 调用After事件
        triggerAfterEach(
          toLocation as RouteLocationNormalizedLoaded,
          from,
          failure
        )
        return failure
      })
  }

2.navigate 处理事件

  • => 局部离开守卫(beforeRouteLeave)
    => leavingRecords(匹配到的from数组)组件中的守卫
    => leavingRecords(匹配到的from数组)路由配置中的守卫
    => 检查from、to

  • => 全局离开守卫:beforeGuards

  • => 局部路由更新(beforeRouteUpdate)

  • => 局部(路由配置)的beforeEnter
    => to.matched(匹配到的新路由数组)路由配置中的守卫
    => 检查from、to

  • => enterCallbacks
    => enteringRecords(匹配到的to数组)组件中的守卫
    => 检查from、to

  • => 全局进入新路由守卫beforeResolveGuards
    => ebeforeResolveGuards中的守卫
    => 检查from、to

    	(全局完成导航守卫,下一步进行)
    
  • => finalizeNavigation(执行导航)
    => history操作:添加/覆盖
    => 更新当前路由信息
    => wait Render(此时触发routerView监听,具体下面会说到)
    => 调用scrollBehavior

  • => triggerAfterEach 执行导航后事件

注:处理事件是同步执行,利用promise + Array.reduce实现的

三、routerView组件

routerView主要职责=>
		=> 创建路由(setup)
			=> 1. 获取渲染所需路由信息
			=> 2. 向下传递路由信息,使子viewRouter找到渲染的组件:1)已匹配路由;2)下层路由层数
			=> 3. 监听路由变化
			=> 4. 返回渲染函数
				=> 1. 得到匹配组件
				=> 2. 返回组件
				=> 注:如使用插槽返回插槽所需数据;未使用插槽将根据注册组件进行渲染
		=> 各层级如何匹配并选择自己的路由,matched 根据自身深度寻找组件
// src\RouterView.ts
export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterView',
  props: {
    name: {
      type: String as PropType<string>,
      default: 'default',
    },
    route: Object as PropType<RouteLocationNormalizedLoaded>,
  },
  setup(props, { attrs, slots }) {
    __DEV__ && warnDeprecatedUsage()

    console.log('router-view创建')

    /** 1. 获取渲染所需路由信息 */
    const injectedRoute = inject(routeLocationKey)!
    const depth = inject(viewDepthKey, 0)
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => {
        return (props.route || injectedRoute).matched[depth]
      }
    )

    /** 2. 向下传递路由信息,使子viewRouter找到渲染的组件:1)已匹配路由;2)下层路由层数 */
    provide(viewDepthKey, depth + 1)
    provide(matchedRouteKey, matchedRouteRef)

    const viewRef = ref<ComponentPublicInstance>()

    // watch at the same time the component instance, the route record we are
    // rendering, and the name
    /** 3. 监听路由变化 */
    watch(
      () => [viewRef.value, matchedRouteRef.value, props.name] as const,
      ([instance, to, name], [oldInstance, from, oldName]) => {
        // copy reused instances
        if (to) {
          // this will update the instance for new instances as well as reused
          // instances when navigating to a new route
          to.instances[name] = instance
          // the component instance is reused for a different route or name so
          // we copy any saved update or leave guards
          if (from && from !== to && instance && instance === oldInstance) {
            to.leaveGuards = from.leaveGuards
            to.updateGuards = from.updateGuards
          }
        }

        // trigger beforeRouteEnter next callbacks
        if (
          instance &&
          to &&
          // if there is no instance but to and from are the same this might be
          // the first visit
          (!from || !isSameRouteRecord(to, from) || !oldInstance)
        ) {
          ;(to.enterCallbacks[name] || []).forEach(callback =>
            callback(instance)
          )
        }
      },
      { flush: 'post' }
    )

    return () => {
      console.log('创建一个routerView实例');
      // 1. 得到匹配组件
      const route = props.route || injectedRoute
      const matchedRoute = matchedRouteRef.value
      const ViewComponent = matchedRoute && matchedRoute.components[props.name]
      // we need the value at the time we render because when we unmount, we
      // navigated to a different location so the value is different
      const currentName = props.name

      // 未匹配到:有插槽加入变量;没有返回空
      if (!ViewComponent) {
        if(slots.default) {
          console.log(slots.default.toString());
          let r = slots.default({ Component: ViewComponent, route })
          return r;
        }

        return null
      }

      // 匹配到
      // props from route configuration
      const routePropsOption = matchedRoute!.props[props.name]
      const routeProps = routePropsOption
        ? routePropsOption === true
          ? route.params
          : typeof routePropsOption === 'function'
          ? routePropsOption(route)
          : routePropsOption
        : null

      const onVnodeUnmounted: VNodeProps['onVnodeUnmounted'] = vnode => {
        // remove the instance reference to prevent leak
        if (vnode.component!.isUnmounted) {
          matchedRoute!.instances[currentName] = null
        }
      }

      // 创建vnode
      const component = h(
        ViewComponent,
        assign({}, routeProps, attrs, {
          onVnodeUnmounted,
          ref: viewRef,
        })
      )

      // pass the vnode to the slot as a prop.
      // h and <component :is="..."> both accept vnodes
      // 返回(规则同上,需要判断是否有插槽)
      let r = slots.default
        ? slots.default({ Component: component, route })
        : component
      return r
    }
  },
})


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值