VueRouter4源码学习——基础

VueRouter4 github地址:github.com/vuejs/vue-r…
本文基于release版本v4.0.8

一、入口文件分析

源码的入口文件,通常可以通过package.json文件的scripts中得知:

{
    "scripts": {
        "build": "rollup -c rollup.config.js",
        ...
        "dev": "webpack serve --mode=development",
        ...
    }
}
复制代码

在源码的scripts配置中,通过build命令可以得知以下信息:

  • 生产版本通过rollup构建工具进行打包
  • 可以通过rollup.config.js文件获取入口文件信息

rollup.config.js文件中查找input配置,得到入口文件地址为src/index.ts

{
    input: `src/index.ts`,
}
复制代码

二、入口文件导出的模块

打开入口文件src/index.ts,源代码如下:

export { createWebHistory } from './history/html5'
export { createMemoryHistory } from './history/memory'
export { createWebHashHistory } from './history/hash'
export { createRouterMatcher, RouterMatcher } from './matcher'

export {
  LocationQuery,
  parseQuery,
  stringifyQuery,
  LocationQueryRaw,
  LocationQueryValue,
  LocationQueryValueRaw,
} from './query'

export { RouterHistory, HistoryState } from './history/common'

export { RouteRecord, RouteRecordNormalized } from './matcher/types'

export {
  PathParserOptions,
  _PathParserOptions,
} from './matcher/pathParserRanker'

export {
  routeLocationKey,
  routerViewLocationKey,
  routerKey,
  matchedRouteKey,
  viewDepthKey,
} from './injectionSymbols'

export {
  // route location
  _RouteLocationBase,
  LocationAsPath,
  LocationAsRelativeRaw,
  RouteQueryAndHash,
  RouteLocationRaw,
  RouteLocation,
  RouteLocationNormalized,
  RouteLocationNormalizedLoaded,
  RouteParams,
  RouteParamsRaw,
  RouteParamValue,
  RouteParamValueRaw,
  RouteLocationMatched,
  RouteLocationOptions,
  RouteRecordRedirectOption,
  // route records
  _RouteRecordBase,
  RouteMeta,
  START_LOCATION_NORMALIZED as START_LOCATION,
  RouteComponent,
  // RawRouteComponent,
  RouteRecordName,
  RouteRecordRaw,
  NavigationGuard,
  NavigationGuardNext,
  NavigationGuardWithThis,
  NavigationHookAfter,
} from './types'

export {
  createRouter,
  Router,
  RouterOptions,
  RouterScrollBehavior,
} from './router'

export {
  NavigationFailureType,
  NavigationFailure,
  isNavigationFailure,
} from './errors'

export { onBeforeRouteLeave, onBeforeRouteUpdate } from './navigationGuards'
export {
  RouterLink,
  useLink,
  RouterLinkProps,
  UseLinkOptions,
} from './RouterLink'
export { RouterView, RouterViewProps } from './RouterView'

export * from './useApi'
export * from './globalExtensions'
复制代码

分析export的内容,可以将导出的API分为以下几类:

  • history 模块
  • matcher 模块
  • router 模块
  • RouterLink 模块
  • RouterView 模块
  • errors 模块
  • navigationGuards 模块
  • 其他
    • injectionSymbols
    • types
    • useApi
    • globalExtensions

下面将开始从VueRouter的文档中的基础到进阶,逐一分析以上模块的基本功能和实现原理。

三、基础

对应使用文档地址:next.router.vuejs.org/zh/guide/

1、createRouter创建路由实例

在文档提供的示例代码中,路由配置的定义与VueRouter3基本没有变化,但是路由实例,是通过执行createRouter(options)方法创建的。

const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]
const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
})
复制代码

在入口文件中找到createRouter方法,它是通过router模块导出的,router模块源码路径为src/router.ts,在该文件中找到createRouter方法源码,简化后代码如下。
简而言之,该方法就是传入RouterOptions类型的对象,然后返回一个Router实例。

 export function createRouter(options: RouterOptions): Router {
  // ... 省略了一堆代码 ...
  const router: Router = {
    currentRoute,

    addRoute,
    removeRoute,
    hasRoute,
    getRoutes,
    resolve,
    options,

    push,
    replace,
    go,
    back: () => go(-1),
    forward: () => go(1),

    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,

    onError: errorHandlers.add,
    isReady,

    install(app: App) {
      // ...
    },
  }

  return router
}
复制代码

2、参数:RouterOptions

createRouter()方法只有一个options对象参数,类型是RouterOptions

// src/router.ts
export interface RouterOptions extends PathParserOptions {
  history: RouterHistory
  routes: RouteRecordRaw[]
  scrollBehavior?: RouterScrollBehavior
  parseQuery?: typeof originalParseQuery
  stringifyQuery?: typeof originalStringifyQuery
  linkActiveClass?: string
  linkExactActiveClass?: string
}
复制代码

从接口定义可以看出,必须包含的对象属性为:

  • history:用于路由实现历史记录,类型为RouterHistory
  • routes:应该添加到路由的初始路由列表,类型为 RouteRecordRaw

以下属性为非必需属性:

  • scrollBehavior:在页面之间导航时控制滚动的函数。可以返回一个 Promise 来延迟滚动。
  • parseQuery:用于解析查询的自定义实现。必须解码查询键和值。参见对应的 stringifyQuery
  • stringifyQuery:对查询对象进行字符串化的自定义实现。不应该在前面加上 ?。应该正确编码查询键和值。 parseQuery 对应于处理查询解析。
  • linkActiveClass:用于激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-active
  • linkExactActiveClass:用于精准激活的 RouterLink 的默认类。如果什么都没提供,则会使用 router-link-exact-active

下面分析下historyroutes这两个属性。

2-1、history

RouterHistory的接口定义如下:

interface RouterHistory {
  // 只读属性,基本路径,会添加到每个url的前面
  readonly base: string
  // 只读属性,当前路由
  readonly location: HistoryLocation
  // 只读属性,当前状态
  readonly state: HistoryState
  // 路由跳转方法
  push(to: HistoryLocation, data?: HistoryState): void
  // 路由跳转方法
  replace(to: HistoryLocation, data?: HistoryState): void
  // 路由跳转方法
  go(delta: number, triggerListeners?: boolean): void
  // 添加一个路由事件监听器
  listen(callback: NavigationCallback): () => void
  // 生成在锚点标签中使用的href的方法
  createHref(location: HistoryLocation): string
  // 清除listeners
  destroy(): void
}
复制代码

VueRouter提供三种方法创建RouterHistory对象:

  • createWebHashHistory(): 创建一个 hash 历史记录。对于没有主机的 web 应用程序 (例如 file://),或当配置服务器不能处理任意 URL 时这非常有用。注意:如果 SEO 对你很重要,你应该使用 createWebHistory。

  • createWebHistory(): 创建一个 HTML5 历史,即单页面应用程序中最常见的历史记录。应用程序必须通过 http 协议被提供服务。

  • createMemoryHistory():创建一个基于内存的历史记录。这个历史记录的主要目的是处理 SSR。它在一个特殊的位置开始,这个位置无处不在。如果用户不在浏览器上下文中,它们可以通过调用 router.push() 或 router.replace() 将该位置替换为启动位置。

换一句话说,在创建VueRouter实例时,options.history参数为以上三种的其中一种或者自定义方法(需要返回RouterHistory类型对象)。

1、createWebHashHistory

(1)base

在上面提供的例子中,调用createWebHashHistory方法时没有传入任何参数,访问地址是 http://localhost:8080 ,则此时base为'/',此时没有#,所以base会再追加一个#符号。接着调用createWebHistory函数继续创建其他属性或者方法。

base = location.host ? base || location.pathname + location.search : ''
if (base.indexOf('#') < 0) 
    base += '#'
return createWebHistory(base)
复制代码

(2)其他属性和方法

除了base属性,其他属性和方法都是通过createWebHistory(base)方法进行创建的,所以其他属性和方法,在createWebHistory(base)中进行分析。

2、createWebHistory

(1)base属性

首先对base进行格式化,上面在调用createWebHashHistory创建hash模式history对象时,传进来的base的值为'/#',调用normalizeBase函数后,得到的base依然是'/#'

如果是在创建VueRouter实例时,调用createWebHistory()创建history对象,则此时base时undefined,调用normalizeBase格式化后的base为空字符串''

base = normalizeBase(base)
复制代码

normalizeBase方法代码如下:

// src/utils/env.ts
export const isBrowser = typeof window !== 'undefined'

// src/history/common.ts
function normalizeBase(base?: string): string {
  if (!base) {
    if (isBrowser) {
      const baseEl = document.querySelector('base')
      base = (baseEl && baseEl.getAttribute('href')) || '/'
      base = base.replace(/^\w+:\/\/[^\/]+/, '')
    } else {
      base = '/'
    }
  }
  if (base[0] !== '/' && base[0] !== '#') base = '/' + base
  return removeTrailingSlash(base)
}

// src/location.ts
const TRAILING_SLASH_RE = /\/$/
export const removeTrailingSlash = (path: string) => path.replace(TRAILING_SLASH_RE, '')
复制代码

(2)其他属性和方法的创建

createWebHistory方法中,通过调用useHistoryStateNavigation(base)方法,返回一个包含location,state,push,replace属性和方法的对象。

// src/history/html5.ts
const historyNavigation = useHistoryStateNavigation(base)
复制代码

然后通过调用useHistoryListeners(...)函数,返回pauseListeners,listen,destroy方法的对象。

const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace
  )
复制代码

接着声明go()方法:

function go(delta: number, triggerListeners = true) {
    // ...
}
复制代码

然后将以上两个方法的到的对象以及默认对象组合成routerHistory对象,此时routerHistory对象创建完成。

// src/history/html5.ts
const routerHistory: RouterHistory = assign(
  {
    // it's overridden right after
    location: '',
    base,
    go,
    createHref: createHref.bind(null, base),
  },

  historyNavigation,
  historyListeners
)
复制代码

最后为location,state这两个属性添加getter,读取这两个属性值时,返回该对象的value属性值。

Object.defineProperty(routerHistory, 'location', {
  enumerable: true,
  get: () => historyNavigation.location.value,
})

Object.defineProperty(routerHistory, 'state', {
  enumerable: true,
  get: () => historyNavigation.state.value,
})
复制代码

hash和history路由模式,除了base的处理逻辑不同,其他属性或者方法使用的是共同的逻辑。现在了解了创建RouterHistory对象的整体流程,然后再具体分析除了base之外的属性或者方法的实现逻辑。

  • location

    location属性是在useHistoryStateNavigation()方法中声明的,该方法中与location相关的代码简化后如下所示。

    function useHistoryStateNavigation(base: string) {
      const { location } = window
      let currentLocation: ValueContainer<HistoryLocation> = {
        value: createCurrentLocation(base, location),
      }
      return {
        location: currentLocation,
      }
    }
    
    function createCurrentLocation(
      base: string,
      location: Location
    ): HistoryLocation {
      const { pathname, search, hash } = location
      // 支持的hash如 #, /#, #/, #!, #!/, /#!/, 或者 /folder#end
      const hashPos = base.indexOf('#')
      // 如果是hash类型
      if (hashPos > -1) {
        let slicePos = hash.includes(base.slice(hashPos))
          ? base.slice(hashPos).length
          : 1
        let pathFromHash = hash.slice(slicePos)
        // prepend the starting slash to hash so the url starts with /#
        if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash
        return stripBase(pathFromHash, '')
      }
      const path = stripBase(pathname, base)
      return path + search + hash
    }
    
    function stripBase(pathname: string, base: string): string {
      // 没有 base 或者 base 在开始位置找不到
      if (!base || pathname.toLowerCase().indexOf(base.toLowerCase()))
        return pathname
      return pathname.slice(base.length) || '/'
    }
    复制代码

    首先声明内部属性currentLocation,是只有一个value属性的对象,value的值通过createCurrentLocation(base, location)方法获取,该方法的作用是将当前url格式化为标准的path + search + hash的字符串。

  • state

    state属性也是在useHistoryStateNavigation方法中声明的,与state相关的代码如下。

    function useHistoryStateNavigation(base: string) {
      const { history } = window
      let historyState: ValueContainer<StateEntry> = { value: history.state }
    
      return {
        state: historyState,
      }
    }
    复制代码

    state属性是window.history.state作为value属性值的对象。

    在创建声明了state属性之后,需要进行以下判断。

    如果该值是假值时,表示浏览器history未进行任何操作,此时需要调用changeLocation方法。changeLocation方法十分重要,它是push方法和replace等路由跳转方法的实现基础。该方法包含三个参数:目标location、目标state对象、是否为替换当前location。

    if (!historyState.value) {
      changeLocation(
        currentLocation.value,
        {
          back: null,
          current: currentLocation.value,
          forward: null,
          position: history.length - 1,
          replaced: true,
          scroll: null,
        },
        true
      )
    }
    
    let createBaseLocation = () => location.protocol + '//' + location.host
    
    function changeLocation(
      to: HistoryLocation,
      state: StateEntry,
      replace: boolean
    ): void {
      const hashIndex = base.indexOf('#')
      const url =
        hashIndex > -1
          ? (location.host && document.querySelector('base')
              // base + currentLocation.value
              ? base
              // #xxx + currentLocation.value
              : base.slice(hashIndex)) + to
          // `url`为:`协议://主机地址 + base + currentLocation.value`;
          : createBaseLocation() + base + to
      try {
        // 尝试使用history api更改window.history
        history[replace ? 'replaceState' : 'pushState'](state, '', url)
        historyState.value = state
      } catch (err) {
        // 如果使用history api失败,则降级使用window.location方法代替
        location[replace ? 'replace' : 'assign'](url)
      }
    }
    复制代码
  • replace 方法

    replace方法也是在useHistoryStateNavigation中声明的,相关源代码如下,replace方法接收todata参数,首先通过参数构建state对象,然后调用changeLocation方法进行路由跳转。

    function useHistoryStateNavigation(base: string) {
      const { history } = window
      function replace(to: HistoryLocation, data?: HistoryState) {
        // 整合state对象
        const state: StateEntry = assign(
          {},
          history.state,
          buildState(
            historyState.value.back,
            to,
            historyState.value.forward,
            true
          ),
          data,
          { position: historyState.value.position }
        )
        // 调用路由跳转方法
        changeLocation(to, state, true)
        currentLocation.value = to
      }
    
      return {
        replace,
      }
    }
    
    const computeScrollPosition = () =>
      ({
        left: window.pageXOffset,
        top: window.pageYOffset,
      } as _ScrollPositionNormalized)
    
    function buildState(
      back: HistoryLocation | null,
      current: HistoryLocation,
      forward: HistoryLocation | null,
      replaced: boolean = false,
      computeScroll: boolean = false
    ): StateEntry {
      return {
        back,
        current,
        forward,
        replaced,
        position: window.history.length,
        scroll: computeScroll ? computeScrollPosition() : null,
      }
    }
    复制代码
  • push 方法

    push方法和replace方法类型,相关源码如下。

    function useHistoryStateNavigation(base: string) {
      const { history } = window
    
      function push(to: HistoryLocation, data?: HistoryState) {
        const currentState = assign(
          {},
          historyState.value,
          history.state as Partial<StateEntry> | null,
          {
            forward: to,
            scroll: computeScrollPosition(),
          }
        )
    
        changeLocation(currentState.current, currentState, true)
    
        const state: StateEntry = assign(
          {},
          buildState(currentLocation.value, to, null),
          { position: currentState.position + 1 },
          data
        )
    
        changeLocation(to, state, false)
        currentLocation.value = to
      }
    
      return {
        push,
      }
    }
    复制代码

    观察push方法,发现方法内部调用了两次changeLocation方法,这是为什么呢?

    主要的原因是需要保存当前location对应的页面滚动信息(scroll)以及将要跳转的页面信息(forward),此时调用changeLocation方法的replace参数为true,也就是更新当前location

    然后再创建要跳转的页面的state,这里和replace的不同的是,postion的值需要+1,此时调用changeLocation方法的replace参数为false

  • go 方法

    go方法是直接在createWebHistory方法中声明的,调用window.history.go(delta)进行路由跳转。另外支持triggerListeners参数,默认触发listeners中的回调函数执行,如果传入了false,则调用historyListeners.pauseListeners(),该方法修改了pauseState变量,该变量在后面的popState事件中会涉及到。

    function go(delta: number, triggerListeners = true) {
        if (!triggerListeners) historyListeners.pauseListeners()
        history.go(delta)
    }
    复制代码
    function pauseListeners() {
        pauseState = currentLocation.value
    }
    复制代码
  • listen 方法

    listen方法由useHistoryListeners函数返回,在useHistoryListeners函数内部查看listen方法相关源码。

    function useHistoryListeners() {
      let listeners: NavigationCallback[] = []
      let teardowns: Array<() => void> = []
    
      function listen(callback: NavigationCallback) {
        listeners.push(callback)
    
        const teardown = () => {
          const index = listeners.indexOf(callback)
          if (index > -1) listeners.splice(index, 1)
        }
    
        teardowns.push(teardown)
        return teardown
      }
    
      return {
        listen,
      }
    }
    复制代码

    listen方法的作用是将传入的回调函数添加到listeners数组中,并返回移除函数,同时将移除函数添加到teardowns数组中,方便进行批量移除。

  • createHref 方法

    该方法在createWebHistory方法中bind公共的createHref方法,创建一个参数为basecreateHref方法,调用routerHistory中的createHref方法时,返回一个由当前base组成href。

    // src/history/html5.ts
    function createWebHistory(base?: string): RouterHistory {
        // 省略其他代码
        const routerHistory: RouterHistory = assign(
            {
              // 省略其他代码
              createHref: createHref.bind(null, base),
            },
            // 省略其他代码
        )
        // 省略其他代码
    }
    复制代码
    // src/history/common.ts
    // remove any character before the hash
    const BEFORE_HASH_RE = /^[^#]+#/
    export function createHref(base: string, location: HistoryLocation): string {
      return base.replace(BEFORE_HASH_RE, '#') + location
    }
    复制代码
  • destroy 方法

    destroy方法是在useHistoryListeners函数中生成,简化后的函数源码为:

    function useHistoryListeners() {
      let teardowns: Array<() => void> = []
    
      const popStateHandler: PopStateListener = ({
        state,
      }: {
        state: StateEntry | null
      }) => {
        // ...
      }
    
      function beforeUnloadListener() {
        // ...
      }
    
      function destroy() {
        for (const teardown of teardowns) teardown()
        teardowns = []
        window.removeEventListener('popstate', popStateHandler)
        window.removeEventListener('beforeunload', beforeUnloadListener)
      }
    
      window.addEventListener('popstate', popStateHandler)
      window.addEventListener('beforeunload', beforeUnloadListener)
    
      return {
        destroy,
      }
    }
    复制代码

    destroy方法被调用时,首先遍历teardowns数组,移除所有listener,然后清空teardowns数组,接着解绑在window对象上的popstatebeforeunload事件。

    popstate事件在活动历史记录条目更改时被触发,此时调用popStateHandler函数,该函数源码如下。

    const popStateHandler: PopStateListener = ({
      state,
    }: {
      state: StateEntry | null
    }) => {
      const to = createCurrentLocation(base, location)
      const from: HistoryLocation = currentLocation.value
      const fromState: StateEntry = historyState.value
      let delta = 0
    
      if (state) {
        currentLocation.value = to
        historyState.value = state
    
        // ignore the popstate and reset the pauseState
        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,
        })
      })
    }
    复制代码

    当用户操作浏览器导航按钮或者应用中调用了push/replace/go等方法时,都会触发该事件,调用此函数。除了更新某些对象值之外,这个函数的关键在于遍历listeners数组调用每一个注册的回调函数。

    在上面分析go方法的时候,提到了pauseListeners方法,当该方法被调用后,且等于上一次的location时,pauseState && pauseState === from条件成立,然后会执行pauseState = null;return语句,所以就不会继续执行后面遍历listeners的逻辑了。

    beforeunload事件当浏览器窗口关闭或者刷新时被触发,此时会调用beforeUnloadListener函数,该函数源码如下,主要作用是将当前滚动信息保存到当前历史记录实体中。

    function beforeUnloadListener() {
      const { history } = window
      if (!history.state) return
      history.replaceState(
        assign({}, history.state, { scroll: computeScrollPosition() }),
        ''
      )
    }
    复制代码

createWebHistorycreateWebHashHistory的实现原理分析完毕,先做一个小总结。从以上的分析可以得出,history和hash模式的差别在于base的处理,换句话可以说是浏览器url表现方式的差异,路由的跳转、事件的监听,都是基于history api,但是当使用history进行跳转出现错误时,VueRouter做了容错处理,降级使用location进行跳转。

3、createMemoryHistory

有了以上两个函数的分析基础,再来看createMemoryHistory函数就容易理解过了,内存类型的RouterHistory,由于不是运行在浏览器端,所以不存在window.history对象,接下来直接看routerHistory对象中的属性和方法分别是如何实现的。

  • base
    直接取的base参数的值,默认是’/’。

  • location
    初始值为常量START,每次获取location时,都会通过queue[position]获取最新location的值。

    // src/history/common.ts
    const START: HistoryLocation = ''
    
    // src/history/memory.ts
    const routerHistory: RouterHistory = {
        // rewritten by Object.defineProperty
        location: START,
    }
    Object.defineProperty(routerHistory, 'location', {
        enumerable: true,
        // queue数组模拟的是浏览器历史记录
        get: () => queue[position],
    })
    复制代码
  • state
    默认为一个空对象state: {},但是源代码中有TODO的注释,后续版本应该会有改动。

    // TODO: should be kept in queue
    state: {},
    复制代码
  • push方法
    简而言之,该方法实现的是往queue数组中追加一个location。

    let queue: HistoryLocation[] = [START]
    function setLocation(location: HistoryLocation) {
      position++
      if (position === queue.length) {
        // we are at the end, we can simply append a new entry
        queue.push(location)
      } else {
        // we are in the middle, we remove everything from here in the queue
        queue.splice(position)
        queue.push(location)
      }
    }
    const routerHistory: RouterHistory = {
      push(to, data?: HistoryState) {
        setLocation(to)
      },
    }
    复制代码
  • replace方法
    push方法类型,差别在于replace被调用时,会先把queue中最后一个location删掉,然后再调用setLocation方法,追加一个location

    const routerHistory: RouterHistory = {
      replace(to) {
        // remove current entry and decrement position
        queue.splice(position--, 1)
        setLocation(to)
      },
    }
    复制代码
  • go方法
    通过delta参数大小,然后更新position的值。

    这里并没有调用setLocation方法,也就是不需要更新queue,只是更新了position,后续获取location时是通过queue[position]获取的,所以可以拿到正确的location

    function triggerListeners(
      to: HistoryLocation,
      from: HistoryLocation,
      { direction, delta }: Pick<NavigationInformation, 'direction' | 'delta'>
    ): void {
      const info: NavigationInformation = {
        direction,
        delta,
        type: NavigationType.pop,
      }
      for (let callback of listeners) {
        callback(to, from, info)
      }
    }
    
    const routerHistory: RouterHistory = {
      go(delta, shouldTrigger = true) {
        const from = this.location
        const direction: NavigationDirection =
          // we are considering delta === 0 going forward, but in abstract mode
          // using 0 for the delta doesn't make sense like it does in html5 where
          // it reloads the page
          delta < 0 ? NavigationDirection.back : NavigationDirection.forward
        position = Math.max(0, Math.min(position + delta, queue.length - 1))
        if (shouldTrigger) {
          triggerListeners(this.location, from, {
            direction,
            delta,
          })
        }
      },
    }
    复制代码
  • listen方法
    跟浏览器端要实现的功能基本一致,方法被调用时,将传进来的回调函数添加到listeners数组中,然后返回删除该回调的函数。

    let listeners: NavigationCallback[] = []
    const routerHistory: RouterHistory = {
      listen(callback) {
        listeners.push(callback)
        return () => {
          const index = listeners.indexOf(callback)
          if (index > -1) listeners.splice(index, 1)
        }
      },
    }
    复制代码
  • createHref方法

    实现方式和浏览器端一致。

    const routerHistory: RouterHistory = {
      createHref: createHref.bind(null, base),
    }
    复制代码
  • destroy方法

    该方法被调用时,重置路由相关变量。

    const routerHistory: RouterHistory = {
      destroy() {
        listeners = []
        queue = [START]
        position = 0
      },
    }
    复制代码

    这里与浏览器端的对比,少了teardowns数组,此方法中是直接将listeners数组清空,为什么浏览器端不能直接清空,而需要通过teardowns数组进行管理呢??

history属性的分析到此完结了,通过history三种创建方式的分析,我们了解到了浏览器端路由相关的属性、方法和事件,以及非浏览器端是如何模拟实现浏览器端的路由的,其中内存类型感觉还存在需要完善的地方,后续版本应该会有些许改动。

2-2、routes 属性

回到createRouter方法中,可以看到该方法中只有一个地方用到了options.routes,它作为createRouterMatcher参数,执行后返回一个RouterMatcher类型的对象。

export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  // ....
}
复制代码

matcher模块的入口是src/matcher/index.ts,该模块提供了路由配置相关的属性和方法,matcher接口定义如下。

// src/matcher/index.ts
interface RouterMatcher {
  addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
  removeRoute: {
    (matcher: RouteRecordMatcher): void
    (name: RouteRecordName): void
  }
  getRoutes: () => RouteRecordMatcher[]
  getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined
  resolve: (
    location: MatcherLocationRaw,
    currentLocation: MatcherLocation
  ) => MatcherLocation
}
复制代码

1、createRouterMatcher函数的基本逻辑

简化后的代码如下。

function createRouterMatcher(
  routes: RouteRecordRaw[],
  globalOptions: PathParserOptions
): RouterMatcher {
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  globalOptions = mergeOptions(
    { strict: false, end: true, sensitive: false } as PathParserOptions,
    globalOptions
  )

  function getRecordMatcher(name: RouteRecordName) {
    // ...
  }

  function addRoute(
    record: RouteRecordRaw,
    parent?: RouteRecordMatcher,
    originalRecord?: RouteRecordMatcher
  ) {
    // ...
  }

  function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
    // ...
  }

  function getRoutes() {
    // ...
  }

  function insertMatcher(matcher: RouteRecordMatcher) {
    // ...
  }

  function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): MatcherLocation {
    // ...
  }

  // add initial routes
  routes.forEach(route => addRoute(route))

  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
复制代码

该函数接收两个参数,第一个参数是路由配置数组,第二个参数是VueRouter初始化时传进来的options。然后声明两个变量matchersmatcherMap,然后是声明一系列方法,在return之前,遍历routes,通过addRoute方法,将路由配置转化为matcher。

现在来逐个分析这几个方法。

  • addRoute方法

    function addRoute(
      record: RouteRecordRaw,
      parent?: RouteRecordMatcher,
      originalRecord?: RouteRecordMatcher
    ) {
      let isRootAdd = !originalRecord
      let mainNormalizedRecord = normalizeRouteRecord(record)
      mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
      const options: PathParserOptions = mergeOptions(globalOptions, record)
      const normalizedRecords: typeof mainNormalizedRecord[] = [
        mainNormalizedRecord,
      ]
      if ('alias' in record) {
        const aliases =
          typeof record.alias === 'string' ? [record.alias] : record.alias!
        for (const alias of aliases) {
          normalizedRecords.push(
            assign({}, mainNormalizedRecord, {
              components: originalRecord
                ? originalRecord.record.components
                : mainNormalizedRecord.components,
              path: alias,
              aliasOf: originalRecord
                ? originalRecord.record
                : mainNormalizedRecord,
            }) as typeof mainNormalizedRecord
          )
        }
      }
    
      let matcher: RouteRecordMatcher
      let originalMatcher: RouteRecordMatcher | undefined
    
      for (const normalizedRecord of normalizedRecords) {
        let { path } = normalizedRecord
        if (parent && path[0] !== '/') {
          let parentPath = parent.record.path
          let connectingSlash =
            parentPath[parentPath.length - 1] === '/' ? '' : '/'
          normalizedRecord.path =
            parent.record.path + (path && connectingSlash + path)
        }
        matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
        if (originalRecord) {
          originalRecord.alias.push(matcher)
        } else {
          originalMatcher = originalMatcher || matcher
          if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
          if (isRootAdd && record.name && !isAliasRecord(matcher))
            removeRoute(record.name)
        }
    
        if ('children' in mainNormalizedRecord) {
          let children = mainNormalizedRecord.children
          for (let i = 0; i < children.length; i++) {
            addRoute(
              children[i],
              matcher,
              originalRecord && originalRecord.children[i]
            )
          }
        }
        originalRecord = originalRecord || matcher
        insertMatcher(matcher)
      }
    
      return originalMatcher
        ? () => {
            removeRoute(originalMatcher!)
          }
        : noop
    }
    复制代码

    这个方法要做的事情就是,根据路由配置对象,创建一个matcher对象,然后添加到matchers数组中,最后根据originalMatcher条件return一个移除路由方法或者noop(let noop = () => {})。应用中传进来的路由配置是不完整的,所以需要首先通过normalizeRouteRecord方法对路由配置格式化,生成完整的路由配置对象,其中props属性通过normalizeRecordProps函数格式化,格式化对象是根据路由配置对象中的component或者components生成,如果存在component属性,则props对象包含一个default属性,并赋值为配置中的props,否则取components对象中的key,并从路由配置属性props中取对应的值。

    function normalizeRouteRecord(
      record: RouteRecordRaw
    ): RouteRecordNormalized {
      return {
        path: record.path,
        redirect: record.redirect,
        name: record.name,
        meta: record.meta || {},
        aliasOf: undefined,
        beforeEnter: record.beforeEnter,
        props: normalizeRecordProps(record),
        children: record.children || [],
        instances: {},
        leaveGuards: new Set(),
        updateGuards: new Set(),
        enterCallbacks: {},
        components:
          'components' in record
            ? record.components || {}
            : { default: record.component! },
      }
    }
    
    function normalizeRecordProps(
      record: RouteRecordRaw
    ): Record<string, _RouteRecordProps> {
      const propsObject = {} as Record<string, _RouteRecordProps>
      const props = (record as any).props || false
      if ('component' in record) {
        propsObject.default = props
      } else {
        for (let name in record.components)
          propsObject[name] = typeof props === 'boolean' ? props : props[name]
      }
    
      return propsObject
    }
    复制代码

    调用normalizeRouteRecord方法格式化路由配置对象后,将处理后的mainNormalizedRecord对象添加到normalizedRecords数组中。
    接着判断'alias' in record,如果有别名,则添加记录到normalizedRecords数组,基本逻辑是复制mainNormalizedRecord,然后重新设置componentspathaliasOf属性,换句话说,别名的实现原理是通过拷贝记录,并调整部分属性得到新的记录
    以上代码都是为接下来创建matcher做准备,继续往下分析代码,首先准备两个变量:matcheroriginalMatcher,然后开始遍历normalizedRecords

    这里有个小技巧,matcher是在遍历的过程中赋值的,既然如此为什么不放到遍历内部呢?这是因为如果放在内部,每次遍历都会生成一个新的对象,如果路由数量很多,则会在短时间内频繁地创建临时对象,导致内存占用上升,可能导致频繁的垃圾回收,最终导致页面卡顿,所以放在遍历的外面,可以减少临时变量的数量,优化内存占用,减少垃圾回收的次数。

    在遍历的内部,根据路由配置对象,创建matcher,并插入到matchers中,分为以下几步:

    • 1、如果是子路由配置,且path不是以/开头,则将父路由的path和子路由的path加起来生成完整path

    • 2、调用createRouteRecordMatcher方法,创建matcher对象,如果存在parent,则在parent.children中添加当前matcher对象。

      function createRouteRecordMatcher(
        record: Readonly<RouteRecord>,
        parent: RouteRecordMatcher | undefined,
        options?: PathParserOptions
      ): RouteRecordMatcher {
        const parser = tokensToParser(tokenizePath(record.path), options)
        const matcher: RouteRecordMatcher = assign(parser, {
          record,
          parent,
          children: [],
          alias: [],
        })
      
        if (parent) {
          if (!matcher.record.aliasOf === !parent.record.aliasOf)
            parent.children.push(matcher)
        }
      
        return matcher
      }
      复制代码

      matcher对象类型为RouteRecordMatcher,它继承自PathParser接口,所以一个matcher对象应该包含以下属性和方法,前面五个属性或方法是通过tokensToParser(tokenizePath(record.path), options)创建的,下面将在该方法中分析这几个属性或方法的实现逻辑。

      • re: RegExp
      • score: Array<number[]>
      • keys: PathParserParamKey[]
      • parse(path: string): PathParams | null
      • stringify(params: PathParams): string
      • record: RouteRecord 保存格式化后的路由配置记录
      • parent: RouteRecordMatcher | undefined 保存父路由matcher对象
      • children: RouteRecordMatcher[] 子路由,初始化为空数组
      • alias: RouteRecordMatcher[] 别名,初始化为空数组

      在分析tokensToParser之前,需要先看一下tokenizePath(record.path),该方法将path转化为一个token数组。

      export const enum TokenType {
        Static,
        Param,
        Group,
      }
      
      const enum TokenizerState {
        Static,
        Param,
        ParamRegExp, // custom re for a param
        ParamRegExpEnd, // check if there is any ? + *
        EscapeNext,
      }
      
      interface TokenStatic {
        type: TokenType.Static
        value: string
      }
      
      interface TokenParam {
        type: TokenType.Param
        regexp?: string
        value: string
        optional: boolean
        repeatable: boolean
      }
      
      interface TokenGroup {
        type: TokenType.Group
        value: Exclude<Token, TokenGroup>[]
      }
      
      export type Token = TokenStatic | TokenParam | TokenGroup
      
      const ROOT_TOKEN: Token = {
        type: TokenType.Static,
        value: '',
      }
      
      const VALID_PARAM_RE = /[a-zA-Z0-9_]/
      // After some profiling, the cache seems to be unnecessary because tokenizePath
      // (the slowest part of adding a route) is very fast
      
      // const tokenCache = new Map<string, Token[][]>()
      
      export function tokenizePath(path: string): Array<Token[]> {
        if (!path) return [[]]
        if (path === '/') return [[ROOT_TOKEN]]
        if (!path.startsWith('/')) {
          throw new Error(
            __DEV__
              ? `Route paths should start with a "/": "${path}" should be "/${path}".`
              : `Invalid path "${path}"`
          )
        }
      
        // if (tokenCache.has(path)) return tokenCache.get(path)!
      
        function crash(message: string) {
          throw new Error(`ERR (${state})/"${buffer}": ${message}`)
        }
      
        let state: TokenizerState = TokenizerState.Static
        let previousState: TokenizerState = state
        const tokens: Array<Token[]> = []
        // the segment will always be valid because we get into the initial state
        // with the leading /
        let segment!: Token[]
      
        function finalizeSegment() {
          if (segment) tokens.push(segment)
          segment = []
        }
      
        // index on the path
        let i = 0
        // char at index
        let char: string
        // buffer of the value read
        let buffer: string = ''
        // custom regexp for a param
        let customRe: string = ''
      
        function consumeBuffer() {
          if (!buffer) return
      
          if (state === TokenizerState.Static) {
            segment.push({
              type: TokenType.Static,
              value: buffer,
            })
          } else if (
            state === TokenizerState.Param ||
            state === TokenizerState.ParamRegExp ||
            state === TokenizerState.ParamRegExpEnd
          ) {
            if (segment.length > 1 && (char === '*' || char === '+'))
              crash(
                `A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`
              )
            segment.push({
              type: TokenType.Param,
              value: buffer,
              regexp: customRe,
              repeatable: char === '*' || char === '+',
              optional: char === '*' || char === '?',
            })
          } else {
            crash('Invalid state to consume buffer')
          }
          buffer = ''
        }
      
        function addCharToBuffer() {
          buffer += char
        }
      
        while (i < path.length) {
          char = path[i++]
      
          if (char === '\\' && state !== TokenizerState.ParamRegExp) {
            previousState = state
            state = TokenizerState.EscapeNext
            continue
          }
      
          switch (state) {
            case TokenizerState.Static:
              if (char === '/') {
                if (buffer) {
                  consumeBuffer()
                }
                finalizeSegment()
              } else if (char === ':') {
                consumeBuffer()
                state = TokenizerState.Param
              } else {
                addCharToBuffer()
              }
              break
      
            case TokenizerState.EscapeNext:
              addCharToBuffer()
              state = previousState
              break
      
            case TokenizerState.Param:
              if (char === '(') {
                state = TokenizerState.ParamRegExp
              } else if (VALID_PARAM_RE.test(char)) {
                addCharToBuffer()
              } else {
                consumeBuffer()
                state = TokenizerState.Static
                // go back one character if we were not modifying
                if (char !== '*' && char !== '?' && char !== '+') i--
              }
              break
      
            case TokenizerState.ParamRegExp:
              // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
              // it already works by escaping the closing )
              // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
              // is this really something people need since you can also write
              // /prefix_:p()_suffix
              if (char === ')') {
                // handle the escaped )
                if (customRe[customRe.length - 1] == '\\')
                  customRe = customRe.slice(0, -1) + char
                else state = TokenizerState.ParamRegExpEnd
              } else {
                customRe += char
              }
              break
      
            case TokenizerState.ParamRegExpEnd:
              // same as finalizing a param
              consumeBuffer()
              state = TokenizerState.Static
              // go back one character if we were not modifying
              if (char !== '*' && char !== '?' && char !== '+') i--
              customRe = ''
              break
      
            default:
              crash('Unknown state')
              break
          }
        }
      
        if (state === TokenizerState.ParamRegExp)
          crash(`Unfinished custom RegExp for param "${buffer}"`)
      
        consumeBuffer()
        finalizeSegment()
      
        // tokenCache.set(path, tokens)
      
        return tokens
      }
      复制代码

      这个函数的目的是将path字符串转换为数组,方便后续的处理。
      比如/user会转换为[[{type: 0, value: 'user'}]], /user/:id则会转换成:

      [
          [{type: 0, value: "user"}],
          [{type: 1, value: "id", regexp: "", repeatable: false, optional: false}],
      ]
      复制代码

      再回到tokensToParser函数进行分析PathParser是如何生成的。

      • re
        就是一个正则表达式,通过从参数传进来的path的tokens以及一些列的条件判断,将token转换为匹配path的正则表达式。

        const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
          sensitive: false,
          strict: false,
          start: true,
          end: true,
        }
        function tokensToParser(
          segments: Array<Token[]>,
          extraOptions?: _PathParserOptions
        ): PathParser {
          const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
          let pattern = options.start ? '^' : ''
          for (const segment of segments) {
              // 遍历tokens逐步完善正则表达式
              // TODO:这里挖个坑,以后再分析如何生成正则表达式的
          }
          if (!options.strict) pattern += '/?'
          if (options.end) pattern += '$'
          else if (options.strict) pattern += '(?:/|$)'
          const re = new RegExp(pattern, options.sensitive ? '' : 'i')
          // ...
        }
        复制代码
      • score
        给当前path计算一个分数,在后续path之间进行比较时,可以用到score的值,相当于权重的比较。

        let score: Array<number[]> = []
        for (const segment of segments) {
            const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
            // ...
            score.push(segmentScores)
        }
        if (options.strict && options.end) {
            const i = score.length - 1
            score[i][score[i].length - 1] += PathScore.BonusStrict
        }
        复制代码
      • keys
        保存路由中的动态参数。

        const keys: PathParserParamKey[] = []
        for (const segment of segments) {
            // ...
            if (token.type === TokenType.Param) {
                const { value, repeatable, optional, regexp } = token
                keys.push({
                  name: value,
                  repeatable,
                  optional,
                })
            }
            // ...
        }
        复制代码
      • parse
        传入path参数,然后根据re,然后遍历得到的结果,获取动态参数对象。

        function parse(path: string): PathParams | null {
            const match = path.match(re)
            const params: PathParams = {}
        
            if (!match) return null
        
            for (let i = 1; i < match.length; i++) {
              const value: string = match[i] || ''
              const key = keys[i - 1]
              params[key.name] = value && key.repeatable ? value.split('/') : value
            }
        
            return params
        }
        复制代码
      • stringify
        该方法传入params对象,然后返回参数对象结合path组成的替换了参数值的path。

        function stringify(params: PathParams): string {
            let path = ''
            // for optional parameters to allow to be empty
            let avoidDuplicatedSlash: boolean = false
            for (const segment of segments) {
              if (!avoidDuplicatedSlash || !path.endsWith('/')) path += '/'
              avoidDuplicatedSlash = false
        
              for (const token of segment) {
                if (token.type === TokenType.Static) {
                  path += token.value
                } else if (token.type === TokenType.Param) {
                  const { value, repeatable, optional } = token
                  const param: string | string[] = value in params ? params[value] : ''
        
                  if (Array.isArray(param) && !repeatable)
                    throw new Error(
                      `Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
                    )
                  const text: string = Array.isArray(param) ? param.join('/') : param
                  if (!text) {
                    if (optional) {
                      // if we have more than one optional param like /:a?-static we
                      // don't need to care about the optional param
                      if (segment.length < 2) {
                        // remove the last slash as we could be at the end
                        if (path.endsWith('/')) path = path.slice(0, -1)
                        // do not append a slash on the next iteration
                        else avoidDuplicatedSlash = true
                      }
                    } else throw new Error(`Missing required param "${value}"`)
                  }
                  path += text
                }
              }
            }
        
            return path
        }
        复制代码

      经过以上复杂的步骤,就得到了一个完整的matcher对象,实属不易。

    • 3、然后处理originalMatcher属性,如果是首次赋值,则将originalMatcher赋值为matcher,后面的遍历中,不再重新赋值,而是将matcher添加到originalRecord.alias数组中。

    • 4、接着根据'children' in mainNormalizedRecord条件判断是否有子路由,如果有子路由,则遍历mainNormalizedRecord.children数组,并调用addRoute方法,参数为:children[i], matcher, originalRecord && originalRecord.children[i]

    • 5、最后调用insertMatcher(matcher)方法,将matcher添加到matchers中,并更新matcherMap

      function insertMatcher(matcher: RouteRecordMatcher) {
        let i = 0
        while (
          i < matchers.length &&
          comparePathParserScore(matcher, matchers[i]) >= 0
        )
          i++
        matchers.splice(i, 0, matcher)
        if (matcher.record.name && !isAliasRecord(matcher))
          matcherMap.set(matcher.record.name, matcher)
      }
      复制代码

    addRoute方法执行完毕。

  • resolve方法
    resolve方法返回MatcherLocation对象,该对象包含的属性为:namepathparamsmatchedmeta,作用就是根据传入的location进行路由匹配,找到location对应的matcher对应的路由信息。

    function resolve(
      location: Readonly<MatcherLocationRaw>,
      currentLocation: Readonly<MatcherLocation>
    ): MatcherLocation {
      let matcher: RouteRecordMatcher | undefined
      let params: PathParams = {}
      let path: MatcherLocation['path']
      let name: MatcherLocation['name']
    
      if ('name' in location && location.name) {
        matcher = matcherMap.get(location.name)
    
        if (!matcher)
          throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
            location,
          })
    
        name = matcher.record.name
        params = assign(
          paramsFromLocation(
            currentLocation.params,
            matcher.keys.filter(k => !k.optional).map(k => k.name)
          ),
          location.params
        )
        path = matcher.stringify(params)
      } else if ('path' in location) {
        path = location.path
    
        matcher = matchers.find(m => m.re.test(path))
        if (matcher) {
          params = matcher.parse(path)!
          name = matcher.record.name
        }
      } else {
        matcher = currentLocation.name
          ? matcherMap.get(currentLocation.name)
          : matchers.find(m => m.re.test(currentLocation.path))
        if (!matcher)
          throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
            location,
            currentLocation,
          })
        name = matcher.record.name
        params = assign({}, currentLocation.params, location.params)
        path = matcher.stringify(params)
      }
    
      const matched: MatcherLocation['matched'] = []
      let parentMatcher: RouteRecordMatcher | undefined = matcher
      while (parentMatcher) {
        matched.unshift(parentMatcher.record)
        parentMatcher = parentMatcher.parent
      }
    
      return {
        name,
        path,
        params,
        matched,
        meta: mergeMetaFields(matched),
      }
    }
    复制代码
  • removeRoute方法
    该方法接受一个参数matcherRef,参数类型可以传入路由name属性或者matcher对象,然后通过matcherRef找到对应的matcher或者matcher索引,在matcherMapmatchers中删除对应的matcher,然后再递归删除matcher.childrenmatcher.alias中对该matcher对象的引用。

    function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
      if (isRouteName(matcherRef)) {
        const matcher = matcherMap.get(matcherRef)
        if (matcher) {
          matcherMap.delete(matcherRef)
          matchers.splice(matchers.indexOf(matcher), 1)
          matcher.children.forEach(removeRoute)
          matcher.alias.forEach(removeRoute)
        }
      } else {
        let index = matchers.indexOf(matcherRef)
        if (index > -1) {
          matchers.splice(index, 1)
          if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
          matcherRef.children.forEach(removeRoute)
          matcherRef.alias.forEach(removeRoute)
        }
      }
    }
    复制代码
  • getRoutes方法

    该方法直接返回matchers数组。

    function getRoutes() {
        return matchers
    }
    复制代码
  • getRecordMatcher方法

    该方法十分简单,就是通过路由名称从matcherMap中获取对应的matcher对象。

    function getRecordMatcher(name: RouteRecordName) {
        return matcherMap.get(name)
    }
    复制代码

3、安装VueRouter实例

通过上面的分析,我们了解到:

  • 通过createRouter(options)方法创建VueRouter对象
  • 该方法的参数为一个配置对象,该对象必须提供两个属性:history和routes
    • history根据需要可以通过VueRouter提供的三种方法创建不同类型的history对象,该对象提供了路由属性和路由跳转方法。
    • routes为路由配置,VueRouter根据routes配置创建了matcher对象,通过matcher对象,可以为VueRouter实例提供了诸如添加路由、匹配路由、移除路由等路由配置相关的属性和方法。

接下来我们继续分析创建了VueRouter实例对象后,如何将VueRouter实例和Vue实例关联起来的。

在文档示例代码中,通过 app.use 将VueRouter实例对象添加到Vue实例对象中。

// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使整个应用支持路由。
app.use(router)
复制代码

app.use(router)执行时,实际上调用了VueRouter实例的install方法。

export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
  path: '/',
  name: undefined,
  params: {},
  query: {},
  hash: '',
  fullPath: '/',
  matched: [],
  meta: {},
  redirectedFrom: undefined,
}

export function createRouter(options: RouterOptions): Router {
  // shallowRef: 创建一个 ref,它跟踪自己的 .value 更改,但不会使其值成为响应式的。
  const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
  )

  let routerHistory = options.history

  const router: Router = {

    install(app: App) {
      const router = this
  
      // 在vue实例中,注册全局路由组件 RouterLink 和 RouterView
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
  
      // 在vue实例给 config.globalProperties.$router 赋值为当前 VueRouter 实例
      app.config.globalProperties.$router = router
      /**
       * 当读取 app.config.globalProperties.$route 时,
       * 返回 unref(currentRoute), 即当前路由信息,初始值为path为`/`的对象
       */
      Object.defineProperty(app.config.globalProperties, '$route', {
        enumerable: true,
        get: () => unref(currentRoute),
      })

      if (
        isBrowser &&
        // 避免在多个应用中使用router时多次 push,只有在首次install时stated才是false
        !started &&
        currentRoute.value === START_LOCATION_NORMALIZED
      ) {
        started = true
        // 跳转到浏览器url中对应的路由
        push(routerHistory.location)
      }
      
      // 复制currentRoute对象并转为响应式对象reactiveRoute,该对象可以在组件中通过inject routeLocationKey 获取
      const reactiveRoute = {} as {
        [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
          RouteLocationNormalizedLoaded[k]
        >
      }
      for (let key in START_LOCATION_NORMALIZED) {
        // @ts-ignore: the key matches
        reactiveRoute[key] = computed(() => currentRoute.value[key])
      }
      
      // 向vue实例中注入 router 相关 provider,组件使用 inject 来接收这几个值
      // 这几个值的属性都是 Symbol 类型,可以通过 src/injectionSymbols.ts 源码了解
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(reactiveRoute))
      app.provide(routerViewLocationKey, currentRoute)
  
      // 拦截vue实例unmount方法,当vue实例卸载时,重置某些属性和事件解绑,然后再执行vue实例卸载方法
      let unmountApp = app.unmount
      installedApps.add(app)
      app.unmount = function () {
        installedApps.delete(app)
        if (installedApps.size < 1) {
          removeHistoryListener()
          currentRoute.value = START_LOCATION_NORMALIZED
          started = false
          ready = false
        }
        unmountApp()
      }
    },
  }
}
复制代码

总的来说,install方法做了以下几件事:

  • 注册两个路由组件为vue全局组件。
  • app.config.globalProperties上添加$router$route属性,$router是VueRouter实例对象本身,$route为当前location对应的路由对象。
  • 如果是首次install,则通过push方法跳转到url对应的路由。
  • 注入三个VueRouter相关的provider。
  • 拦截vue实例的unmount方法,在unmount方法调用之前,先执行VueRouter相关的卸载工作。

四、总结

在阅读源码的过程中,Typescript使阅读源码相对少吃点力,通过类型定义对理解每个变量的作用提供了很好的帮助。从VueRouter实例对象的创建开始分析,对路由的基本实现原理有了一定的理解,也认识到要实现一个功能完善的路由功能,不是一件很简单的事情,需要考虑的边界问题很多。由于能力有限,也还存在很多细节不能够理解。阅读源码,始终是让人受益匪浅的一件事,不管目前能够理解多少。

本文分析的是VueRouter的基础部分,后续还会继续分析进阶部分,一起学习,一起进步吧!

其他

  • 文档生成工具:vitepress
  • 测试:单元测试使用 jest,e2e测试使用 nightwatch
  • changelog生成:conventional-changelog-cli
  • 提交代码检查:lint-staged
  • Typescript: YYDS
  • 还有很多需要填坑的地方,比如发现了一些TODO

history api 浏览器兼容性

现在浏览器基本都支持history api,VueRouter中的push、replace、go方法,都基于history api。

image.png

本人能力有限,可能会有一些理解上的错误,欢迎走过路过的各位大佬不吝赐教,感谢!?

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享