电梯

Vue 源码

源码篇 剖析 Vue2 双向绑定原理

源码篇 使用及分析 Vue 全局 API

源码篇 虚拟DOM

源码篇 模板编译

源码篇 实例方法

源码篇 生命周期

源码篇 Vue 的扩展机制设计

Vue Router 4 源码

源码篇 Vue Router 4 上篇

源码篇 Vue Router 4 中篇

源码篇 Vue Router 4 下篇

开始之前,我们需要先区分两个名词:

在 Vue Router 的语境里,“路由守卫” 和 “导航守卫” 这两个名词常常混用,但它们在严格意义上是 包含关系不同 的两个概念。

名称英文含义范围
导航守卫Navigation Guard任何能拦截、控制导航(路由跳转)的函数官方标准用语
路由守卫Route Guard通常特指某条「路由记录」上的守卫(如 beforeEnter)或组件内守卫是导航守卫的一部分

正文

下面我们直接切入正题!

1.回顾

在前面这篇 前端面试知识汇总(Vue模块——15点.16点)中,围绕前端面试中遇到的路由相关问题展开了描述,并介绍了几种导航守卫:全局守卫、路由独享守卫、组件内守卫。

下面便是那篇文章中提到的 是什么?怎么用?何时用?

导航守卫是前端框架中用来控制页面导航的机制。它允许在用户进入、离开或切换路由时执行特定的逻辑。

类型常用钩子执行时机
全局守卫beforeEach(前置)beforeResolve(解析)afterEach(后置)作用于所有路由跳转
路由独享守卫beforeEnter只在进入路由时触发
组件内守卫

beforeRouteEnter(组件渲染)

beforeRouteUpdate(hook:onBeforeRouteUpdate)

beforeRouteLeave(hook:onBeforeRouteLeave)

控制组件渲染前后行为

完整执行顺序

从页面 A 导航到页面 B 时,我们模拟一个完整的业务场景:

1️⃣ 全局 beforeEach —— 登录鉴权
2️⃣ 路由独享 beforeEnter —— 进入页面前,输出“将进入页面B”
3️⃣ 组件内 beforeRouteLeave(离开旧组件)—— 输出“离开页面A”
4️⃣ 组件内 beforeRouteUpdate(复用组件时)—— 输出路由拼接的参数
5️⃣ 组件内 beforeRouteEnter(进入新组件)—— 输出“等待执行 next(vm)”
6️⃣ 全局 beforeResolve —— 输出“所有守卫执行完毕”
7️⃣ 导航被确认 —— 输出“路由正式生效,开始渲染组件”
8️⃣ 全局 afterEach —— 输出“从页面A到页面B已完成跳转”
9️⃣ DOM 更新完成后 beforeRouteEnter 的 next(vm) —— 输出“组件创建完成”

2.全局守卫

beforeEach 前置守卫

  • 触发时机:路由跳转开始时
  • 如 登录权限验证
  • 可通过 next() 控制是否放行或重定向
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next('/login')  // 拦截跳转登录
  } else {
    next()          // 放行
  }
})

beforeResolve 解析守卫

  • 触发时机:在导航被确认之前,最后一次机会修改导航行为
  • 常用于异步数据加载完成后再进入页面
  • 在所有路由组件内守卫和异步路由组件解析之后调用
router.beforeResolve((to, from, next) => {
  console.log('所有组件守卫执行完毕,准备导航')
  next()
})

afterEach 后置守卫

  • 触发时机:导航已经确认并完成后
  • 无 next(),无法中断导航
  • 常用于日志、埋点、页面统计
router.afterEach((to, from) => {
  console.log(`从 ${from.fullPath} 到 ${to.fullPath}`)
})

全局路由守卫注册在 router 实例上,影响整个应用的所有导航,因此它会出现在 createRouter

源码位置:packages/router/src/router.ts

export function createRouter(options: RouterOptions): Router {
    const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
    const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
    const afterGuards = useCallbacks<NavigationHookAfter>()

    // ...

    const router: Router = {
        beforeEach: beforeGuards.add,
        beforeResolve: beforeResolveGuards.add,
        afterEach: afterGuards.add
        // ...
    }

    // ...

    return router
}

这段简化后的代码就是 全局导航守卫的注册机制,下面我们来逐步解析:

1.创建守卫回调容器

const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()

所使用到的 useCallbacks() 是一个内部工具函数,用于创建一个「回调函数列表」,每个返回值形如:

{
  add: (fn) => { ... },    // 注册回调
  list: [...],             // 已注册的回调集合
  reset: () => { ... }     // 重置
}

因此:

  • beforeGuards 用于保存所有的 beforeEach 守卫
  • beforeResolveGuards 保存所有的 beforeResolve 守卫
  • afterGuards 保存所有的 afterEach 守卫

useCallbacks 源码位置:packages/router/src/utils/callbacks.ts

export function useCallbacks<T>() {
  // 存储所有注册的回调函数
  // 泛型 T 意味着可以存储任意类型的回调
  let handlers: T[] = []

  // 注册一个回调函数(添加回调并返回移除函数)
  function add(handler: T): () => void {
    // 将 handler 存入 handlers 数组
    handlers.push(handler)
    // 返回一个取消注册的函数
    return () => {
      const i = handlers.indexOf(handler)
      if (i > -1) handlers.splice(i, 1)
    }
  }

  // 清空所有已注册的回调(常在 router 销毁或重新初始化时调用,用于释放内存)
  function reset() {
    handlers = []
  }

  return {
    add,
    list: () => handlers.slice(),
    reset,
  }
}

以 beforeEach 为例:

const guards = useCallbacks<Function>()
const remove = guards.add(() => console.log('beforeEach'))

执行后:

handlers = [() => console.log('beforeEach')]

若之后执行 remove(),就会:

handlers = []

因此:

  • 当调用 router.beforeEach(fn) 时,实际上执行的是:beforeGuards.add(fn),所有守卫被统一保存在 beforeGuards.list() 内部
  • 在路由导航时,Vue Router 会读取这些守卫:
runGuardQueue(beforeGuards.list().map(guard => guard(to, from, next)))
  • 这里 .list() 返回所有注册的 beforeEach 守卫,然后依次执行

至此,我们又发现了新的关键点:runGuardQueue,而说 runGuardQueue 之前,我们需要先提到 navigate(),它是执行从一个路由跳转到另一个路由时,所有守卫的调度器。它并不需要我们手动调用,而是内部被 push/replace 等导航函数调用。

当我们执行:

router.push('/home')

大致流程如下:

push()
  └── navigate(to, from)
        ├── 执行 beforeRouteLeave
        ├── 执行 beforeEach
        ├── 执行 beforeRouteUpdate
        ├── 执行 beforeEnter
        ├── 执行 beforeRouteEnter
        ├── 执行 beforeResolve
        └── 导航确认(进入页面)

因此:

navigate() 可以说是整个路由守卫调用链的核心引擎

源码位置:packages/router/src/router.ts

我们就按照图中划分的三部分来分析:

function navigate(to, from): Promise<any> {
  let guards: Lazy<any>[]
  const [leavingRecords, updatingRecords, enteringRecords] =
    extractChangingRecords(to, from)

extractChangingRecords(to, from) 这个函数会比对「旧路由」和「新路由」的 matched route record 列表,从而得到:

  • leavingRecords:要离开的组件(页面 A)
  • updatingRecords:路径相同但参数不同的复用组件
  • enteringRecords:新进入的组件(页面 B)
guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from)

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

const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from)

guards.push(canceledNavigationCheck)

return runGuardQueue(guards)
  • 收集离开的组件的 beforeRouteLeave 守卫
  • 转换为 promise 执行队列
  • 每执行完一个就检查是否被取消

下面是这部分链式调用的解析:

第一个.then() 执行全局 beforeEach:

guards = []
for (const guard of beforeGuards.list()) {
  guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)

第二个.then() 执行复用组件的 beforeRouteUpdate:

guards = extractComponentsGuards(
  updatingRecords,
  'beforeRouteUpdate',
  to,
  from
)

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

第三 个.then() 执行路由独享 beforeEnter:

guards = []
for (const record of enteringRecords) {
  if (record.beforeEnter) {
    if (isArray(record.beforeEnter)) {
      for (const beforeEnter of record.beforeEnter)
        guards.push(guardToPromiseFn(beforeEnter, to, from))
    } else {
      guards.push(guardToPromiseFn(record.beforeEnter, to, from))
    }
  }
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)

第四个.then() 执行组件内的 beforeRouteEnter:

to.matched.forEach(record => (record.enterCallbacks = {}))

guards = extractComponentsGuards(
  enteringRecords,
  'beforeRouteEnter',
  to,
  from,
  runWithContext
)
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)

第五个.then() 执行全局 beforeResolve:

guards = []
for (const guard of beforeResolveGuards.list()) {
  guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)

.catch() 捕获异常:

若守卫返回 false 或抛出异常,则中断导航

.catch(err =>
  isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
    ? err
    : Promise.reject(err)
)

其中都用到了一个核心执行器 runGuardQueue,用来 串行执行守卫队列:

function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
    return guards.reduce(
      (promise, guard) => promise.then(() => runWithContext(guard)),
      Promise.resolve()
    )
}
  • 按顺序执行每个守卫
  • 每个守卫执行完再执行下一个
  • 若有拒绝(导航取消、异常),Promise 链会中断

还用到了 guardToPromiseFn,它可以将一个“普通守卫函数”包装成一个返回 Promise 的函数的,因为守卫的可能写法多种多样,比如:

(to, from, next) => { next() }
(to, from) => true
(to, from) => false
(to, from) => Promise.resolve()

无论守卫写成哪种形式,guardToPromiseFn 都统一转为 Promise 形式,方便用 runGuardQueue 串行执行。


源码位置:packages/router/src/navigationGuards.ts

我们发现 guardToPromiseFn 有三段定义,这其实是 TypeScript 的函数重载机制(Function Overload),为了让 TypeScript 在不同调用方式下能自动推断正确类型。

  • 前两段是 类型重载声明,用于给不同守卫类型提供精确的参数签名
  • 第三段是 真正的实现,负责统一处理各种守卫执行逻辑

1.关键变量和入参

export function guardToPromiseFn(
  guard, // 守卫函数(beforeEach, beforeEnter, beforeRouteEnter 等)
  to, from, // 路由对象
  record?, name?, // 当前守卫所在的组件路由记录及视图名称
  runWithContext = fn => fn() // 可选:保证 setup 环境 this 正确
)

enterCallbackArray 是给 beforeRouteEnter 特殊用的

因为 beforeRouteEnter 的回调(next(vm))会在组件实例创建后再执行,这里先缓存下来:

const enterCallbackArray =
  record && (record.enterCallbacks[name!] = record.enterCallbacks[name!] || [])

2.核心执行流程

// 整个函数返回一个 高阶函数
return () => new Promise((resolve, reject) => { ... })

// 外部可以这样使用
const guardPromise = guardToPromiseFn(guard, to, from)
await guardPromise()

下面这段代码就是 定义 next() 回调:

把 next() 形式 转化为 Promise 的 resolve() / reject() 调用。不同的 next() 参数代表不同导航结果:

  • false → 终止导航
  • Error → 抛出异常
  • RouteLocation → 重定向
  • function(只在 beforeRouteEnter)→ 延迟回调
  • 其他情况 → 正常通过
const next: NavigationGuardNext = (valid?) => {
  if (valid === false) {
    reject(createRouterError(ErrorTypes.NAVIGATION_ABORTED, { from, to }))
  } else if (valid instanceof Error) {
    reject(valid)
  } else if (isRouteLocation(valid)) {
    reject(createRouterError(ErrorTypes.NAVIGATION_GUARD_REDIRECT, { from: to, to: valid }))
  } else {
    if (enterCallbackArray && typeof valid === 'function') {
      enterCallbackArray.push(valid)
    }
    resolve()
  }
}

执行守卫函数:

const guardReturn = runWithContext(() =>
  guard.call(record && record.instances[name!], to, from, next)
)

let guardCall = Promise.resolve(guardReturn)
  • 这里用 .call() 是为了保证 this 指向组件实例
  • runWithContext 是在 <script setup> 环境中执行时保持响应式上下文
  • 最后将返回值封装成 Promise.resolve(guardReturn),保证同步/异步都能兼容

判断守卫类型(是否带 next 参数):

if (guard.length < 3) guardCall = guardCall.then(next)
  • 如果守卫只定义了 (to, from),说明它是返回 Promise 的形式 → 那就自动在执行后调用一次 next()
  • 如果守卫定义了 (to, from, next),就交给用户自己调用 next()
// 写法1(带 next)
beforeEach((to, from, next) => { next() })

// 写法2(返回 Promise)
beforeEach(async (to, from) => { return true })

3.路由独享守卫

beforeEnter

  • 在路由配置中定义,仅作用于该路由本身
  • 只在进入该路由时执行
  • 不会在同一路由内参数变化时再次执行
  • 优先于组件内守卫
const routes = [
  {
    path: '/about',
    component: About,
    beforeEnter: (to, from, next) => {
      console.log('进入 /about 之前触发')
      next()
    }
  }
]

它的源码我们上面已经展示,就在很长的 promise 链式调用中,因为路由独享守卫的封装和全局守卫一致,都是通过 guardToPromiseFn 完成:

源码位置:packages/router/src/router.ts

4.小结

至此,我们分析完了 全局守卫 和 路由独享守卫,我们总结出了一个规律:Vue Router4 中所有守卫都按照如下执行过程:

  1. 通过 guardToPromiseFn 将回调方法统一封装为 Promise 形式,方便 runGuardQueue 链式调用
  2. 执行 runGuardQueue 方法,完成导航守卫拦截

但下面即将分析的 组件内守卫,在进行 promise 形式封装时,与前面两种有所不同,我们先看三段代码:

源码位置:packages/router/src/router.ts

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

    guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from, runWithContext)

    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
})
.then(() => {
    guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from)

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

    guards.push(canceledNavigationCheck)

    return runGuardQueue(guards)
})
guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from)

我们可以发现,组件守卫的三个钩子函数都调用了 extractComponentsGuards,因此下面我们将重点分析这个函数。

5.组件内守卫

定义在路由组件的 setup() 或 options API 中,控制组件渲染前后的行为

beforeRouteEnter 组件渲染

  • 在组件被创建之前调用
  • 不能访问 this(组件实例还未创建)
  • 可通过 next(vm => {...}) 获取实例
beforeRouteEnter(to, from, next) {
  next(vm => {
    console.log('组件创建完成', vm)
  })
}

beforeRouteUpdate 路由更新

  • 当前路由改变但复用同一个组件实例时调用
  • 常见于路径参数变化但组件未被销毁的情况
    • 从 /user/1 → /user/2,组件是同一个 <User />,会触发这个守卫
beforeRouteUpdate(to, from, next) {
  console.log('参数更新', to.params)
  next()
}

beforeRouteLeave 离开组件

  • 在导航离开当前组件对应路由时调用
  • 常用于保存草稿、确认离开等
beforeRouteLeave(to, from, next) {
  const answer = window.confirm('确定要离开吗?未保存的更改将丢失')
  if (answer) next()
  else next(false)
}

导航守卫提取机制

  • 从路由记录(RouteRecordNormalized)中提取出每个组件定义的导航守卫(beforeRouteEnter / beforeRouteUpdate / beforeRouteLeave),并封装成成 Promise 形式的可执行函数列表

这些提取到的守卫最后会被 navigate() 调用,串联成导航流程
源码位置:packages/router/src/navigationGuards.ts

type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave'

export function extractComponentsGuards(
  matched: RouteRecordNormalized[], // 当前导航涉及的匹配路由记录
  guardType: GuardType, // 要提取的守卫类型
  to: RouteLocationNormalized, // 目标路由
  from: RouteLocationNormalizedLoaded, // 来源路由
  runWithContext: <T>(fn: () => T) => T = fn => fn() // 上下文运行器(用于 setup 语境)
)

// 返回值
Array<() => Promise<void>>
// 用于收集所有组件守卫
const guards: Array<() => Promise<void>> = []

// 遍历每条路由记录
for (const record of matched) {
    // 遍历每个组件
    for (const name in record.components) {
        let rawComponent = record.components[name]

        // ...接下来会对每个组件进行类型与合法性判断

        // 特殊情况跳过:beforeRouteEnter 没有实例
        // beforeRouteUpdate 和 beforeRouteLeave 需要组件实例才能执行
        // beforeRouteEnter 在实例创建前执行,所以即使还没实例也可以运行
        if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue

        // ...
    }
}
// 如果是同步组件
if (isRouteComponent(rawComponent)) {
  const options = (rawComponent as any).__vccOpts || rawComponent
  const guard = options[guardType]
  guard && guards.push(guardToPromiseFn(guard, to, from, record, name, runWithContext))
}
// 如果是异步组件,懒加载
else {
  let componentPromise = (rawComponent as Lazy<RouteComponent>)()

  guards.push(() =>
    componentPromise.then(resolved => {
      const resolvedComponent = isESModule(resolved) ? resolved.default : resolved
      record.mods[name] = resolved
      record.components![name] = resolvedComponent

      const options = (resolvedComponent as any).__vccOpts || resolvedComponent
      const guard = options[guardType]
      return guard && guardToPromiseFn(guard, to, from, record, name, runWithContext)()
    })
  )
}

最终返回 return guards,这些返回的函数稍后会在 navigate() 中通过 runGuardQueue(guards) 按顺序执行。

 整体执行流程图

extractComponentsGuards()
   │
   ├─ 遍历 matched 路由记录
   │   ├─ 遍历 record.components 中的每个组件
   │   │   ├─ 检查合法性与懒加载写法
   │   │   ├─ 如果不是 beforeRouteEnter 且没实例 → 跳过
   │   │   ├─ 如果是同步组件 → 直接提取守卫
   │   │   └─ 如果是异步组件 → 等待加载再提取守卫
   │
   └─ 返回所有 Promise 形式的守卫函数数组

全篇总结

到这里,我们的 源码篇 Vue Router 4 已全部结束~后续会持续更新完善内容

参考文章

https://juejin.cn/column/7140106983243251726

https://router.vuejs.org/zh/api/

https://router.vuejs.org/zh/api/interfaces/Router.html

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐