Vue Router 源码分析(一)-- createRouter

615 阅读25分钟

Vue Router源码学习(一)-- createRouter

断更了许久,想不到吧,我又回来了。好不容易获得一段离职后的喘息时日,暂时不用过早出晚归的骡马生活,空气都变得香甜了。

言归正传,Vue源码学习先按个暂停键,要记录的东西太多。既然如此,那为什么不先解决Router这种更轻松的问题呢?想到了就开干!下面开始汇报今天的学习成果!

Router作为Vue Router的核心内容,很值得好好研究一番。回顾了下在项目里的使用,是从函数createRouter开始的吧。既如此,那源码学习也从这儿开始咯。虽说高手都喜欢按部就班从index.js开始,但是天才嘛,自然是想到哪儿就看到哪儿。为此很有必要先知道两个重要的类型接口:RouterOptionsRouter

1. RouterOptions

RouterOptionscreateRouter初始化Router实例时传入的选项参数。

  • history -- 路由模式(必需):可用createWebHistory来创建history模式,但是要求服务端有相应的配置来解决刷新时的路由问题;也可以使用createWebHashHistory来创建hash模式,无需服务端进行额外的配置,但是搜索引擎不会处理路由对应的页面,不利于SEO;

  • routes -- 初始路由列表(必需);

  • scrollBehavior -- 控制切换页面时滚动行为的函数(可选),可返回一个Promise来延迟滚动;

    • 入参:tofromsavedPosition
  • parseQuery -- 自定义将字符串query转化为对象的方式(可选),一般用不上;

  • stringifyQuery -- 自定义讲query对象转化为字符串的方法(可选),一般用不上;

  • linkActiveClass -- 活跃路由对应的link的类名,可用于定制活跃路由对应的链接的样式,默认为router-link-exact-active

  • linkExactActiveClass -- 当前精准活跃路由的类名,可用于定制当前精准活跃路由对应的链接的样式,默认为router-link-inactive

linkActiveClasslinkExactActiveClass 的区别:

  • linkActiveClass :符合路由匹配的规则的为活跃路由,例如某个路由的路径为/active,那么当页面处于/active/123时,该路由也能被匹配到,因此该路由也是活跃路由。
  • linkExactActiveClass :精准匹配到的活跃路由,当页面处于/active/123时,仅有路由/active/123能精准匹配。

这里直接把github里的源码复制过来得了,注释给的太清晰了。

/**
 * Options to initialize a {@link Router} instance.
 * 初始化Router实例时传入的选项
 */
export interface RouterOptions extends PathParserOptions {
  /**
   * History implementation used by the router. Most web applications should use
   * `createWebHistory` but it requires the server to be properly configured.
   * You can also use a _hash_ based history with `createWebHashHistory` that
   * does not require any configuration on the server but isn't handled at all
   * by search engines and does poorly on SEO.
   *
   * @example
   * ```js
   * createRouter({
   *   history: createWebHistory(),
   *   // other options...
   * })
   * ```
   */
  // 路由模式:可用createWebHistory来创建history模式,但是要求服务端有相应的配置来解决刷新时的路由问题;
  // 也可以使用createWebHashHistory来创建hash模式,无需服务端进行额外的配置,但是搜索引擎不会处理路由对应的页面,不利于SEO
  history: RouterHistory
  /**
   * Initial list of routes that should be added to the router.
   */
  routes: Readonly<RouteRecordRaw[]>
  /**
   * Function to control scrolling when navigating between pages. Can return a
   * Promise to delay scrolling. Check {@link ScrollBehavior}.
   *
   * @example
   * ```js
   * function scrollBehavior(to, from, savedPosition) {
   *   // `to` and `from` are both route locations
   *   // `savedPosition` can be null if there isn't one
   * }
   * ```
   */
  scrollBehavior?: RouterScrollBehavior
  /**
   * Custom implementation to parse a query. See its counterpart,
   * {@link RouterOptions.stringifyQuery}.
   *
   * @example
   * Let's say you want to use the [qs package](https://github.com/ljharb/qs)
   * to parse queries, you can provide both `parseQuery` and `stringifyQuery`:
   * ```js
   * import qs from 'qs'
   *
   * createRouter({
   *   // other options...
   *   parseQuery: qs.parse,
   *   stringifyQuery: qs.stringify,
   * })
   * ```
   */
  parseQuery?: typeof originalParseQuery
  /**
   * Custom implementation to stringify a query object. Should not prepend a leading `?`.
   * {@link RouterOptions.parseQuery | parseQuery} counterpart to handle query parsing.
   */
  stringifyQuery?: typeof originalStringifyQuery
  /**
   * Default class applied to active {@link RouterLink}. If none is provided,
   * `router-link-active` will be applied.
   */
  linkActiveClass?: string
  /**
   * Default class applied to exact active {@link RouterLink}. If none is provided,
   * `router-link-exact-active` will be applied.
   */
  linkExactActiveClass?: string
  /**
   * Default class applied to non-active {@link RouterLink}. If none is provided,
   * `router-link-inactive` will be applied.
   */
  // linkInactiveClass?: string
}

2. Router

天天都在用的Router,应该没人会感到陌生,但应该很多人都是“夹生”。来过一遍Router的实例上有哪些属性和方法:

  • currentRoute -- 当前路由对象;

  • options -- 由createRouter创建初始路由时的选项;

  • 配合微前端使用

  • listening -- 布尔值,是否监听history事件,这是个为微前端设计的api,非微前端项目用不上;

  • 路由的增删查

  • addRoute -- 路由实例可用此方法添加新的路由,多用于从服务端拉取准许的路由信息后再添加到router中,有俩重载签名,接收的参数不一样:

    1. (parentName: RouteRecordName, route: RouteRecordRaw) => (() => void),入参为parantNameroute,将route作为parentName的子路由来添加;
    2. (route: RouteRecordRaw) => (() => void),入参为route,直接添加到根路由下;
  • removeRoute -- 入参为name,移除对应的路由;

  • hasRoute -- 入参为name,判断路由实例中是否存在对应的路由;

  • getRoutes -- 返回所有的路由数据(数组);

  • resolve -- 解析路由,入参为tocurrentLocation(可选),生成新的路由位置,其中多出来一个href属性,包含了已存在的base,即存在有效的base时,href可以形成完整的链接。

  • 路由切换的几个方法

  • push -- (to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>,不多解释,push方法大家都在用,只需要注意返回的是Promise

  • replace -- (to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>,同push,两者的区别应该没人不知道吧;

  • back -- 返回历史路由栈中的上一个路由;

  • forward -- 切换到历史路由栈中的下一个路由;

  • go -- 入参delta,数值类型,切换为历史路由栈中指定跨度的路由;

  • 全局守卫钩子

  • beforeEach -- 入参guard函数,类型为NavigationGuardWithThis<undefined>,此时新页面的Vue实例尚未创建,在该gurad中的thisundefined;此时guard接收三个参数:tofromnext

  • beforeResolve:入参guard函数,类型为NavigationGuardWithThis<undefined>,此时新页面的Vue实例尚未创建,在该gurad中的thisundefined;此时guard接收三个参数:tofromnext

  • afterEach:入参guard函数,类型为NavigationHookAfter。此时guard接收三个参数:tofromfailure(可选);

  • 错误处理

  • onError -- 入参为handler错误处理函数

  • 路由实例是否已准备完毕

  • isReady -- 得到一个Promise

  • 安装路由

  • install -- 提供给app.use调用

同样copy一下源码贴在这儿:

/**
 * Router instance.
 */
export interface Router {
  /**
   * @internal
   */
  // readonly history: RouterHistory
  /**
   * Current {@link RouteLocationNormalized}
   */
  readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
  /**
   * Original options object passed to create the Router
   */
  readonly options: RouterOptions
​
  /**
   * Allows turning off the listening of history events. This is a low level api for micro-frontends.
   */
  listening: boolean
​
  /**
   * Add a new {@link RouteRecordRaw | route record} as the child of an existing route.
   *
   * @param parentName - Parent Route Record where `route` should be appended at
   * @param route - Route Record to add
   */
  addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
  /**
   * Add a new {@link RouteRecordRaw | route record} to the router.
   *
   * @param route - Route Record to add
   */
  addRoute(route: RouteRecordRaw): () => void
  /**
   * Remove an existing route by its name.
   *
   * @param name - Name of the route to remove
   */
  removeRoute(name: RouteRecordName): void
  /**
   * Checks if a route with a given name exists
   *
   * @param name - Name of the route to check
   */
  hasRoute(name: RouteRecordName): boolean
  /**
   * Get a full list of all the {@link RouteRecord | route records}.
   */
  getRoutes(): RouteRecord[]
​
  /**
   * Returns the {@link RouteLocation | normalized version} of a
   * {@link RouteLocationRaw | route location}. Also includes an `href` property
   * that includes any existing `base`. By default, the `currentLocation` used is
   * `router.currentRoute` and should only be overridden in advanced use cases.
   *
   * @param to - Raw route location to resolve
   * @param currentLocation - Optional current location to resolve against
   */
  resolve(
    to: RouteLocationRaw,
    currentLocation?: RouteLocationNormalizedLoaded
  ): RouteLocation & { href: string }
​
  /**
   * Programmatically navigate to a new URL by pushing an entry in the history
   * stack.
   *
   * @param to - Route location to navigate to
   */
  push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
​
  /**
   * Programmatically navigate to a new URL by replacing the current entry in
   * the history stack.
   *
   * @param to - Route location to navigate to
   */
  replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
​
  /**
   * Go back in history if possible by calling `history.back()`. Equivalent to
   * `router.go(-1)`.
   */
  back(): ReturnType<Router['go']>
  /**
   * Go forward in history if possible by calling `history.forward()`.
   * Equivalent to `router.go(1)`.
   */
  forward(): ReturnType<Router['go']>
  /**
   * Allows you to move forward or backward through the history. Calls
   * `history.go()`.
   *
   * @param delta - The position in the history to which you want to move,
   * relative to the current page
   */
  go(delta: number): void
​
  /**
   * Add a navigation guard that executes before any navigation. Returns a
   * function that removes the registered guard.
   *
   * @param guard - navigation guard to add
   */
  beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
  /**
   * Add a navigation guard that executes before navigation is about to be
   * resolved. At this state all component have been fetched and other
   * navigation guards have been successful. Returns a function that removes the
   * registered guard.
   *
   * @param guard - navigation guard to add
   * @returns a function that removes the registered guard
   *
   * @example
   * ```js
   * router.beforeResolve(to => {
   *   if (to.meta.requiresAuth && !isAuthenticated) return false
   * })
   * ```
   *
   */
  beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
​
  /**
   * Add a navigation hook that is executed after every navigation. Returns a
   * function that removes the registered hook.
   *
   * @param guard - navigation hook to add
   * @returns a function that removes the registered hook
   *
   * @example
   * ```js
   * router.afterEach((to, from, failure) => {
   *   if (isNavigationFailure(failure)) {
   *     console.log('failed navigation', failure)
   *   }
   * })
   * ```
   */
  afterEach(guard: NavigationHookAfter): () => void
​
  /**
   * Adds an error handler that is called every time a non caught error happens
   * during navigation. This includes errors thrown synchronously and
   * asynchronously, errors returned or passed to `next` in any navigation
   * guard, and errors occurred when trying to resolve an async component that
   * is required to render a route.
   *
   * @param handler - error handler to register
   */
  onError(handler: _ErrorListener): () => void
  /**
   * Returns a Promise that resolves when the router has completed the initial
   * navigation, which means it has resolved all async enter hooks and async
   * components that are associated with the initial route. If the initial
   * navigation already happened, the promise resolves immediately.
   *
   * This is useful in server-side rendering to ensure consistent output on both
   * the server and the client. Note that on server side, you need to manually
   * push the initial location while on client side, the router automatically
   * picks it up from the URL.
   */
  isReady(): Promise<void>
​
  /**
   * Called automatically by `app.use(router)`. Should not be called manually by
   * the user. This will trigger the initial navigation when on client side.
   *
   * @internal
   * @param app - Application that uses the router
   */
  install(app: App): void
}

3. createRouter

上正菜咯。这个函数加上注释接近千行,或许要分段拆解了。函数的具体类型:(options: RouterOptions) => Router。接下来逐段分析函数体:

  • 创建matcher
  • 从选项参数中取出parseQuerystringifyQueryrouterHistory;前两者有默认值,而后者是必需的选项
  • 如果没有提供routerHistory选项,则在DEV环境下就会报错告知开发者
// 根据options创建matcher,稍后看一下createRouterMatcher是何许人也
const matcher = createRouterMatcher(options.routes, options)
// 得到解析/字符串化query的方式,以及路由模式
const parseQuery = options.parseQuery || originalParseQuery
const stringifyQuery = options.stringifyQuery || originalStringifyQuery
const routerHistory = options.history
if (__DEV__ && !routerHistory)
  throw new Error(
    'Provide the "history" option when calling "createRouter()":' +
      ' https://next.router.vuejs.org/api/#history.'
  )
  • 创建全局守卫
  • 创建当前路由对象(使用shallowRef)
  • 初始化pending路由位置
  • 设置滚动恢复行为:如果是在浏览器环境且选项参数中给出了滚动行为,并且路由模式history中有scrollRestoration属性,则将history.scrollRestoratio设置为手动manual
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
)
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
// leave the scrollRestoration if no scrollBehavior is provided
if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
  history.scrollRestoration = 'manual'
}

添加几个辅助函数:

  • normalizeParams -- 规范化参数
  • encodeParams -- 编码参数
  • decodeParams -- 解码参数
const normalizeParams = applyToParams.bind(
    null,
    paramValue => '' + paramValue
)
const encodeParams = applyToParams.bind(null, encodeParam)
const decodeParams: (params: RouteParams | undefined) => RouteParams =
    // @ts-expect-error: intentionally avoid the type check
    applyToParams.bind(null, decode)

路由的增删查:

  • addRoute -- 还记得addRoute有两个重载签名的嘛?因此这里的实现签名,第一个参数名为parentOrRoute,兼顾RouteRecordName | RouteRecordRaw,而第二个参数为可选参数,仅当parentOrRouteRouteRecordName时才用的上。这里其实可以略微优化一下,调换一下参数的位置,使第一个参数为route,第二个参数叫RouteRecordName(可选),这样就不用判断parentOrRouteTS类型了

    • 判断第一个参数:

      1. 当第一个参数为路由名称时,使用 matcher.getRecordMatcher 根据路由名称获取路由对象(如果找不到该路由名称对应的路由对象,则会在开发环境下报错提示),此时要添加的路由为第二个参数 route;
      2. 当第一个参数不是路由名称时,则应为路由对象,要添加的路由即为该参数;
      3. 调用matcher.addRoute,将路由添加到对应的父级路由或根路由下
  • removeRoute -- 根据参数name查找到对应的路由对象,再调用matcher.removeRoute将其移除;如果没找到该路由对象,会在开发环境下发出提醒;

  • getRoutes -- 调用matcher.getRoutes获得所有的routeMatcher,再通过map方法得到对应的record,即所有的路由信息。

  • hasRoute -- 根据路由名称,调用matcher.getRecordMatcher查找是否有对应的路由对象;

function addRoute(
    parentOrRoute: RouteRecordName | RouteRecordRaw,
    route?: RouteRecordRaw
) {
    let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
    let record: RouteRecordRaw
    if (isRouteName(parentOrRoute)) {
        // 当第一个参数为路由名称时,使用 matcher.getRecordMatcher 根据路由名称获取路由对象
        parent = matcher.getRecordMatcher(parentOrRoute)
        // 如果找不到该路由名称对应的路由对象,则会在开发环境下报错提示
        if (__DEV__ && !parent) {
            warn(
                `Parent route "${String(
                    parentOrRoute
                )}" not found when adding child route`,
                route
            )
        }
        // 此时要添加的路由为第二个参数 route
        record = route!
    } else {
        // 当第一个参数不是路由名称时,则应为路由对象,要添加的路由即为此参数
        record = parentOrRoute
    }
    // 通过 matcher.addRoute 将路由添加到对应的父级路由下,当 parent 为空时,则添加到根路由下
    return matcher.addRoute(record, parent)
}
​
function removeRoute(name: RouteRecordName) {
    // 查找路由对象
    const recordMatcher = matcher.getRecordMatcher(name)
    if (recordMatcher) {
        // 找到了则移除
        matcher.removeRoute(recordMatcher)
    } else if (__DEV__) {
        // 找不到则会在开发环境下进行提示
        warn(`Cannot remove non-existent route "${String(name)}"`)
    }
}
​
function getRoutes() {
    return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
​
function hasRoute(name: RouteRecordName): boolean {
    return !!matcher.getRecordMatcher(name)
}

看到这儿,我的心里充满了问号。matcher是个啥东东?怎么增删查哪儿都有它?createRouter函数体第一句就用到它,一行之下,千行之上,足以显示出其身份之尊贵。略微翻了下代码,关于matcher的有个单独的文件夹,四五个新文件在那儿盯着我,吓得我赶紧关了电脑。天晓得是不是一个文件里又有几千行的蚂蚁等着我呢。毕加思索之后,我决定过两天再另开一篇。现在还是继续createRouter的内容吧,虽然这个也不容易,但是来都来了,总不至于半途而废咯。

  • resolve -- 通过参数rawLocation以及当前路由位置currentLocation来生成一个新的路由位置,还有个私生子href,啧啧。

    • 拷贝一份当前路由位置信息currentLocation

    • 如果参数rawLocation是个字符串:

      • 则根据rawLocationcurrentLocation来得到规范化的路由位置locationNormalized
      • 根据locationNormalized调用matcher.resolve来得到对应的路由信息;
      • 根据locationNormalized.fullPath调用routerHistory.createHref来获得href
      • locationNormalizedmatchedRoute等对象合并并返回合并的结果;
    • 如果参数rawLocation不是字符串:

      • 处理params并获取matchedRoute
      • rawLocation中取出hash属性,默认为空字符串;
      • 生成fullPath字符串;
      • 根据fullPath字符串得到href
      • 最后依然是合并各个对象并返回结果。
function resolve(
    rawLocation: Readonly<RouteLocationRaw>,
    currentLocation?: RouteLocationNormalizedLoaded
): RouteLocation & { href: string } {
    // const objectLocation = routerLocationAsObject(rawLocation)
    // we create a copy to modify it later
    currentLocation = assign({}, currentLocation || currentRoute.value)
    if (typeof rawLocation === 'string') {
        const locationNormalized = parseURL(
            parseQuery,
            rawLocation,
            currentLocation.path
        )
        const matchedRoute = matcher.resolve(
            { path: locationNormalized.path },
            currentLocation
        )
​
        const href = routerHistory.createHref(locationNormalized.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 "${rawLocation}"`)
            }
        }
​
        // locationNormalized is always a new object
        return assign(locationNormalized, matchedRoute, {
            params: decodeParams(matchedRoute.params),
            hash: decode(locationNormalized.hash),
            redirectedFrom: undefined,
            href,
        })
    }
​
    // 当rawLocation不是字符串时,校验参数是否合规,不合规就会在开发环境下发出警告
    if (__DEV__ && !isRouteLocation(rawLocation)) {
        warn(
            `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
            rawLocation
        )
        rawLocation = {}
    }
​
    let matcherLocation: MatcherLocationRaw
​
    // path could be relative in object as well
    if (rawLocation.path != null) {
        if (
            __DEV__ &&
            'params' in rawLocation &&
            !('name' in rawLocation) &&
            // @ts-expect-error: the type is never
            Object.keys(rawLocation.params).length
        ) {
            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 {
        // remove any nullish param
        // 移除空的参数
        const targetParams = assign({}, rawLocation.params)
        for (const key in targetParams) {
            if (targetParams[key] == null) {
                delete targetParams[key]
            }
        }
        // pass encoded values to the matcher, so it can produce encoded path and fullPath
        matcherLocation = assign({}, rawLocation, {
            params: encodeParams(targetParams),
        })
        // current location params are decoded, we need to encode them in case the
        // matcher merges the params
        currentLocation.params = encodeParams(currentLocation.params)
    }
​
    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}".`
        )
    }
​
    // the matcher might have merged current location params, so
    // we need to run the decoding again
    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 "${
                    rawLocation.path != null ? rawLocation.path : rawLocation
                }"`
            )
        }
    }
​
    return assign(
        {
            fullPath,
            // keep the hash encoded so fullPath is effectively path + encodedQuery +
            // hash
            hash,
            query:
            // if the user is using a custom query lib like qs, we might have
            // nested objects, so we keep the query as is, meaning it can contain
            // numbers at `$route.query`, but at the point, the user will have to
            // use their own type anyway.
            // https://github.com/vuejs/router/issues/328#issuecomment-649481567
            stringifyQuery === originalStringifyQuery
                ? normalizeQuery(rawLocation.query)
                : ((rawLocation.query || {}) as LocationQuery),
        },
        matchedRoute,
        {
            redirectedFrom: undefined,
            href,
        }
    )
}
  • locationAsObject -- 将路由位置参数转化为对象并返回;
function locationAsObject(
    to: RouteLocationRaw | RouteLocationNormalized
): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
    return typeof to === 'string'
    ? parseURL(parseQuery, to, currentRoute.value.path)
    : assign({}, to)
}
  • checkCanceledNavigation -- 比对topendingLocation,如果不一致创建路由错误对象;
function checkCanceledNavigation(
    to: RouteLocationNormalized,
    from: RouteLocationNormalized
): NavigationFailure | void {
    if (pendingLocation !== to) {
        return createRouterError<NavigationFailure>(
            ErrorTypes.NAVIGATION_CANCELLED,
            {
                from,
                to,
            }
        )
    }
}

接下来是两个常用的导航方法:pushreplace。从代码上看,replace也是调用的push方法,只是参数中多了个配置replace = true。因此,合理推测调用push时手动设置replace属性为true(从pushWithRedirect的代码来看必须是true才有效),应该和直接调用replace效果一致。

function push(to: RouteLocationRaw) {
    return pushWithRedirect(to)
}
​
function replace(to: RouteLocationRaw) {
    return push(assign(locationAsObject(to), { replace: true }))
}

在进入pushWithRedirect 之前,最好先看看另一个函数,在pushWithRedirect中需要用到的handleRedirectRecord

  • handleRedirectRecord -- 用于判断本次导航是否要重定向,接收参数to。从to.matched中取出最后一个,即lastMatched;仅当lastMatched有效且其上存在有效的redirect值时,会处理需要定向到的新路由位置,否则函数得到undefiend

    • 根据lastMatched.redirect获得新的目标位置newTargetLocation
    • newTargetLocation为字符串(此时redirect也是字符串),则将其处理为对象;
    • 如果newTargetLocation中既没有有效的path由没有name属性,则会在开发环境发出警告;
    • 最后将新路由与queryhashparams属性合并并返回结果。
function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
    const lastMatched = to.matched[to.matched.length - 1]
    if (lastMatched && lastMatched.redirect) {
        const { redirect } = lastMatched
        let newTargetLocation =
        typeof redirect === 'function' ? redirect(to) : redirect
​
        // newTargetLocation转化为对象
        if (typeof newTargetLocation === 'string') {
            newTargetLocation =
            newTargetLocation.includes('?') || newTargetLocation.includes('#')
                ? (newTargetLocation = locationAsObject(newTargetLocation))
                : // force empty params
                { path: newTargetLocation }
            // @ts-expect-error: force empty params when a string is passed to let
            // the router parse them again
            newTargetLocation.params = {}
        }
​
        // 不规范的路由,开发环境警告
        if (
            __DEV__ &&
            newTargetLocation.path == null &&
            !('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,
                // avoid transferring params if the redirect has a path
                params: newTargetLocation.path != null ? {} : to.params,
            },
            newTargetLocation
        )
    }
}
  • pushWithRedirect -- push内部调用的方法,入参toredirectedFrom(可选)。

    • 跟腱炎参数to获取一些准备数据;

    • 调用上面的handleRedirectRecord来判断是否需要重定向,若需要重定向,则递归pushWithRedirect ;

    • 整理新的路由位置toLocation

    • 如若是新旧位置同一个路由位置,且不强制导航(!force),则产生路由错误,并滚动至此前的页面位置;

    • 异常处理;

    • 触发后置守卫钩子的调用triggerAfterEach,遍历调用afterGuards中的每个守卫;

      function triggerAfterEach(
          to: RouteLocationNormalizedLoaded,
          from: RouteLocationNormalizedLoaded,
          failure?: NavigationFailure | void
      ): void {
          // navigation is confirmed, call afterGuards
          // TODO: wrap with error handlers
          afterGuards
              .list()
              .forEach(guard => runWithContext(() => guard(to, from, failure)))
          }
      
      • runWithContext -- 使用app.runWithContext来调用传入的回调fn,或者直接调用fn
      function runWithContext<T>(fn: () => T): T {
          const app: App | undefined = installedApps.values().next().value
          // support Vue < 3.3
          return app && typeof app.runWithContext === 'function'
              ? app.runWithContext(fn)
              : fn()
          }
      

看到这里,心里的疑问又多了一重,既然后置守卫钩子在这里触发,那么Router实例上另外两个全局守卫是在什么时候触发的呢?组件内守卫又是如何维护及触发的呢?先埋个坑。

function pushWithRedirect(
    to: RouteLocationRaw | RouteLocation,
    redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
    const targetLocation: RouteLocation = (pendingLocation = resolve(to))
    const from = currentRoute.value
    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
​
    const shouldRedirect = handleRedirectRecord(targetLocation)
​
    if (shouldRedirect)
        return pushWithRedirect(
            assign(locationAsObject(shouldRedirect), {
                state:
                typeof shouldRedirect === 'object'
                    ? assign({}, data, 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
​
    toLocation.redirectedFrom = redirectedFrom
    let failure: NavigationFailure | void | undefined
​
    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
        )
    }
​
    // 失败则返回失败的Promise,否则导航至新的目标位置
    return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
        // 异常处理
        .catch((error: NavigationFailure | NavigationRedirectError) =>
            isNavigationFailure(error)
            ? // navigation redirects still mark the router as ready
                isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
                ? error
                : markAsReady(error) // also returns the error
            : // reject any unknown error
                triggerError(error, toLocation, from)
        )
        
        .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-expect-error: added only in dev
                        (redirectedFrom._count = redirectedFrom._count
                            ? // @ts-expect-error
                            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.\n Are you always returning a new location within a navigation guard? That would lead to this error. Only return when redirecting or aborting, that should fix this. This might break in production if not fixed.`
                        )
                        return Promise.reject(
                            new Error('Infinite redirect in navigation guard')
                        )
                    }
​
                    return pushWithRedirect(
                        // keep options
                        assign(
                            {
                                // preserve an existing replacement but allow the redirect to override it
                                replace,
                            },
                            locationAsObject(failure.to),
                            {
                                state:
                                    typeof failure.to === 'object'
                                    ? assign({}, data, failure.to.state)
                                    : data,
                                force,
                            }
                        ),
                        // preserve the original redirectedFrom if any
                        redirectedFrom || toLocation
                    )
                }
            } else {
                // if we fail we don't finalize the navigation
                failure = finalizeNavigation(
                    toLocation as RouteLocationNormalizedLoaded,
                    from,
                    true,
                    replace,
                    data
                )
            }
        triggerAfterEach(
            toLocation as RouteLocationNormalizedLoaded,
            from,
            failure
        )
        return failure
    })
}
  • checkCanceledNavigationAndReject -- 辅助函数,用于新的导航动作发生时通过抛出reject来拒绝并跳过所有的守卫钩子。
/**
 * Helper to reject and skip all navigation guards if a new navigation happened
 * @param to
 * @param from
 */
function checkCanceledNavigationAndReject(
  to: RouteLocationNormalized,
  from: RouteLocationNormalized
): Promise<void> {
  const error = checkCanceledNavigation(to, from)
  return error ? Promise.reject(error) : Promise.resolve()
}
  • navigate -- 虽然叫navigate,但不是实际导航动作发生的函数,而是用于依次触发各个路由守卫。入参为tofrom,提供给函数guardToPromiseFn,用于得到某些路由守卫,如beforeRouteLeave

    • 抽离出变化的路由并解构:leavingRecordsupdatingRecordsenteringRecords得到各组件;
    • 从所有的即将离开的组件中抽离出beforeRouteLeave守卫钩子,收集到guards队列中;
    • guards队列中加入canceledNavigationCheck,用以判断是否需要跳过所有的守卫钩子的执行;
    • 调用队列中的守卫(除了canceledNavigationCheck以外都是beforeRouteLeave);
    • 随后收集全局beforeEach守卫以及canceledNavigationCheck并调用;
    • 再是收集需要更新的组件内的beforeRouteUpdate守卫以及canceledNavigationCheck并调用;
    • 之后收集对应路由的beforeEnter守卫以及canceledNavigationCheck并调用(复用的视图不会再次触发beforeEnter);
    • 清空上次从路由对象to对应的所有的组件中得到的beforeRouteEnter守卫,重新收集本次相应的beforeRouteEnter守卫以及canceledNavigationCheck并调用;
    • 最后是收集全局守卫beforeResolve以及canceledNavigationCheck并调用;
    • 以及catch异常捕获并重新抛出,以传递给外层函数消费;

之前的坑在这儿填上了一半,所有的路由守卫都在这里调用,也明确了从路由A成功切换到路由B的过程中触发各守卫的优先级:

  1. 路由A相关的所有组件的beforeRouteLeave
  2. 全局守卫beforeEach
  3. 被更新组件内的路由守卫beforeRouteUpdate
  4. 路由B独享守卫beforeEnter(复用的视图对应的守卫不会再次触发);
  5. 路由B相关的所有组件的beforeRouteEnter
  6. 全局守卫beforeResolve
  7. 全局守卫afterEach(在navigate执行后的then中触发,优先级在最后);
function navigate(
    to: RouteLocationNormalized,
    from: RouteLocationNormalizedLoaded
): Promise<any> {
    let guards: Lazy<any>[]
​
    const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)
​
    // all components here have been resolved once because we are leaving
    guards = extractComponentsGuards(
        leavingRecords.reverse(),
        'beforeRouteLeave',
        to,
        from
    )
​
    // leavingRecords is already reversed
    for (const record of leavingRecords) {
        record.leaveGuards.forEach(guard => {
            guards.push(guardToPromiseFn(guard, to, from))
        })
    }
​
    const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
        null,
        to,
        from
    )
​
    guards.push(canceledNavigationCheck)
​
    // run the queue of per route beforeRouteLeave guards
    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(() => {
                // 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(() => {
                // check the route beforeEnter
                guards = []
                for (const record of enteringRecords) {
                    // do not trigger beforeEnter on reused views
                    if (record.beforeEnter) {
                        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)
​
                // run the queue of per route beforeEnter guards
                return runGuardQueue(guards)
            })
            .then(() => {
                // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
​
                // clear existing enterCallbacks, these are added by extractComponentsGuards
                to.matched.forEach(record => (record.enterCallbacks = {}))
​
                // check in-component beforeRouteEnter
                guards = extractComponentsGuards(
                    enteringRecords,
                    'beforeRouteEnter',
                    to,
                    from,
                    runWithContext
                )
                guards.push(canceledNavigationCheck)
​
                // run the queue of per route beforeEnter guards
                return runGuardQueue(guards)
            })
            .then(() => {
                // check global guards beforeResolve
                guards = []
                for (const guard of beforeResolveGuards.list()) {
                    guards.push(guardToPromiseFn(guard, to, from))
                }
                guards.push(canceledNavigationCheck)
​
                return runGuardQueue(guards)
            })
            // catch any navigation canceled
            .catch(err =>
                isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
                    ? err
                    : Promise.reject(err)
            )
        )
    }
  • runGuardQueue -- 调用守卫队列
// TODO: type this as NavigationGuardReturn or similar instead of any
function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
    return guards.reduce(
        (promise, guard) => promise.then(() => runWithContext(guard)),
        Promise.resolve()
    )
}

在这儿产生了个疑问,guards队列中最后入队的是canceledNavigationCheck,但是按理说每次调用队列里的守卫时,应该是最先执行canceledNavigationCheck从而决定是否执行其它所有的钩子才对?但是从上面的代码来看,似乎canceledNavigationCheck是最后执行的?暂时没搞明白,有懂的小伙伴欢迎评论区指导我一下,不胜感激!

  • finalizeNavigation -- 清除路由守卫;push类型的导航则会通过routerHistory.replacerouterHistory.push来切换路由,关于routerHistory先留个坑;触发滚动行为;
/**
 * - Cleans up any navigation guards
 * - Changes the url if necessary
 * - Calls the scrollBehavior
 */
function finalizeNavigation(
    toLocation: RouteLocationNormalizedLoaded,
    from: RouteLocationNormalizedLoaded,
    isPush: boolean,
    replace?: boolean,
    data?: HistoryState
): NavigationFailure | void {
    // a more recent navigation took place
    const error = checkCanceledNavigation(toLocation, from)
    if (error) return error
​
    // only consider as push if it's not the first navigation
    const isFirstNavigation = from === START_LOCATION_NORMALIZED
    const state: Partial<HistoryState> | null = !isBrowser ? {} : history.state
​
    // change URL only if the user did a push/replace and if it's not the initial navigation because
    // it's just reflecting the url
    
    if (isPush) {
        // on the initial navigation, we want to reuse the scroll position from
        // history state if it exists
        
        // replace或者页面首次导航时调用replace方法
        if (replace || isFirstNavigation)
            routerHistory.replace(
                toLocation.fullPath,
                assign(
                    {
                    scroll: isFirstNavigation && state && state.scroll,
                    },
                    data
                )
            )
        // 否则调用push方法
        else routerHistory.push(toLocation.fullPath, data)
    }
​
    // accept current navigation
    // 设置当前路由
    currentRoute.value = toLocation
    // 处理滚动
    handleScroll(toLocation, from, isPush, isFirstNavigation)
​
    // 一切就绪
    markAsReady()
}
  • setupListeners -- 安装路由侦听器,监测路由的手动变更

    • removeHistoryListener已有有效值,则直接返回,避免因首次导航不合规导致重复安装;

    • 否则,调用用routerHistory.listen来对监听路由,并得到removeHistoryListener用于解除监听:

      • 若是Router实例上listening属性为虚值,则什么也不做(此时应该是微前端项目,路由的监听可交由微前端框架来处理)
      • 通过resolve解析to参数,得到要去的路由位置toLocation
      • 检测如果需要重定向,则立即进行重定向导航;
      • pendingLocation指向toLocation
      • 如果是在浏览器环境,则保存当前页面滚动位置,以便后续回到本页面时恢复到该位置;
      • 调用navigate进行导航动作以及异常处理;
      • triggerAfterEach触发afterEach守卫;
let removeHistoryListener: undefined | null | (() => void)
// attach listener to history to trigger navigations
function setupListeners() {
    // avoid setting up listeners twice due to an invalid first navigation
    if (removeHistoryListener) return
    removeHistoryListener = routerHistory.listen((to, _from, info) => {
        // 微前端项目,在Vue Router中关闭路由监听
        if (!router.listening) return
        // cannot be a redirect route because it was in history
        const toLocation = resolve(to) as RouteLocationNormalized
​
        // due to dynamic routing, and to hash history with manual navigation
        // (manually changing the url or calling history.hash = '#/somewhere'),
        // there could be a redirect record in history
        
        // 动态路由中手动更改url或调用history.hash,就可能会产生重定向记录
        const shouldRedirect = handleRedirectRecord(toLocation)
        if (shouldRedirect) {
            pushWithRedirect(
                assign(shouldRedirect, { replace: true }),
                toLocation
            ).catch(noop)
            return
        }
​
        pendingLocation = toLocation
        const from = currentRoute.value
​
        // TODO: should be moved to web history?
        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)
                ) {
                    // Here we could call if (info.delta) routerHistory.go(-info.delta,
                    // false) but this is bug prone as we have no way to wait the
                    // navigation to be finished before calling pushWithRedirect. Using
                    // a setTimeout of 16ms seems to work but there is no guarantee for
                    // it to work on every browser. So instead we do not restore the
                    // history entry and trigger a new navigation as requested by the
                    // navigation guard.
​
                    // the error is already handled by router.push we just want to avoid
                    // logging the error
                    pushWithRedirect(
                        (error as NavigationRedirectError).to,
                        toLocation
                        // avoid an uncaught rejection, let push call triggerError
                    )
                    .then(failure => {
                        // manual change in hash history #916 ending up in the URL not
                        // changing, but it was changed by the manual url change, so we
                        // need to manually change it ourselves
                        if (
                            isNavigationFailure(
                                failure,
                                ErrorTypes.NAVIGATION_ABORTED |
                                ErrorTypes.NAVIGATION_DUPLICATED
                            ) &&
                            !info.delta &&
                            info.type === NavigationType.pop
                        ) {
                            routerHistory.go(-1, false)
                        }
                    })
                    .catch(noop)
                    // avoid the then branch
                    return Promise.reject()
                }
                // do not restore history on unknown direction
                if (info.delta) {
                    routerHistory.go(-info.delta, false)
                }
                // unrecognized error, transfer to the global handler
                return triggerError(error, toLocation, from)
            })
            .then((failure: NavigationFailure | void) => {
                failure =
                    failure ||
                    finalizeNavigation(
                        // after navigation, all matched components are resolved
                        toLocation as RouteLocationNormalizedLoaded,
                        from,
                        false // isPush参数为false,不会当成push类型的导航来处理
                    )
​
                // revert the navigation
                if (failure) {
                    if (
                    info.delta &&
                    // a new navigation has been triggered, so we do not want to revert, that will change the current history
                    // entry while a different route is displayed
                    !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED)
                    ) {
                        routerHistory.go(-info.delta, false)
                    } else if (
                        info.type === NavigationType.pop &&
                        isNavigationFailure(
                            failure,
                            ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED
                        )
                    ) {
                        // manual change in hash history #916
                        // it's like a push but lacks the information of the direction
                        routerHistory.go(-1, false)
                    }
                }
​
                triggerAfterEach(
                    toLocation as RouteLocationNormalizedLoaded,
                    from,
                    failure
                )
            })
            // avoid warnings in the console about uncaught rejections, they are logged by triggerErrors
            .catch(noop)
        })
    }

初始化就绪状态与错误侦听器:

let readyHandlers = useCallbacks<OnReadyCallback>()
let errorListeners = useCallbacks<_ErrorListener>()
let ready: boolean
  • triggerError -- 触发异常:

    • 就绪状态标记为异常;
    • 依次触发errorListeners中的异常处理程序;
    • 返回Peomise拒绝状态;
/**
 * Trigger errorListeners added via onError and throws the error as well
 *
 * @param error - error to throw
 * @param to - location we were navigating to when the error happened
 * @param from - location we were navigating from when the error happened
 * @returns the error as a rejected promise
 */
function triggerError(
    error: any,
    to: RouteLocationNormalized,
    from: RouteLocationNormalizedLoaded
): Promise<unknown> {
    markAsReady(error)
    const list = errorListeners.list()
    if (list.length) {
        list.forEach(handler => handler(error, to, from))
    } else {
        if (__DEV__) {
            warn('uncaught error during route navigation:')
        }
        console.error(error)
    }
    // reject the error no matter there were error listeners or not
    return Promise.reject(error)
}
  • isReady -- 用于判断前端路由器是否已就绪;
function isReady(): Promise<void> {
    if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
        return Promise.resolve()
    return new Promise((resolve, reject) => {
        readyHandlers.add([resolve, reject])
    })
}
  • markAsReady:标记就绪状态以及安装路由监听器;有两个重载签名:接收error并返回错误或不接收参数且无返回值(返回undefiend);
/**
 * Mark the router as ready, resolving the promised returned by isReady(). Can
 * only be called once, otherwise does nothing.
 * @param err - optional error
 */
function markAsReady<E = any>(err: E): E
function markAsReady<E = any>(): void
function markAsReady<E = any>(err?: E): E | void {
    if (!ready) {
        // still not ready if an error happened
        ready = !err
        // setupListeners中有检测是否已安装监听器,因此无需担心重复执行
        setupListeners()
        readyHandlers
            .list()
            .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
        readyHandlers.reset()
    }
    return err
}
  • handleScroll -- 处理滚动行为

    • 非浏览器环境或者没有在选项参数中设置scrollBehavior,则不会有任何效果;
    • 获取滚动位置:对于是否是push类型的导航,获取方式不一样;
    • 结合nextTick来滚动至对应位置;
// Scroll behavior
function handleScroll(
    to: RouteLocationNormalizedLoaded,
    from: RouteLocationNormalizedLoaded,
    isPush: boolean,
    isFirstNavigation: boolean
  ): Promise<unknown> {
    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))
    }

到这里,Router的实例所需要的内容基本都创建完毕。剩下的还可以关注的内容便是install方法。众所周知这是提供给app.use使用的。

  • 添加两个全局组件RouterLinkRouterView
  • Router的实例router和当前路由信息route挂到Vue的全局属性app.config.globalProperties.$routerapp.config.globalProperties.$route上,以便再模板以及proxy中使用;值得一提的是,组合式APIuseRouteruseRoute也是通过inject取的这俩;
  • 客户端(浏览器环境下)会导航至初始路由;
  • Object.defineProperty配合shallowReactive得到响应式路由信息;
  • 提供路由器、响应式路由信息等数据;
  • Vue应用添加到已安装应用中;
  • 修改Vue应用的卸载方法,添加一些重置路由器的逻辑;
  • 开发环境下还会添加开发者工具;
const go = (delta: number) => routerHistory.go(delta)
let started: boolean | undefined
const installedApps = new Set<App>()
​
const router: Router = {
    currentRoute,
    listening: true,
​
    addRoute,
    removeRoute,
    hasRoute,
    getRoutes,
    resolve,
    options,
​
    push,
    replace,
    go,
    back: () => go(-1),
    forward: () => go(1),
​
    // 在项目里使用到的几个路由钩子,都不过是各个守卫的`add`方法
    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,
​
    onError: errorListeners.add,
    isReady,
​
    install(app: App) {
        const router = this
        app.component('RouterLink', RouterLink)
        app.component('RouterView', RouterView)
​
        app.config.globalProperties.$router = router
        Object.defineProperty(app.config.globalProperties, '$route', {
            enumerable: true,
            get: () => unref(currentRoute),
        })
​
        // this initial navigation is only necessary on client, on server it doesn't
        // make sense because it will create an extra unnecessary navigation and could
        // lead to problems
        if (
            isBrowser &&
            // used for the initial navigation client side to avoid pushing
            // multiple times when the router is used in multiple apps
            !started &&
            currentRoute.value === START_LOCATION_NORMALIZED
        ) {
            // see above
            started = true
            push(routerHistory.location).catch(err => {
                if (__DEV__) warn('Unexpected error when starting the router:', err)
            })
        }
​
        const reactiveRoute = {} as RouteLocationNormalizedLoaded
        for (const key in START_LOCATION_NORMALIZED) {
            Object.defineProperty(reactiveRoute, key, {
                get: () => currentRoute.value[key as keyof RouteLocationNormalized],
                enumerable: true,
            })
        }
​
        app.provide(routerKey, router)
        app.provide(routeLocationKey, shallowReactive(reactiveRoute))
        app.provide(routerViewLocationKey, currentRoute)
​
        const unmountApp = app.unmount
        installedApps.add(app)
        app.unmount = function () {
            installedApps.delete(app)
            // the router is not attached to an app anymore
            if (installedApps.size < 1) {
                // invalidate the current navigation
                pendingLocation = START_LOCATION_NORMALIZED
                removeHistoryListener && removeHistoryListener()
                removeHistoryListener = null
                currentRoute.value = START_LOCATION_NORMALIZED
                started = false
                ready = false
            }
            unmountApp()
        }
​
        // TODO: this probably needs to be updated so it can be used by vue-termui
        if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
            addDevtools(app, router, matcher)
        }
    },
}

最后是返回创建好的router

export function createRouter(options: RouterOptions): Router {
    // ...
    return router
}

顺便看一下用到的另一个方法:extractChangingRecords用于获取离开、更新、进入的路由记录:

  • 既能被from匹配到又会被to匹配到的会被放入将被更新的数组中;
  • 只被from匹配到的则是将离开的路由数组中;
  • 只被to匹配到的则是在即将进入的路由数组中;
function extractChangingRecords(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
  const leavingRecords: RouteRecordNormalized[] = []
  const updatingRecords: RouteRecordNormalized[] = []
  const enteringRecords: RouteRecordNormalized[] = []
​
  const len = Math.max(from.matched.length, to.matched.length)
  for (let i = 0; i < len; i++) {
    const recordFrom = from.matched[i]
    if (recordFrom) {
      if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
        updatingRecords.push(recordFrom)
      else leavingRecords.push(recordFrom)
    }
    const recordTo = to.matched[i]
    if (recordTo) {
      // the type doesn't matter because we are comparing per reference
      if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
        enteringRecords.push(recordTo)
      }
    }
  }
​
  return [leavingRecords, updatingRecords, enteringRecords]
}

虽然才一千几百行代码,阅读起来感觉就是个小卡拉米,写起来却跟打Boss一样。一通乱分析看起来洋洋洒洒春风得意,实则早已戴上了痛苦面具饱受折磨。为什么我要经历这种人间疾苦,呜呜呜。

虽然但是,也不算白干,尽管没多少收获,但至少我累着了。那就这样吧,下一章再见!