vue-router

47 阅读27分钟

router前置知识 以及 vue-router源码部分解析

URI和URL的简介

URI

统一资源标识符,允许用户对网络中的资源通过特定的协议进行交互操作

  • uniform:规定的统一的语法格式,以方便处理多种不同类型的资源,而无需根据上下文环境来识别资源的类型
  • resource:表示可标识的任何资源,资源不仅可以作为单一对象,也可以作为多个对象的集合体
  • Identifier:表示可标识对象,也被称作标识符

URI是由某个协议方案表示的资源的定位标识符,协议方案是指访问资源时使用的协议类型名称,HTTP就是一种协议类型名称,除此之外,还有FTP,file,TELNET等的协议方案

通用语法组成

URI = schme:[//authority]path[?query][fragment]

authority组成

authority = [userInfo]host[sport]

在authority当中,

  • userinfo是登录信息,通常是指定的用户名和密码,当作身份认证凭证使用,可选项
  • 服务器地址host使用需要指定访问的服务器地址,地址可以DNS解析
  • port:服务器连接的端口号,可选项,不指定就是默认的端口号

在URI当中,scheme是协议方案名,在使用HTTPS或者HTTP等协议方案名是不区分大小写,data:指定的脚本程序或者数据

path是带层次的文件路径,访问特定的资源

query是查询字符串,针对指定路径的文件资源

fragment片段标识符,通常标记已经获取资源的子资源

URL

统一资源定位器,是URI的一种,标识网络资源

语法定义和URI是一样的

浏览器URI编码

百分号编码

使用百分号进行编码。对于需要编码的字符串,将其表示成两个16进制数字,在前面放置%,替换字符相应的位置进行编码

特殊意思的字符,?表示查询,#表示片段标识,没有特殊意义的就进行编码操作

encodeURI

对于ASCII字符,对数字和字母进行编码,但是不对标点符号进行编码

对于非ASCII字符,在每个字节前面放置转义字符,放置对应的位置

encodeURIComponent

这个的假定参数是URI的一部分,(比如:协议名,主机名,路径或者查询字符串)将转移encodeURI转移不到的符号,好处是:如果字符串被发送到服务端进行解析,那么服务端会把紧跟在%后面的字节当成普通的字节,而不会将他当成各个参数或者键值对的分隔符

相比这个,encodeURI被用作对完整的URI进行编码,encodeURIComponent用作对URI的一个组件中的一个片段进行编码

两者的区别:

encodeURL:

  • 用来编码整个URL,只处理“非法字符”,保留URL的结构(:,/,之类的)

encodeURLComponent:

  • 用来编码URL的某一个片段,会把所有的特殊字符都转移,包括encodeURL不包括的

简单的对比

字符 encodeURL encodeURLComponent 说明

空格%20%20都编码为空格
:不编码不编码保留协议部分
/不编码%2FencodeURI保留路径结构
?不编码%3FencodeURI保留查询开始符
=不编码%3DencodeURIComponent编码参数值中的=
&不编码%26防止被误认为参数分隔符
#不编码%23防止被误认为锚点
中文%E...%E...都会编码

使用场景:

  • 编码整个URL,使用encodeURL
  • 编码参数值(部分场景),使用encodeURLcomponent

浏览器记录

是指浏览器中各页面的导航记录,在现代浏览器当中,浏览器当中没有直接API可以获取,浏览器记录由浏览器统一管理,并不属于某个具体的页面,和页面形式及其内存无关

window.history中包含很多属性,这一特性包括pushState,replaceState|popState事件 分别用来添加和修改历史记录条目

history.pushState

基本用法

用来无刷新增增加历史栈记录,调用pushState方法可以修改浏览器路径。

使用需要三个参数,状态对象,标题(忽略),可选择的URL

当设置第三个参数URL的时候,可以改变浏览器的URL且不会刷新浏览器,

第一个参数是需要传进来的状态,状态的类型可以是任意类型,设置第一个参数之后我们就可以使用history.state进行读取

历史栈的变化

pushState调用会引起历史栈的变化,浏览器通常会维护一个用户访问过的历史栈,使用前进或者后退进行访问(window.history.go),调用pushState方法,历史栈的内容会被修改,会添加历史栈的栈记录,同时也会改变指针的指向

是向栈顶放置新的历史记录,并使之成为新的栈顶

第三个参数不传入,也会增加长度

history.replaceState

基本用法

pushState使用方法类似,但是不是新建而是修改当前的历史记录,length不会发生变化

历史栈变化

replaceState不会改变历史栈的数量,改变的当前栈指针指向的内容

通过相对路径添加和修改浏览器的记录

上面的两个参数除了支持绝对路径导航之外,还支持相对路径导航,会自动的进行路径的补全,但是注意补全路径的正确性

在base元素存在的情况下添加和修改浏览器的记录

当HTML元素当中base元素存在使用base的href的值作为基准路径

浏览器跳转

内置的window存在history对象和location对象,可以进行导航的跳转

window.history.go

用来完成用户历史记录中向后向前的跳转,传入的参数可以是正数和负数,0代表刷新,仅仅移动指针

window.history.forward

作为跳转当前栈指针所指钱一个记录的方法,栈指针向前移动一位,等同于go(1)

注意:前进是否刷新取决于历史栈中的栈记录,push操作不会进行书信

window.history.back

go(-1)

window.location.href

将window.location.href导航会产生一个新的历史记录,将字符串设置到window.location和设置到window.location.href行为一致,这个设置操作会刷新页面并重新加载URL所指定的内容

是相当于增加了历史栈,对原来的历史栈没影响

操作流程:中断当前页面的执行,发送新的HTTP请求,完全重新加载页面,创建新的历史记录

window.location.hash

同样会设产生新的历史栈记录,如果设置location.hash和浏览器的URL的地址相同,不会触发任何事件,也不会添加历史记录

想要不产生新的历史栈记录,使用replace实现

window.llocation.replace

替换当前栈的记录,并刷新页面,重新加载导入的URL,和设置href不同,旧页面的不会保存,不能进行后退操作

浏览器相关的事件

popState

pushState或者replaceState产生历史栈记录当中,移动指针或者前进后退将会触发popState事件,可以通过window.addListener进行监听

监听函数的参数是对应的popState事件的事件对象,

使用pushHistory或者replaceState都不会触发popState事件,前进后退或者go,back之类的会触发事件

除了可以从popState事件对象获取当前的state对象,还可以通过history获取当前的对象,就是读取history.state的值即可

注意:有的浏览器对popState实现不一致,当网页加载完成之后,可能不会触发,我们可以使用history获取状态对象,而不是通过popState获取,在事件当中更改event.state,history.state不会改变,但是直接更改就会改变

当通过href设置的hash值,不管设置前后是否相同,都会触发事件

hashChange

用来监听浏览器hash值的变化,使用addListener即可进行监听

hashChange事件可以通过设置location.hash,在地址栏中手动的修改hash,调用window.history.go,在浏览器前进或者后退触发

可以获取事件对象HashChangeEvent,获得相应的继承属性

注意:history.pushState不会触发hashChange事件,即使前后导航的URL仅仅hash不同

手动触发事件

对于popState事件,如果调用pushState方法,则replaceState方法不会触发,仅仅移动指针才会触发

但是我们通过dispatchEvent方法,也能实现不移动指针便可控制popState方法的触发,调用这个dispatchEvent方法,方法的返回值是事件的取消状态

详细解释一下dispatch和addListener的关系:

  • dispatch相当于触发操作,addlistener相当于接收这个操作并执行相应的回调,所有的listen都会收到对应的消息
  • 使用dispatch之前还是需要手动的创建事件,dispatch只能触发事件,没有创建的过程

使用场景:

  • 自动化测试
  • 程序化表单提交
  • 自定义组件通信
  • 模拟用户交互
  • 集成第三方库

history

主要了解三种模式定义的区别

在vue-router的的使用下,3版本之前使用history实现,但是最新版本下已经实现了自己的history状态管理

相对应的关系

Vue Router 功能history 库对应说明
createWebHistory()createBrowserHistory()HTML5 History API
createWebHashHistory()createHashHistory()Hash 模式
createMemoryHistory()createMemoryHistory()服务端渲染

这里我们可以直接学习vue-router中自己实现的history状态管理

核心的接口设计 实现主要的接口和方法

export interface RouterHistory{
    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 
    back(): void
    forward(): void
    
    listen(callback: NavigationCallback): () => void
    destory(): void
}

三种History的实现

webHistory

HTML5 History API

接收一个参数,函数可以接收一个base字符串的参数,这个参数提供了一个基础的路径,在createWebHistory会首先调用normalizeBase函数,将base标准化

在没有配置base的情况下,在浏览器环境下会尝试获取标签中的href属性作为base,如果没有或者标签的href属性没有值,base最终使用/,然后对base进行操作,去除HTTP的部分,最终目的通过base+fullPath的形式建立一个href,base标准化之后,会使用historyNavigationhistoryListener变量

export function createWebHistory(base?: string): RouterHistory {
  base = normalizeBase(base);
  
  const historyNavigation = useHistoryStateNavigation(base);
  const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace
  );
  
  function go(delta: number, triggerListeners = true) {
    if (!triggerListeners) historyListeners.pauseListeners();
    window.history.go(delta);
  }
  
  return {
    base,
    location: historyNavigation.location,
    state: historyNavigation.state,
    
    push: historyNavigation.push,
    replace: historyNavigation.replace,
    
    go,
    back: () => go(-1),
    forward: () => go(1),
    
    listen: historyListeners.listen,
    destroy: historyListeners.teardown,
  };
}

HashHistory

hash模式

export function createWebHashHistory(base?: string): RouterHistory {
  base = location.host ? base || location.pathname : '';
  // 确保 base 包含 #
  if (!base.includes('#')) base += '#';
  
  return createWebHistory(base);
}

MemoryHistory

export function createMemoryHistory(base: string = ''): RouterHistory {
  let currentLocation: HistoryLocation = normalizeBase(base);
  let currentState: HistoryState = {};
  let listeners: NavigationCallback[] = [];
  let position = 0;
  
  function setLocation(
    to: HistoryLocation,
    state: HistoryState,
    replace: boolean
  ) {
    const from = currentLocation;
    currentLocation = to;
    currentState = state;
    
    if (!replace) position++;
    
    listeners.forEach(listener => {
      listener(currentLocation, from, { delta: replace ? 0 : 1 });
    });
  }
  
  return {
    base,
    location: currentLocation,
    state: currentState,
    
    push(to: HistoryLocation, data?: HistoryState) {
      setLocation(to, data || {}, false);
    },
    
    replace(to: HistoryLocation, data?: HistoryState) {
      setLocation(to, data || {}, true);
    },
    
    go(delta: number) {
      const newPosition = position + delta;
      if (newPosition >= 0 && newPosition < position) {
        position = newPosition;
        // 这里会模拟历史记录跳转
      }
    },
    
    listen(callback: NavigationCallback) {
      listeners.push(callback);
      return () => {
        const index = listeners.indexOf(callback);
        if (index > -1) listeners.splice(index, 1);
      };
    },
    
    destroy() {
      listeners = [];
    }
  };
}

createWebHistorycreateWebHashHistory一样,createMemoryHistory同样返回一个routerHistory类型的对象,和前面两个方法不同的是,这个方法维护一个队列queue和 一个position保证历史存储的正确性,主要是SSR中正确的使用

核心函数的实现

useHistoryStateNavigation

状态导航管理:

主要负责维护和操作浏览器的历史记录状态,主要的包含的部分就是

  • location 当前路径

    • location:包含一个value属性值,value值是createCurrentLocation() 方法的返回值

    • createCurrentLocation()方法作用是通过window.location创建一个规范化的history location。,方法接收两个参数:经过标准化base字符串和一个window.location对象

      function createCurrentLocation(
        base: string,
        location: Location
      ): HistoryLocation {
        const { pathname, search, hash } = location
        // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
        // 从base中获取#的索引
        const hashPos = base.indexOf('#')
        // 如果base中包含#
        if (hashPos > -1) {
          // 如果hash包含base中的#后面部分,slicePos为base中#及后面字符串的的长度,否则为1
          let slicePos = hash.includes(base.slice(hashPos))
            ? base.slice(hashPos).length
            : 1
          // 从location.hash中获取path,/#add, #add
          let pathFromHash = hash.slice(slicePos)
          // 在开头加上/,形成/#的格式
          if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash
          // stripBase(pathname, base):将pathname去除base部分
          return stripBase(pathFromHash, '')
        }
        // 如果base中不包含#,把pathname中的base部分删除
        const path = stripBase(pathname, base)
        return path + search + hash
      }
      

      将base删除拿到的就是location

  • state 当前状态

    • 一个包含value的属性对象,value存储的是当前的history.state
  • push 添加新的记录

    • 向历史记录当中添加一条记录,在push的过程中你会发现调用了两次changeLiocation,目的是为了记录当前页面在滚动位置,如果使用history.back()或者前进|后退会返回对应的位置,为了不在历史栈中保存新的记录,会进行替换
  • replace 替换当前的记录

状态的数据结构historyState

interface HistoryState{
    position: number, // 在历史栈当中的位置
    back: string|null, // 上一个路径
    forward: string|null, // 下一个路径
    timestamp: number, //时间戳
    scollPostion?:{ // 滚动的位置
        x: number,
        y: number,
    }
    [key:stirng]: any  //自定义数据
}
// 这里的base已经是处理之后的路径了
function useHistoryStateNavigation(base: string) {
  const { history, location } = window;
  
  // 当前状态
  const currentLocation: HistoryLocation = normalizeLocation(
    location,
    base
  );
  
  // 创建初始状态
  const currentState: HistoryState = {
    position: history.length - 1,
    ...(history.state || {})
  };
  
  // 改变历史记录
  function changeLocation(
    to: HistoryLocation,
    state: HistoryState,
    replace: boolean
  ): void {
    const url = createBaseLocation(base, to);
    try {
      // 使用 History API
      if (replace) {
        history.replaceState(state, '', url);
      } else {
        history.pushState(state, '', url);
      }
    } catch (err) {
      // 降级方案:对于某些浏览器限制
      if (replace) {
        location.replace(url);
      } else {
        location.assign(url);
      }
    }
  }
  
    // 添加新的历史记录
  function push(to: HistoryLocation, data?: HistoryState) {
    const state: HistoryState = {
      ...currentState,
      ...data,
      position: currentState.position + 1,
    };
    
    changeLocation(to, state, false);
    currentLocation.value = to;
    currentState.value = state;
  }
  
    // 替换当前的记录
  function replace(to: HistoryLocation, data?: HistoryState) {
    const state: HistoryState = {
      ...currentState,
      ...data,
      position: currentState.position,
    };
    
    changeLocation(to, state, true);
    currentLocation.value = to;
    currentState.value = state;
  }
  
  return {
    location: currentLocation,
    state: currentState,
    push,
    replace,
  };
}

useHistoryListeners

主要是用来管理浏览器的历史记录监听的核心函数,主要处理PopState事件和页面卸载之前的状态保存

接收四个参数,base(标准化的base),historyState,currentLocation,replace,(后面的三个参数是useHistoryStateNavigation的返回值),在listeners当中,会对popStatebeforeunload进行监听

返回参数的类型对象包含三个属性:

  • pauseListeners:暂停监听的函数
  • listen:接受一个回调函数,并返回一个删除监听的函数,这个函数会加入到listener数组当中,并向teardowns数组中添加卸载函数
  • destory:销毁函数,清空listenersteardowns。移除popState``beforeunload监听
function useHistoryListeners(
  base: string,
  historyState: ValueContainer<StateEntry>,
  currentLocation: ValueContainer<HistoryLocation>,
  replace: RouterHistory['replace']
) {
  let listeners: NavigationCallback[] = []
  let teardowns: Array<() => void> = []
  let pauseState: HistoryLocation | null = null
​
  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
​
      // 如果暂停监听了,则直接return,同时pauseState赋为null
      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,
      })
    })
  }
​
  function pauseListeners() {
    pauseState = currentLocation.value
  }
​
  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
  }
​
  function beforeUnloadListener() {
    const { history } = window
    if (!history.state) return
    // 当页面关闭时记录页面滚动位置
    history.replaceState(
      assign({}, history.state, { scroll: computeScrollPosition() }),
      ''
    )
  }
​
  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 {
    pauseListeners,
    listen,
    destroy,
  }
}

现在已经实现了两个核心的函数,我们回到createWebHistory当中,创建之后声明go函数,这个函数接受两个的变量,delta历史记录移动步数,triggerListener是是否触发监听的函数

function go(delta: number, triggerListeners = true) {
  if (!triggerListeners) historyListeners.pauseListeners()
  history.go(delta)
}

最后创建一个routerHistory对象,并将其返回

const routerHistory: RouterHistory = assign(
  {
    location: '',
    base,
    go,
    createHref: createHref.bind(null, base),
  },
  historyNavigation,
  historyListeners
)
​
// 拦截routerHistory.location,使routerHistory.location返回当前路由地址
Object.defineProperty(routerHistory, 'location', {
  enumerable: true,
  get: () => historyNavigation.location.value,
})
​
// 拦截routerHistory.state,使routerHistory.state返回当前的的history.state
Object.defineProperty(routerHistory, 'state', {
  enumerable: true,
  get: () => historyNavigation.state.value,
})
​
return routerHistory

Matcher

什么是matcher?

vue-router当中,每一个我们定义的的路由都会解析成一个对应的matcherrouteRecordMatcher类型),路由的增删改查,都会依赖matcher实现

route和matcher的区别

路由是配置对象,具有映射关系(匹配)

matcher是匹配器,负责解析和匹配路由(处理)和进行路由管理

基本的功能:

  • 管理路由规则:维护所有的路由配置信息,包括路径,组件和嵌套路由
  • 路由匹配

    • 根据当前的路径(URL)进行访问找到应该显示的组件
  • 生成URL

    • 根据当前的路由名称和参数生成对应的URL路径
  • 处理路由动态变化

    • 支持添加,删除路由规则(动态路由)

工作原理:

  1. 初始化阶段:

    • 当创建vue router实例的时候,会根据传入的routes配置生成一个matcher实例
    • matcher会递归的解析路由配置,构建内部的数据结构
    • 处理动态的路由参数,嵌套路由,命名路由等特殊配置
  2. 路由匹配的过程

    • 当URL发生变化的时候,matcher会接收新的路径进行匹配
    • 从当前的根路由开始,按照定义的路由规则逐层匹配URL片段
    • 优先匹配静态路由,在动态处理路由参数和通配符
    • 最终返回匹配到的路由记录数组
  3. 动态路由处理:

    • 通过matcher.addRoutes()| router.addRoute()可以动态添路由
    • matcher会更新拆路由规则数据结构-,使得新的路由立即生效

createRouteMatcher

createRouter当中会通过createRouteMatcher创建一个matcher

这个函数接收两个参数routes,globalOptions其中routes为我们定义的路由表,也就是在createRouter时候传入的options.routes,而globalOpotions就是createRouter中的options

createRouteMatcher中声明了两个变量matchersmatcherMap,用来存储通过路由表解析的matcher,然后遍历routes,对每个元素调用addRoute方法,最后返回一个对象,这个对象有addRouteresolveremoveRoutegetRoute,getRecordMatcher几个属性,每个属性对应一个函数

export 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 {
    // ...
  }
​
  routes.forEach(route => addRoute(route))
​
  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

addRoute

接受三个参数,record(新增路由),parent(父matcher),originalRecord(原始matcher

function addRoute(
  record: RouteRecordRaw,
  parent?: RouteRecordMatcher,
  originalRecord?: RouteRecordMatcher
) {
  // used later on to remove by name
  const isRootAdd = !originalRecord
  // 标准化化路由记录
  const mainNormalizedRecord = normalizeRouteRecord(record)
  // aliasOf表示此记录是否是另一个记录的别名
  mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
  const options: PathParserOptions = mergeOptions(globalOptions, record)
  // 声明一个记录的数组用来处理别名
  const normalizedRecords: typeof mainNormalizedRecord[] = [
    mainNormalizedRecord,
  ]
  // 如果record设置了别名
  if ('alias' in record) {
    // 别名数组
    const aliases =
      typeof record.alias === 'string' ? [record.alias] : record.alias!
    // 遍历别名数组,并根据别名创建记录存储到normalizedRecords中
    for (const alias of aliases) {
      normalizedRecords.push(
        assign({}, mainNormalizedRecord, {
          components: originalRecord
            ? originalRecord.record.components
            : mainNormalizedRecord.components,
          path: alias,
          // 如果有原始记录,aliasOf为原始记录,如果没有原始记录就是它自己
          aliasOf: originalRecord
            ? originalRecord.record
            : mainNormalizedRecord,
        }) as typeof mainNormalizedRecord
      )
    }
  }
​
  let matcher: RouteRecordMatcher
  let originalMatcher: RouteRecordMatcher | undefined
​
  // 遍历normalizedRecords
  for (const normalizedRecord of normalizedRecords) {
​
    // 处理normalizedRecord.path为完整的path
    const { path } = normalizedRecord
    // 如果path不是以/开头,那么说明它不是根路由,需要拼接为完整的path
    // { path: '/a', children: [ { path: 'b' } ] } -> { path: '/a', children: [ { path: '/a/b' } ] }
    if (parent && path[0] !== '/') {
      const parentPath = parent.record.path
      const connectingSlash =
        parentPath[parentPath.length - 1] === '/' ? '' : '/'
      normalizedRecord.path =
        parent.record.path + (path && connectingSlash + path)
    }
​
    // 提示*应使用正则表示式形式
    if (__DEV__ && normalizedRecord.path === '*') {
      throw new Error(
        'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
          'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
      )
    }
​
    // 创建一个路由记录匹配器
    matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
​
    // 检查是否有丢失的参数
    if (__DEV__ && parent && path[0] === '/')
      checkMissingParamsInAbsolutePath(matcher, parent)
​
    // 如果有originalRecord,将matcher放入原始记录的alias中,以便后续能够删除
    if (originalRecord) {
      originalRecord.alias.push(matcher)
      // 检查originalRecord与matcher中动态参数是否相同
      if (__DEV__) {
        checkSameParams(originalRecord, matcher)
      }
    } else { // 没有originalRecord
      // 因为原始记录索引为0,所以originalMatcher为有原始记录所产生的matcher
      originalMatcher = originalMatcher || matcher
      // 如果matcher不是原始记录产生的matcher,说明此时matcher是由别名记录产生的,此时将matcher放入originalMatcher.alias中
      if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
      // 如果命名并且仅用于顶部记录,则删除路由(避免嵌套调用)
      if (isRootAdd && record.name && !isAliasRecord(matcher))
        removeRoute(record.name)
    }
​
    // 遍历children,递归addRoute
    if ('children' in mainNormalizedRecord) {
      const children = mainNormalizedRecord.children
      for (let i = 0; i < children.length; i++) {
        addRoute(
          children[i],
          matcher,
          originalRecord && originalRecord.children[i]
        )
      }
    }
​
    originalRecord = originalRecord || matcher
    // 添加matcher
    insertMatcher(matcher)
  }
​
  // 返回一个删除原始matcher的方法
  return originalMatcher
    ? () => {
        removeRoute(originalMatcher!)
      }
    : noop
}

addRoute当中,会对record进行标准化的处理,如果原来存在原始的matcher,也就是originRecord,说明此时要添加的路由时另一记录的别名,这时会将originalRecord.record传入mainNormalizeRecord.aliasOf当中(当设置了路由别名的情况下就会这样)

const isRootAdd = !originalRecord
// 标准化化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// aliasOf表示此记录是否是另一个记录的别名
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// 声明一个记录的数组用来处理别名
const normalizedRecords: typeof mainNormalizedRecord[] = [
  mainNormalizedRecord,
]

然后会遍历record的别名,向normalizedRecords中添加由别名产生的路由

if ('alias' in record) {
  // 别名数组
  const aliases =
    typeof record.alias === 'string' ? [record.alias] : record.alias!
  // 遍历别名数组,并根据别名创建记录存储到normalizedRecords中
  for (const alias of aliases) {
    normalizedRecords.push(
      assign({}, mainNormalizedRecord, {
        components: originalRecord
          ? originalRecord.record.components
          : mainNormalizedRecord.components,
        path: alias,
        // 如果有原始记录,aliasOf为原始记录,如果没有原始记录就是它自己
        aliasOf: originalRecord
          ? originalRecord.record
          : mainNormalizedRecord,
      }) as typeof mainNormalizedRecord
    )
  }
}

后面会遍历normalizedRocords,遍历过程中,会首先处理成完整的path,然后通过createRouteMatcher方法创建一个matcher,如果matcher是由别名产生的,那么matcher会被加入由原始记录产生的matcher的alias属性当中,然后会遍历normalizedRecord的children属性,递归调用addRoute方法,最后,调用insertMatcher添加新创建的matcher

for (const normalizedRecord of normalizedRecords) {
​
  // 处理normalizedRecord.path为完整的path
  const { path } = normalizedRecord
  // 如果path不是以/开头,那么说明它不是根路由,需要拼接为完整的path
  // { path: '/a', children: [ { path: 'b' } ] } -> { path: '/a', children: [ { path: '/a/b' } ] }
  if (parent && path[0] !== '/') {
    const parentPath = parent.record.path
    const connectingSlash =
      parentPath[parentPath.length - 1] === '/' ? '' : '/'
    normalizedRecord.path =
      parent.record.path + (path && connectingSlash + path)
  }
​
  // 提示*应使用正则表示式形式
  if (__DEV__ && normalizedRecord.path === '*') {
    throw new Error(
      'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
        'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.'
    )
  }
​
  // 创建一个路由记录匹配器
  matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
​
  // 检查是否有丢失的参数
  if (__DEV__ && parent && path[0] === '/')
    checkMissingParamsInAbsolutePath(matcher, parent)
​
  // 如果有originalRecord,将matcher放入原始记录的alias中,以便后续能够删除
  if (originalRecord) {
    originalRecord.alias.push(matcher)
    // 检查originalRecord与matcher中动态参数是否相同
    if (__DEV__) {
      checkSameParams(originalRecord, matcher)
    }
  } else { // 没有originalRecord
    // 因为原始记录索引为0,所以originalMatcher为有原始记录所产生的matcher
    originalMatcher = originalMatcher || matcher
    // 如果matcher不是原始记录产生的matcher,说明此时matcher是由别名记录产生的,此时将matcher放入originalMatcher.alias中
    if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
    // 如果存在record.name并且是顶部记录,则删除路由(避免嵌套调用)
    if (isRootAdd && record.name && !isAliasRecord(matcher))
      removeRoute(record.name)
  }
​
  // 遍历children,递归addRoute
  if ('children' in mainNormalizedRecord) {
    const children = mainNormalizedRecord.children
    for (let i = 0; i < children.length; i++) {
      addRoute(
        children[i],
        matcher,
        originalRecord && originalRecord.children[i]
      )
    }
  }
  // 如果originalRecord是方法传入的,那么originalRecord继续保持
  // 如果originalRecord方法未传入。由于原始的matcher总是在索引为0的位置,所以如果有别名,那么这些别名的原始matcher会始终指向索引为0的位置
  originalRecord = originalRecord || matcher
  // 添加matcher
  insertMatcher(matcher)
}

token

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

token具有三种类型

  • TokenStatic:一种静态的token,不可变
  • TokenParam:参数token
  • TokenGroup:分组token
  1. /one/two/three 对应的token数组

    [
      [{ type: TokenType.Static, value: 'one' }],
      [{ type: TokenType.Static, value: 'two' }],
      [{ type: TokenType.Static, value: 'three' }]
    ]
    
  2. /user/:id 对应的token数组

    [
      [
        {
          type: TokenType.Static,
          value: 'user',
        },
      ],
      [
        {
          type: TokenType.Param,
          value: 'id',
          regexp: '',
          repeatable: false,
          optional: false,
        }
      ]
    ]
    
  3. /:id(\d+)new 对应的token数组

    [
      [
        {
          type: TokenType.Param,
          value: 'id',
          regexp: '\d+',
          repeatable: false,
          optional: false,
        },
        {
          type: TokenType.Static,
          value: 'new'
        }
      ]
    ]
    

我们可以看出token数组详细的描述了path的每一级路由的生成,通过token我们能够知道他是一个一级路由,并且他的这次的路由是由两部分组成,其中一部分是参数部分,另一部分是静态的,并且参数部分还说明了参数的正则,以及是否重复,是否可选配置

接下来就是tokanizePath是如何将path转化成token

tokenizePath

tokenizePath的过程就是利用有限状态机生成token数组,解析path生成对应的token数组

生成对应的数据和对应的节点

  • 将路径字符串分解成有意义的令牌
  • 模式识别,识别静态端,动态参数,可选参数,通配符
  • 结构解析:理解路径的语法结构
export const enum TokenType {
  Static,
  Param,
  Group,
}
​
const ROOT_TOKEN: Token = {
  type: TokenType.Static,
  value: '',
}
​
export function tokenizePath(path: string): Array<Token[]> {
  if (!path) return [[]]
  if (path === '/') return [[ROOT_TOKEN]]
  // 如果path不是以/开头,抛出错误
  if (!path.startsWith('/')) {
    throw new Error(
      __DEV__
        ? `Route paths should start with a "/": "${path}" should be "/${path}".`
        : `Invalid path "${path}"`
    )
  }
​
  function crash(message: string) {
    throw new Error(`ERR (${state})/"${buffer}": ${message}`)
  }
​
  // token所处状态
  let state: TokenizerState = TokenizerState.Static
  // 前一个状态
  let previousState: TokenizerState = state
  const tokens: Array<Token[]> = []
  //  声明一个片段,该片段最终会被存入tokens中
  let segment!: Token[]
​
  // 添加segment至tokens中,同时segment重新变为空数组
  function finalizeSegment() {
    if (segment) tokens.push(segment)
    segment = []
  }
​
  let i = 0
  let char: string
  let buffer: string = ''
  // custom regexp for a param
  let customRe: string = ''
​
  // 消费buffer,即生成token添加到segment中
  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
  }
​
  // 遍历path
  while (i < path.length) {
    char = path[i++]
​
    // path='/\:'
    if (char === '\' && state !== TokenizerState.ParamRegExp) {
      previousState = state
      state = TokenizerState.EscapeNext
      continue
    }
​
    switch (state) {
      case TokenizerState.Static:
        if (char === '/') {
          if (buffer) {
            consumeBuffer()
          }
          // char === /时说明已经遍历完一层路由,这时需要将segment添加到tokens中
          finalizeSegment()
        } else if (char === ':') { // char为:时,因为此时状态是TokenizerState.Static,所以:后是参数,此时要把state变为TokenizerState.Param
          consumeBuffer()
          state = TokenizerState.Param
        } else { // 其他情况拼接buffer
          addCharToBuffer()
        }
        break
​
      case TokenizerState.EscapeNext:
        addCharToBuffer()
        state = previousState
        break
​
      case TokenizerState.Param:
        if (char === '(') { // 碰到(,因为此时state为TokenizerState.Param,说明后面是正则表达式,所以修改state为TokenizerState.ParamRegExp
          state = TokenizerState.ParamRegExp
        } else if (VALID_PARAM_RE.test(char)) {
          addCharToBuffer()
        } else { // 例如/:id/one,当遍历到第二个/时,消费buffer,state变为Static,并让i回退,回退后进入Static
          consumeBuffer()
          state = TokenizerState.Static
          if (char !== '*' && char !== '?' && char !== '+') i--
        }
        break
​
      case TokenizerState.ParamRegExp: 
        // it already works by escaping the closing )
        // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
        // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
        // is this really something people need since you can also write
        // /prefix_:p()_suffix
        if (char === ')') {
          // 如果是\)的情况,customRe = customRe去掉\ + char
          if (customRe[customRe.length - 1] == '\')
            customRe = customRe.slice(0, -1) + char
          else state = TokenizerState.ParamRegExpEnd // 如果不是\)说明正则表达式已经遍历完
        } else {
          customRe += char
        }
        break
​
      case TokenizerState.ParamRegExpEnd: // 正则表达式已经遍历完
        // 消费buffer
        consumeBuffer()
        // 重置state为Static
        state = TokenizerState.Static
        // 例如/:id(\d+)new,当遍历到n时,使i回退,下一次进入Static分支中处理
        if (char !== '*' && char !== '?' && char !== '+') i--
        customRe = ''
        break
​
      default:
        crash('Unknown state')
        break
    }
  }
​
  // 如果遍历结束后,state还是ParamRegExp状态,说明正则是没有结束的,可能漏了)
  if (state === TokenizerState.ParamRegExp)
    crash(`Unfinished custom RegExp for param "${buffer}"`)
​
  // 遍历完path,进行最后一次消费buffer
  consumeBuffer()
  // 将segment放入tokens
  finalizeSegment()
​
  // 最后返回tokens
  return tokens
}
tokenToParser

函数接收一个token数组,和一个可选的extraOptions,在函数中会构造出path对应的正则表达式,动态参数列表keys,token对应的分数,最后返回一个对象

  • 正则生成:根据令牌创建匹配用的正则表达式
  • 参数映射:建立参数名和正则捕获组的对应关系
  • 优先级计算:确定路由匹配的优先顺序
const enum PathScore {
  _multiplier = 10,
  Root = 9 * _multiplier, // 只有一个/时的分数
  Segment = 4 * _multiplier, // segment的基础分数
  SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
  Static = 4 * _multiplier, // type=TokenType.Static时的分数
  Dynamic = 2 * _multiplier, // 动态参数分数 /:someId
  BonusCustomRegExp = 1 * _multiplier, // 用户自定义正则的分数 /:someId(\d+) 
  BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
  BonusRepeatable = -2 * _multiplier, // 当正则是可重复时的分数 /:w+ or /:w*
  BonusOptional = -0.8 * _multiplier, // 当正则是可选择时的分数 /:w? or /:w*
  // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
  BonusStrict = 0.07 * _multiplier, // options.strict: true时的分数
  BonusCaseSensitive = 0.025 * _multiplier, // options.strict:true时的分数
}
const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
  sensitive: false,
  strict: false,
  start: true,
  end: true,
}
const REGEX_CHARS_RE = /[.+*?^${}()[]/\]/g
export function tokensToParser(
  segments: Array<Token[]>,
  extraOptions?: _PathParserOptions
): PathParser {
  const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
​
  // 除了根段“/”之外,分数的数量与segments的长度相同
  const score: Array<number[]> = []
  // 正则的字符串形式
  let pattern = options.start ? '^' : ''
  // 保存路由中的动态参数
  const keys: PathParserParamKey[] = []
​
  for (const segment of segments) {
    // 用一个数组保存token的分数,如果segment.length为0,使用PathScore.Root
    const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
​
    // options.strict代表是否禁止尾部/,如果禁止了pattern追加/
    if (options.strict && !segment.length) pattern += '/'
    // 开始遍历每个token
    for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
      const token = segment[tokenIndex]
      // 当前子片段(单个token)的分数:基础分数+区分大小写 ? PathScore.BonusCaseSensitive : 0
      let subSegmentScore: number =
        PathScore.Segment +
        (options.sensitive ? PathScore.BonusCaseSensitive : 0)
​
      if (token.type === TokenType.Static) {
        // 在开始一个新的片段(tokenIndex !== 0)前pattern需要添加/
        if (!tokenIndex) pattern += '/'
        // 将token.value追加到pattern后。追加前token.value中的.、+、*、?、^、$等字符前面加上\
        // 关于replace,参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace
        pattern += token.value.replace(REGEX_CHARS_RE, '\$&')
        subSegmentScore += PathScore.Static
      } else if (token.type === TokenType.Param) {
        const { value, repeatable, optional, regexp } = token
        // 添加参数
        keys.push({
          name: value,
          repeatable,
          optional,
        })
        const re = regexp ? regexp : BASE_PARAM_PATTERN
        // 用户自定义的正则需要验证正则的正确性
        if (re !== BASE_PARAM_PATTERN) {
          subSegmentScore += PathScore.BonusCustomRegExp
          // 使用前确保正则是正确的
          try {
            new RegExp(`(${re})`)
          } catch (err) {
            throw new Error(
              `Invalid custom RegExp for param "${value}" (${re}): ` +
                (err as Error).message
            )
          }
        }
​
        // /:chapters*
        // 如果是重复的,必须注意重复的前导斜杠
        let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`
​
        // prepend the slash if we are starting a new segment
        if (!tokenIndex)
          subPattern =
            // avoid an optional / if there are more segments e.g. /:p?-static
            // or /:p?-:p2
            optional && segment.length < 2
              ? `(?:/${subPattern})`
              : '/' + subPattern
        if (optional) subPattern += '?'
​
        pattern += subPattern
​
        subSegmentScore += PathScore.Dynamic
        if (optional) subSegmentScore += PathScore.BonusOptional
        if (repeatable) subSegmentScore += PathScore.BonusRepeatable
        if (re === '.*') subSegmentScore += PathScore.BonusWildcard
      }
​
      segmentScores.push(subSegmentScore)
    }
​
    score.push(segmentScores)
  }
​
  // only apply the strict bonus to the last score
  if (options.strict && options.end) {
    const i = score.length - 1
    score[i][score[i].length - 1] += PathScore.BonusStrict
  }
​
  // TODO: dev only warn double trailing slash
  if (!options.strict) pattern += '/?'
​
  if (options.end) pattern += '$'
  // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
  else if (options.strict) pattern += '(?:/|$)'
​
  // 根据组装好的pattern创建正则表达式,options.sensitive决定是否区分大小写
  const re = new RegExp(pattern, options.sensitive ? '' : 'i')
​
  // 根据path获取动态参数对象
  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
  }
​
  // 根据传入的动态参数对象,转为对应的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 and there are more segments, we don't need to
              // care about the optional param
              if (segment.length < 2 && segments.length > 1) {
                // 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
  }
​
  return {
    re,
    score,
    keys,
    parse,
    stringify,
  }
}

createRouterMatcher的实现

export function createRouteRecordMatcher(
  record: Readonly<RouteRecord>,
  parent: RouteRecordMatcher | undefined,
  options?: PathParserOptions
): RouteRecordMatcher {
  // 生成parser对象
  const parser = tokensToParser(tokenizePath(record.path), options)
​
  // 如果有重复的动态参数命名进行提示
  if (__DEV__) {
    const existingKeys = new Set<string>()
    for (const key of parser.keys) {
      if (existingKeys.has(key.name))
        warn(
          `Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`
        )
      existingKeys.add(key.name)
    }
  }
​
  // 将record,parent合并到parser中,同时新增children,alias属性,默认值为空数组
  const matcher: RouteRecordMatcher = assign(parser, {
    record,
    parent,
    // these needs to be populated by the parent
    children: [],
    alias: [],
  })
​
  if (parent) {
    // 两者都是alias或两者都不是alias
    if (!matcher.record.aliasOf === !parent.record.aliasOf)
      parent.children.push(matcher)
  }
​
  return matcher
}

resolve

解析路由信息的核心方法,作用是将抽象的路由描述(路径,命名路由,参数等)转换成包含完整匹配信息路由对象,核心作用就是“翻译路由信息”,将开发者提供的简介的路由描述转换成包含完整路径的结构化信息

resolve根据传进来的location进行路由的匹配,找到对应的matcher的路由信息,方法接收一个location``currentLocation参数,返回一个MatcherLocation类型的对象,该对象的属性包括:name path params matched meta

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) { // 如果location存在name属性,可根据name从matcherMap获取matcher
      matcher = matcherMap.get(location.name)
​
      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
        })
​
      name = matcher.record.name
      // 合并location.params和currentLocation中的params
      params = assign(
        paramsFromLocation(
          currentLocation.params,
          matcher.keys.filter(k => !k.optional).map(k => k.name)
        ),
        location.params
      )
      // 如果不能通过params转为path抛出错误
      path = matcher.stringify(params)
    } else if ('path' in location) { // 如果location存在path属性,根据path从matchers获取对应matcher
      path = location.path
​
      if (__DEV__ && !path.startsWith('/')) {
        warn(
          `The Matcher cannot resolve relative paths but received "${path}". Unless you directly called `matcher.resolve("${path}")`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/router.`
        )
      }
​
      matcher = matchers.find(m => m.re.test(path))
​
      if (matcher) {
        // 通过parse函数获取params
        params = matcher.parse(path)!
        name = matcher.record.name
      }
    } else { // 如果location中没有name、path属性,就使用currentLocation的name或path获取matcher
      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

删除路由

接收参数matcherRefremoveRoute会将matcherRef对应的matchermatcherMapmatchers中删除,并清空matcherRef对应matcher中的childrenalias属性,由于matcherRef对应的matcher删除,子孙以及别名就没用了,也需要将他们删除

function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
  // 如果是路由名字:string或symbol
  if (isRouteName(matcherRef)) {
    const matcher = matcherMap.get(matcherRef)
    if (matcher) {
      // 删除matcher
      matcherMap.delete(matcherRef)
      matchers.splice(matchers.indexOf(matcher), 1)
      // 清空matcher中的children与alias,
      matcher.children.forEach(removeRoute)
      matcher.alias.forEach(removeRoute)
    }
  } else {
    const 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

获取所有的matcher

function getRoutes() {
  return matchers
}

getRecordMatcher

根据路由的名字获取对应的matcher

function getRecordMatcher(name: RouteRecordName) {
  return matcherMap.get(name)
}

insertMatcher

核心是路由的注册机制,负责将路由模式,处理函数和配选项组织起来,按照优先级插入到匹配器当中

添加matcher的时候,并不直接add的,而是根据matcher.score进行排序,比较分数时根据从数组中的每一项挨个比较,而不是总分,主要是实现动态的添加,经过对比之后将不同的地方(类似diff算法优化)进行对比操作

更新过程:

  • 更新matcher的内部规则

    • 将新的路由配置插入到matcher维护的路由数或者映射表中,确保新路由能被后续的路由能被后续的匹配的逻辑识别
  • 保持匹配的优先级

    • 按照路由匹配定义的顺序插入新的规则,保证“先定义者优先”的匹配原则不受动态添加影响
  • 实时生效

    • 插入后立即对路由跳转和URL变化生效,无需重新初始化路由实例
function insertMatcher(matcher: RouteRecordMatcher) {
  let i = 0
  while (
    i < matchers.length &&
    // matcher与matchers[i]比较,matchers[i]应该在前面
    comparePathParserScore(matcher, matchers[i]) >= 0 &&
    // matcher的path与matchers[i]不同或matcher不是matchers[i]的孩子
    (matcher.record.path !== matchers[i].record.path ||
      !isRecordChildOf(matcher, matchers[i]))
  )
    i++
  // 插入matcher
  matchers.splice(i, 0, matcher)
  // 只添加原始matcher到map中
  if (matcher.record.name && !isAliasRecord(matcher))
    matcherMap.set(matcher.record.name, matcher)
}
​
// 返回0表示ab相等;返回>0b先排序;返回<0a先排序
export function comparePathParserScore(a: PathParser, b: PathParser): number {
  let i = 0
  const aScore = a.score
  const bScore = b.score
  while (i < aScore.length && i < bScore.length) {
    const comp = compareScoreArray(aScore[i], bScore[i])
    if (comp) return comp
​
    i++
  }
​
  return bScore.length - aScore.length
}
​
function compareScoreArray(a: number[], b: number[]): number {
  let i = 0
  while (i < a.length && i < b.length) {
    const diff = b[i] - a[i]
    // 一旦ab对位索引对应的值有差值,直接返回
    if (diff) return diff
​
    i++
  }
  if (a.length < b.length) {
      // 如果a.length1且第一个值的分数为PathScore.Static + PathScore.Segment,返回-1,表示a先排序,否则返回1,表示b先排序
    return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
      ? -1
      : 1
  } else if (a.length > b.length) {
    // 如果b.length1且第一个值的分数为PathScore.Static + PathScore.Segment,返回-1,表示b先排序,否则返回1,表示a先排序
    return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
      ? 1
      : -1
  }
​
  return 0
}

考虑token的类型,主要是路由路径解析逻辑和匹配优先级相关

token在这里指的路径解析得到的不同的片段(静态片段,动态参数),

enum TokenType {
  STATIC = 'static',      // 静态片段,如 /users
  PARAM = 'param',        // 参数,如 :id
  OPTIONAL = 'optional',  // 可选参数,如 :id?
  WILDCARD = 'wildcard',  // 通配符,如 *
  REGEX = 'regex',        // 正则表达式参数,如 :id(\d+)
}

总结

主要了解到了matcher是什么,以及实现

主要使用matchersmatcherMap来存储matcher,matcher中权重高matcher会优先进行匹配,matcherMap中的key主要是注册路由表的name,只存放原始的matcher

matcher是核心控制器,matcherMap是管理的数据结构

matcherMapmatcher简化matcher的查询

matcher中包含了path路由对应的正则,路由的分数,动态参数列表, 以及其他的一些相关的信息

生成matcher的过程中,会将path转化成token数组,用来正则的生成,动态参数的提取,分数的计算,等等

createRouter

基本使用

import { createRouter } from 'vue-router'const router = defineRouter({
    history: routerHistory,
    strict: true,
    routes: [
        {
            path: '/',
            componet: .....
        }
    ]
})

createRouter

这个函数主要是声明了一些函数

首先使用createRouterMatcher方法创建了一个路由匹配器matcher,从options中提取了parseQuery,stringifyQuery``history属性,没有相关的属性抛出错误

const matcher = createRouterMatcher(options.routes, options)
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.'
    )

紧接着声明了一些全局守卫相关的变量,和一些相关的params的处理方法,其中有关全局守卫的变量都是通过useCallbacks创建的,params相关方法通过applyToParams创建

// 全局前置守卫相关方法
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// 如果浏览器环境下设置了scrollBehavior,那么需要防止页面自动恢复页面位置
// https://developer.mozilla.org/zh-CN/docs/Web/API/History/scrollRestoration
if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
  history.scrollRestoration = 'manual'
}
​
// 标准化params,转字符串
const normalizeParams = applyToParams.bind(
  null,
  paramValue => '' + paramValue
)
// 编码param
const encodeParams = applyToParams.bind(null, encodeParam)
// 解码params
const decodeParams: (params: RouteParams | undefined) => RouteParams =
  applyToParams.bind(null, decode)

关于callback的实现,在useCallbacks中声明一个handlers数组用来保存所有添加的方法,useCallback的返回值包括三个方法,add(添加一个handler并返回一个删除handler函数),list(返回所有的handler),reset(清空所有的handler

export function useCallbacks<T>() {
  let handlers: T[] = []
​
  function add(handler: T): () => void {
    handlers.push(handler)
    return () => {
      const i = handlers.indexOf(handler)
      if (i > -1) handlers.splice(i, 1)
    }
  }
​
  function reset() {
    handlers = []
  }
​
  return {
    add,
    list: () => handlers,
    reset,
  }
}

addToParams的实现:接收一个处理函数和params对象,遍历params对象,并对每一个属性值执行fn并将结果赋值给一个新的对象

export function applyToParams(
  fn: (v: string | number | null | undefined) => string,
  params: RouteParamsRaw | undefined
): RouteParams {
  const newParams: RouteParams = {}
​
  for (const key in params) {
    const value = params[key]
    newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value)
  }
​
  return newParams
}

然后就是声明了大量的函数,包括addRoute ,removeRoute,getRoutes,这个函数也是我们日常使用的

createaRouter的最后创建了一个router对象,并将其返回,这个对象几乎包含了声明的所有函数

总结

createRouter函数声明了一些全局钩子所需的变量和很多的钩子,这些函数就是我们日常使用的一些方法,add,remove之类的,后续直接使用就行

router API

基本使用

router.addRoute({name: 'admin', path: '/admin', component: Admin})||
router.addRoute('admin', { path: 'setting', component: AdminSettings })
​
// add 方法不同取决于是否是嵌套路由
​
router.removeRoute()
​
router.hasRoute()
​
router.getRoutes()

addRoute

后续的也差不多,以addRoute为例子

在vue-router当中,matcher实现的addRoute和router的addRoute相比

本质上是同一个功能的不同层级的使用,前者是底层的依赖,后者是上层封装

  • 核心功能一致:都是添加新的路由规则
  • 层级不同:router是对matcher方法的封装
  • 差异:使用场景和暴露范围的不同,router是暴露在外使用的api,matcher是底层实现的不支持直接操作

但是我们为什么不直接使用呢?

  • 简化开发者的使用成本,使得开发者不需要关系底层的实现以及传入什么参数
  • 处理边界情况和错误校验
  • 隔离内部实现,保证api的稳定性
  • 提高扩展的灵活性

实现:

addRoute可以接收两个参数,parentRoute(父路由的name或者一个新的路由,如果是父路由的name,name的第二个参数是必须的),record添加的新路由,返回一个删除路由的函数

function addRoute(
    parentOrRoute: RouteRecordName | RouteRecordRaw,
    route?: RouteRecordRaw
  ) {
    let parent: Parameters<typeof matcher['addRoute']>[1] | undefined
    let record: RouteRecordRaw
    // 如果parentOrRoute是路由名称,parent为parentOrRoute对应的matcher,被添加的route是个嵌套路由
    if (isRouteName(parentOrRoute)) {
      parent = matcher.getRecordMatcher(parentOrRoute)
      record = route!
    } else { // 如果parentOrRoute不是路由名称,parentOrRoute就是要添加的路由
      record = parentOrRoute
    }
​
    // 调用matcher.addRoute添加新的记录,返回一个移除路由的函数
    return matcher.addRoute(record, parent)
  }

其中:isRouteName:是通过name是否是string|symbol类型判断的

export function isRouteName(name: any): name is RouteRecordName {
  return typeof name === 'string' || typeof name === 'symbol'
}

removeRoute

删除路由,接收一个name(现有路由的名称)参数

function removeRoute(name: RouteRecordName) {
  // 根据name获取对应的routeRecordMatcher
  const recordMatcher = matcher.getRecordMatcher(name)
  if (recordMatcher) {
    // 如果存在recordMatcher,调用matcher.removeRoute
    matcher.removeRoute(recordMatcher)
  } else if (__DEV__) {
    warn(`Cannot remove non-existent route "${String(name)}"`)
  }
}

hasRoute

判断路由是否存在,接收一个name字符串,返回一个boolean类型的值

通过matcher.getRecordMatcher来获取对应的matcher,没找到就是不存在

function hasRoute(name: RouteRecordName): boolean {
  return !!matcher.getRecordMatcher(name)
}

getRoutes

获取标准化的路由列表,标准化的路由存储在matcher.record

function getRoutes() {
  // 遍历matchers,routeMatcher.record中存储着路由的标准化版本
  return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}

使用的都是matcher的已经实现的API

resolve

主要作用是将路由信息(如路径,命名路由,参数)解析成一个包含完整路由信息的对象,包含各种细节

接收两个参数,rawLocation,currentLocation(可选),其中的rawLocation是待转化的路由,rawLocation可以是字符串或者对象

在resolve的分支当中:

  • 如果rawLocation是string类型,调用parseURL

    const locationNormalized = parseURL(
      parseQuery,
      rawLocation,
      currentLocation.path
    )
    

    parseURL接收三个参数,parseQuery location currentLocation

    export function parseURL(
      parseQuery: (search: string) => LocationQuery,
      location: string,
      currentLocation: string = '/'
    ): LocationNormalized {
      let path: string | undefined,
        query: LocationQuery = {},
        searchString = '',
        hash = ''
    ​
      // location中?的位置
      const searchPos = location.indexOf('?')
      // location中#的位置,如果location中有?,在?之后找#
      const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0)
    ​
      // 如果
      if (searchPos > -1) {
        // 从location中截取[0, searchPos)位置的字符串作为path
        path = location.slice(0, searchPos)
        // 从location截取含search的字符串,不包含hash部分
        searchString = location.slice(
          searchPos + 1,
          hashPos > -1 ? hashPos : location.length
        )
        // 调用parseQuery生成query对象
        query = parseQuery(searchString)
      }
      // 如果location中有hash
      if (hashPos > -1) {
        path = path || location.slice(0, hashPos)
        // 从location中截取[hashPos, location.length)作为hash(包含#)
        hash = location.slice(hashPos, location.length)
      }
    ​
      // 解析以.开头的相对路径
      path = resolveRelativePath(path != null ? path : location, currentLocation)
      // empty path means a relative query or hash `?foo=f`, `#thing`
    ​
      return {
          // fullPath = path + searchString + hash
        fullPath: path + (searchString && '?') + searchString + hash,
        path,
        query,
        hash,
      }
    }
    

    路径的解析过程 对路径进行解析,方便后续的匹配的过程

    export function resolveRelativePath(to: string, from: string): string {
      // 如果to以/开头,说明是个绝对路径,直接返回即可
      if (to.startsWith('/')) return to
      // 如果from不是以/开头,那么说明from不是绝对路径,也就无法推测出to的绝对路径,此时直接返回to
      if (__DEV__ && !from.startsWith('/')) {
        warn(
          `Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`
        )
        return to
      }
    ​
      if (!to) return from
      // 使用/分割from与to
      const fromSegments = from.split('/')
      const toSegments = to.split('/')
    ​
      // 初始化position默认为fromSegments的最后一个索引
      let position = fromSegments.length - 1
      let toPosition: number
      let segment: string
    ​
      for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
        segment = toSegments[toPosition]
        // 保证position不会小于0
        if (position === 1 || segment === '.') continue
        if (segment === '..') position--
        else break
      }
    ​
      return (
        fromSegments.slice(0, position).join('/') +
        '/' +
        toSegments
          .slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
          .join('/')
      )
    }
    

    最后返回对象

    return assign(locationNormalized, matchedRoute, {
      // 对params中的value进行decodeURIComponent
      params:decodeParams(matchedRoute.params),
      // 对hash进行decodeURIComponent
      hash: decode(locationNormalized.hash),
      redirectedFrom: undefined,
      href,
    })
    
  • rawLocation不是string类型,是对象类型操作即可,找到对应的位置

push*

使用:

会直接进行路由的跳转,导航到对应的位置

router.push('/search?name=pen')
router.push({ path: '/search', query: { name: 'pen' } })
router.push({ name: 'search', query: { name: 'pen' } })
// 以上三种方式是等效的。
​
router.replace('/search?name=pen')
router.replace({ path: '/search', query: { name: 'pen' } })
router.replace({ name: 'search', query: { name: 'pen' } })
// 以上三种方式是等效的。

实现:

push方法接收参数到跳转的地方,可以是字符串或者对象,push方法当中调用了一个pushWithRediect函数,并返回结果

pushWithRedirect

方法接收两个参数,.to redirectFrom,并返回函数的结果,redirectForm就是代表to是从哪个路由重定向来的,多次的重定向只会指向最初重定向的那个路由

function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
  // ...
}

因为使用的to中可能存在重定向,所以pushWithRedirect中首先要对重定向进行处理,to中仍然存在重定向的时候,递归的调用pushWithRedirect方法

// 将to处理为规范化的路由
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
// 当前路由
const from = currentRoute.value
// 使用 History API(history.state) 保存的状态
const data: HistoryState | undefined = (to as RouteLocationOptions).state
// force代表强制触发导航,即使与当前位置相同
const force: boolean | undefined = (to as RouteLocationOptions).force
// replace代表是否替换当前历史记录
const replace = (to as RouteLocationOptions).replace === true// 获取要重定向的记录
const shouldRedirect = handleRedirectRecord(targetLocation)
​
// 如果需要重定向,递归调用pushWithRedirect方法
if (shouldRedirect)
  return pushWithRedirect(
    assign(locationAsObject(shouldRedirect), {
      state: data,
      force,
      replace,
    }),
    // 重定向的根来源
    redirectedFrom || targetLocation
  )

处理重定向之后, 接下来就是检测即将跳转的路由和当前的路由是否是同一个路由,如果是同一个路由并且路由不强制跳转,会创建一个失败的函数,并且赋值,然后处理滚动行为

滚动行为的实现,首先从options找到scrollBehavior选项,如果不是浏览器的环境下或者不存在scollBehavior,返回一个Promise对象,相反,获取滚动位置,然后在下一次DOM刷新之后,执行定义的滚动行为函数,滚动指定位置

function handleScroll(
  to: RouteLocationNormalizedLoaded,
  from: RouteLocationNormalizedLoaded,
  isPush: boolean,
  isFirstNavigation: boolean
): Promise<any> {
  const { scrollBehavior } = options
  if (!isBrowser || !scrollBehavior) return Promise.resolve()
​
  // 获取滚动位置
  const scrollPosition: _ScrollPositionNormalized | null =
    (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
    ((isFirstNavigation || !isPush) &&
      (history.state as HistoryState) &&
      history.state.scroll) ||
    null
​
  // 下一次DOM更新后触发滚动行为,滚动行为执行完后,滚动到指定位置
  return nextTick()
    .then(() => scrollBehavior(to, from, scrollPosition))
    .then(position => position && scrollToPosition(position))
    .catch(err => triggerError(err, to, from))
}
​
export function getScrollKey(path: string, delta: number): string {
  // history.state.position记录着当前路由在历史记录中的位置,该位置从0开始
  const position: number = history.state ? history.state.position - delta : -1
  // key值为 在历史记录中的位置+path
  return position + path
}
​
export function getSavedScrollPosition(key: string) {
  // 根据key值查找滚动位置
  const scroll = scrollPositions.get(key)
  // 查完后,删除对应记录
  scrollPositions.delete(key)
  return scroll
}

pushWithRedirect最后返回一个Promise,如果有failure返回failure,执行navigate(toLocation,to)

navigate

接收两个参数 to from

navigator,进行相应的操作:

  1. 解析目标路由 resolve
  2. 执行导航守卫,处理相对应的钩子函数
  3. 更新浏览器的历史记录
  4. 触发组件渲染

负责将开发者的跳转指令push转化成实际URL变化和页面更新,他是框架内部的实现细节

导航的完整的解析流程,执行过程中维护一个队列按顺序执行

  1. 导航被触发
  2. 调用失活组件中的beforeRouteLeave钩子
  3. 调用全局的beforeEach钩子
  4. 调用重用组件中的beforeRouteUpdate钩子
  5. 调用路由配置中的beforeEnter钩子
  6. 解析异步组件路由
  7. 调用激活组件当中的beforeRouteEnter钩子
  8. 调用全局的beforeResolve钩子

执行过程中存在runGuardQueue,只有当前的钩子执行完毕之后才会继续执行

navigate之后执行顺利最后会调用全局afterEach方法,执行finalizeNavigation,主要就是确认我们的导航,改变URL,处理滚动行为

调用最后markAsReady方法

函数会标记路由的准备状态,执行通过isReady添加的回调

function markAsReady<E = any>(err?: E): E | void {
  // 只在ready=false时进行以下操作
  if (!ready) {
    // 如果发生错误,代表还是未准备好
    ready = !err
    // 设置监听器
    setupListeners()
    // 执行ready回调
    readyHandlers
      .list()
      .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
    // 重置ready回调列表
    readyHandlers.reset()
  }
  return err
}

最终执行完毕

replace

replace和push执行的作用几乎相同,push指定replace: true,和直接使用replace的效果是一样的

function replace(to: RouteLocationRaw | RouteLocationNormalized) {
  return push(assign(locationAsObject(to), { replace: true }))
}

这里调用了一个locationAsObject,如果to是string,会调用parseURL解析成to,关于parseURL调用实现类似resolve,解析成对应的URL

go

在访问历史中进行前进或者回退

接收一个参数delta,表示相对当前页面,移动的步数

调用的过程中会调用history.go函数,进而触发popState函数,触发事件。

这些我们在createWebHostoryuseHistoryListeners中已经实现过,直接会触发

// 文件位置:src/history/html5.ts useHistoryListeners方法
window.addEventListener('popstate', popStateHandler)
​
const popStateHandler: PopStateListener = ({
 state,
}: {
  state: StateEntry | null
}) => {
  // 当前location,字符串
  const to = createCurrentLocation(base, location)
  const from: HistoryLocation = currentLocation.value
  const fromState: StateEntry = historyState.value
  let delta = 0
​
  // 如果不存在state
  // 关于为什么state可能为空,可参考:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event
  if (state) {
    currentLocation.value = to
    historyState.value = state
​
    // 如果暂停监听了,并且暂停时的状态是from,直接return
    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,
    })
  })
}

监听的最后,循环调用listener,那listener是什么时候添加的呢?

push的时候调用pushWidthRedirect方法,最后执行finializeNavigation方法的最后调用markAsReady方法

function markAsReady<E = any>(err?: E): E | void {
  if (!ready) {
    // still not ready if an error happened
    ready = !err
    setupListeners()
    readyHandlers
      .list()
      .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
    readyHandlers.reset()
  }
  return err
}

调用了setupListeners的方法,会调用listen添加一个函数

这个函数的监听和push的过程类似,但是和push不同的是,在触发监听时候,一旦出现错误信息,需要将历史回退到相应的位置

back

go(-1)

forward

go(1)

isReady

使用:

router.isReady()
  .then(() => {
    // 成功
  })
  .catch(() => {
    // 失败
  })

实现:

isReady不接受任何参数,如果路由器已经完成了初始化,就会立即解析Promise,相反没有完成初始化,会将resolve reject放入到一个数组中,并添加到列表中,等待执行

全局导航守卫

主要是等待路由初始化完成的时机,确保路由系统已经完成初始化

核心的作用就是解决路由初始化中的异步问题(异步路由组件的加载,初始导航的守卫处理)

使用

  • beforeEach:在导航之前执行,返回一个已经删除导航守卫吃的函数
  • beforeResolve:在导航解析之前执行,返回一个删除已经注册导航守卫函数
  • after:在任何导航之后执行,返回一个删除已经注册导航守卫的函数

实现:

全局导航守卫和onError的实现都是通过维护一个数组进行实现,通过useCallback函数可以创建一个可以重置的列表,全局钩子,以及onError就是通过useCallback实现的

const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
let errorHandlers = useCallbacks<_ErrorHandler>()
​
const router = {
  // ...
  beforeEach: beforeGuards.add,
  beforeResolve: beforeResolveGuards.add,
  afterEach: afterGuards.add,
  onError: errorHandlers.add,
  // ...
}
export function useCallbacks<T>() {
  let handlers: T[] = []
​
  function add(handler: T): () => void {
    handlers.push(handler)
    return () => {
      const i = handlers.indexOf(handler)
      if (i > -1) handlers.splice(i, 1)
    }
  }
​
  function reset() {
    handlers = []
  }
​
  return {
    add,
    list: () => handlers,
    reset,
  }
}

useCalback返回一个对象,具有三个属性,list是内部维护的列表,add是添加handler的函数,返回一个删除的函数,reset重置函数

OnBeforerouteLeave/OnBeforeRouteUpdate

使用 这两个参数是提供的是两个composition API只能用在setup当中

使用:

export default {
  setup() {
    onBeforeRouteLeave() {}
​
    onBeforeRouteUpdate() {}
  }
}

onBeforeRouteLeave

export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
  // 开发模式下没有组件实例,进行提示并return
  if (__DEV__ && !getCurrentInstance()) {
    warn(
      'getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function'
    )
    return
  }
​
  // matchedRouteKey是在RouterView中进行provide的,表示当前组件所匹配到到的路由记录(经过标准化处理的)
  const activeRecord: RouteRecordNormalized | undefined = inject(
    matchedRouteKey,
    // to avoid warning
    {} as any
  ).value
​
  if (!activeRecord) {
    __DEV__ &&
      warn(
        'No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?'
      )
    return
  }
​
  // 注册钩子
  registerGuard(activeRecord, 'leaveGuards', leaveGuard)
}

钩子的注册

function registerGuard(
  record: RouteRecordNormalized,
  name: 'leaveGuards' | 'updateGuards',
  guard: NavigationGuard
) {
  // 一个删除钩子的函数
  const removeFromList = () => {
    record[name].delete(guard)
  }
​
  // 卸载后移除钩子
  onUnmounted(removeFromList)
  // 被keep-alive缓存的组件失活时移除钩子
  onDeactivated(removeFromList)
​
  // 被keep-alive缓存的组件激活时添加钩子
  onActivated(() => {
    record[name].add(guard)
  })
​
  // 添加钩子,record[name]是个set,在路由标准化时处理的
  record[name].add(guard)
}

onBeforeRouteUpdate

实现的差不多就是调用传入的参数不同

export function onBeforeRouteUpdate(updateGuard: NavigationGuard) {
  if (__DEV__ && !getCurrentInstance()) {
    warn(
      'getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function'
    )
    return
  }
​
  const activeRecord: RouteRecordNormalized | undefined = inject(
    matchedRouteKey,
    // to avoid warning
    {} as any
  ).value
​
  if (!activeRecord) {
    __DEV__ &&
      warn(
        'No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?'
      )
    return
  }
​
  registerGuard(activeRecord, 'updateGuards', updateGuard)
}

use之类的

useRoute,useRouter,useLink

useRoute:获取当前的路由信息,path params query之类的,核心就是通过inject从上下文对象中获取的,开始的时候会进行注入一个响应式的路由状态对象(包括路由信息)

useRouter:获取路由实例

useLink:处理连接导航逻辑,提供连接的激活状态,导航方法等核心功能,依赖于前两个,处理路由匹配和激活状态的计算,拿到对应的i数据直接使用navigate isActive isExactActive...

基本使用

<script lant="ts" setup>
import { useRouter, useRoute } from 'vue-router'// router为创建的router实例
const router = useRouter()
// currentRoute当前路由
const currentRoute = useRoute()
</script>

routerLink

简单的使用

<Router-link to="/index" replace custom active="active">To IndexPage</router-link>

说明:

  • 声明式路由导航,代替了传统的href,在不刷新页面的前提下完成页面的跳转

  • 直接使用设置跳转的方向

  • 常见属性

    • to 跳转的路径或者对象
    • replave 替换当前的历史(不能后退)
    • active-class 自定义激活时候的class
  • 内部使用router的相关api实现

  • 注意:不是触发HTTP请求,是使用JavaScript实现跳转的

export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
  name: 'RouterLink',
  props: {
    // 目标路由的链接
    to: {
      type: [String, Object] as PropType<RouteLocationRaw>,
      required: true,
    },
    // 决定是否调用router.push()还是router.replace()
    replace: Boolean,
    // 链接被激活时,用于渲染a标签的class
    activeClass: String,
    // inactiveClass: String,
    // 链接精准激活时,用于渲染a标签的class
    exactActiveClass: String,
    // 是否不应该将内容包裹在<a/>标签中
    custom: Boolean,
    // 传递给aria-current属性的值。https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current
    ariaCurrentValue: {
      type: String as PropType<RouterLinkProps['ariaCurrentValue']>,
      default: 'page',
    },
  },
  useLink,
​
  setup(props, { slots }) {
    // 使用useLink创建router-link所需的一些属性和行为
    const link = reactive(useLink(props))
    // createRouter时传入的options
    const { options } = inject(routerKey)!
​
    // class对象
    const elClass = computed(() => ({
      [getLinkClass(
        props.activeClass,
        options.linkActiveClass,
        'router-link-active'
      )]: link.isActive, // 被激活时的class
      [getLinkClass(
        props.exactActiveClass,
        options.linkExactActiveClass,
        'router-link-exact-active'
      )]: link.isExactActive, // 被精准激活的class
    }))
​
    return () => {
      // 默认插槽
      const children = slots.default && slots.default(link)
      // 如果设置了props.custom,直接显示chldren,反之需要使用a标签包裹
      return props.custom
        ? children
        : h(
            'a',
            {
              'aria-current': link.isExactActive
                ? props.ariaCurrentValue
                : null,
              href: link.href,
              onClick: link.navigate,
              class: elClass.value,
            },
            children
          )
    }
  },
})
​
export const RouterLink = RouterLinkImpl as unknown as {
  new (): {
    $props: AllowedComponentProps &
      ComponentCustomProps &
      VNodeProps &
      RouterLinkProps
​
    $slots: {
      default: (arg: UnwrapRef<ReturnType<typeof useLink>>) => VNode[]
    }
  }
  useLink: typeof useLink
}   

routerView

设置路由出口,设置展示的页面出口

渲染流程:

  1. 导航触发
  2. 调用失活组件当中的beforeRouteLeave钩子
  3. 调用全局组件beforeEach钩子
  4. 调用重用组件中的beforeRouteUpdate钩子
  5. 调用路由配置中的beforeEnter钩子
  6. 解析异步路由组件
  7. 调用激活组件中的beforeRouteEnter钩子
  8. 调用全局afterEach钩子
  9. 导航被确认
  10. 调用全局afterEach钩子
  11. DOM更新
  12. 调用beforeEnter守卫传递给next的回调函数,创建好的组件,会作为回调函数的参数传入

eg:

当我们访问localhost:3000接口的时候,是怎样进行渲染的呢?(渲染流程)

首先我们知道vue-router在install的时候,会立即进行一次路由的跳转,并且立马向app注入一个默认的currentRoute,此时router-view会根据,这个currentRoute进行第一次的渲染(initalNavigation)

因为这个默认的currentRoute中的matched是空的,所以第一次渲染的结果式空的

const START_LOCATION_NORMALIZED = {
    path: '/',
    query: {},
    params: {},
    hash: '',
    matched: [],
    name: null,
    ...
}

,当第一次路由跳转完毕之后,会执行finalizeNavigator方法,

function finalizeNavigation(to, from, replaced, redirectedFrom) {
  // 1. 更新 router.currentRoute
  currentRoute.value = to  // 响应式赋值!// 2. 触发导航完成钩子(afterEach)
  triggerAfterEach(to, from, null)
​
  // 3. 解决首次加载的 promise(如果有 await router.isReady())
}

这个方法更新currentRoute,这时currentRoute中就可以找到对应的渲染组件(target),router-view完成第二次渲染之后,紧接着触发router-view当中的watch,将最新的组件实例赋值给to.instance[name],并循环执行callback(拿到真实的组件)

然后我们进行跳转的时候,假设使用的是push方法,同样在跳转完成之后,会执行finalizeNavigator函数,更新currentRoute,这是会监听到currentRoute的改变,找到需要渲染的组件,将其显示,在渲染前先执行旧组件的卸载功能,将对应的路由设置成null,完成渲染之后,触发watch,将最新的组件实例赋值给to.instance[name],并执行enterCallback函数

浏览器
   │
   ▼
加载 Vue 应用
   │
   ▼
VueRouter.install() → 初始化 router 实例
   │
   ├── 创建响应式 currentRoute(初始值:START_LOCATION_NORMALIZED)
   │    matched: [], path: '/'
   │
   ▼
<router-view> 首次渲染
   │
   ├── 读取 currentRoute.matched
   │
   └── matched 为空 → 渲染 null(页面空白)
   │
   ▼
Vue Router 自动触发 initial navigation(初始导航)
   │
   ├── 解析当前 URL: '/'
   │
   ├── 匹配路由表 → 找到 { path: '/', component: Home }
   │
   ├── 生成完整 to 对象:
   │    {
   │      path: '/',
   │      matched: [ { path: '/', component: Home } ],
   │      ...
   │    }
   │
   ▼
调用 finalizeNavigation(to)
   │
   ├── currentRoute.value = to  ← 响应式更新!
   │
   └── 触发导航完成钩子(afterEach)
   │
   ▼
<router-view> 的 watch 监听到 currentRoute 变化
   │
   ├── 重新计算 matched[0].component → Home
   │
   ├── 创建 Home 组件实例
   │
   ├── 执行 Home 的 beforeRouteEnter(如有)
   │
   └── 渲染 Home 组件到页面 ✅

总的就是

先将matched中的数据进行变化,拿到最新的路由信息

然后将currentRoute进行赋值(finalizeNavigation)to参数

最后watch自动监听到route的变化,找到对应的新组件

进行旧组件的卸载(同时调用一个callback钩子函数)

渲染新的组件,创建组件实例,其中调用特定的函数,帮助组件的渲染