Vue Router与响应式系统的集成

在前面的文章中,我们深入学习了 Vue3 的响应式系统、组件渲染、生命周期等核心机制。今天,我们将探索 Vue Router 是如何与 Vue 的响应式系统无缝集成的。理解路由的实现原理,将帮助我们更好地处理页面导航、路由守卫等复杂场景。

前言:路由的核心挑战

Vue Router 作为 Vue 的官方路由管理器,其最精妙的设计之一就是与 Vue 响应式系统的无缝集成。Vue 作为单页应用(SPA),在路由管理中,面临的核心挑战是:在URL变化时,不刷新页面,而是动态切换组件: 同时,也面临诸多问题:

  • 如何监听URL变化而不刷新页面?
  • 如何让路由变化触发组件重新渲染?
  • 如何管理路由历史?

Vue Router 响应式设计总览

响应式数据的核心

Vue Router 实现响应式导航的核心是:将当前路由状态(currentRoute)作为响应式数据。当路由发生变化时,依赖这个响应式数据的组件(如 router-view)会自动重新渲染:

javascript 复制代码
// 简化的核心代码
const currentRoute = shallowRef(initialRoute);

整体架构

Vue Router 的响应式集成主要包含三个层次:

  • 数据层:currentRoute 响应式对象
  • 视图层:router-view 组件监听路由变化
  • 交互层:router-link 组件和编程式导航

currentRoute:路由响应式数据的实现

核心响应式设计

在 Vue Router 4 中,当前路由状态被设计为一个 shallowRef 响应式对象:

javascript 复制代码
import { shallowRef } from 'vue'

function createRouter(options) {
  // 初始化路由状态
  const START_LOCATION_NORMALIZED = {
    path: '/',
    matched: [],
    meta: {},
    // ... 其他路由属性
  }
  
  // 核心响应式数据
  const currentRoute = shallowRef(START_LOCATION_NORMALIZED)
  
  const router = {
    // 暴露当前路由为只读属性
    get currentRoute() {
      return currentRoute.value
    },
    // ... 其他方法
  }
  
  return router
}

为什么使用 shallowRef 而不是 ref?因为路由对象结构较深,shallowRef 只代理 .value 的变更,内部属性变更不需要触发响应式,这样可以获得更好的性能。

路由响应式数据的使用

Vue Router 通过依赖注入将响应式路由数据提供给所有组件:

javascript 复制代码
install(app) {
  // 注册路由实例
  app.provide(routerKey, router)
  app.provide(routeLocationKey, reactive(this.currentRoute))
  
  // 注册全局组件
  app.component('RouterLink', RouterLink)
  app.component('RouterView', RouterView)
  
  // 在原型上挂载 $router 和 $route
  app.config.globalProperties.$router = router
  app.config.globalProperties.$route = reactive(this.currentRoute)
}

这样,我们在组件中就可以通过 $route 或 useRoute() 访问响应式路由数据:

html 复制代码
<script setup>
import { useRoute } from 'vue-router'

// 返回一个响应式对象,当路由变化时会自动更新
const route = useRoute()

console.log(route.path) // 当前路径
console.log(route.params) // 路由参数
</script>

<template>
  <div>当前路径: {{ $route.path }}</div>
</template>

路由变化时如何触发更新

当路由发生变化时,Vue Router 会更新 currentRoute.value,从而触发所有依赖的重新渲染:

javascript 复制代码
// 路由导航的核心逻辑
async function navigate(to, from) {
  // ... 执行导航守卫、解析组件等
  
  // 更新当前路由(触发响应式更新)
  currentRoute.value = to
  
  // 调用 afterEach 钩子
  callAfterEachGuards(to, from)
}

router-view 组件的渲染原理

router-view 的作用

router-view 是一个函数式组件,它的核心职责是:根据当前路由的匹配结果,渲染对应的组件:

html 复制代码
<template>
  <div id="app">
    <!-- 路由匹配的组件会在这里渲染 -->
    <router-view></router-view>
  </div>
</template>

router-view 的源码实现

javascript 复制代码
const RouterView = defineComponent({
  name: 'RouterView',
  setup(props, { attrs, slots }) {
    // 注入路由实例和当前路由
    const injectedRoute = inject(routeLocationKey)
    const router = inject(routerKey)
    
    // 获取深度(用于嵌套路由)
    const depth = inject(viewDepthKey, 0)
    const matchedRouteRef = computed(() => {
      // 获取当前深度对应的匹配记录
      const matched = injectedRoute.matched[depth]
      return matched
    })
    
    // 提供下一层的 depth
    provide(viewDepthKey, depth + 1)
    
    return () => {
      const match = matchedRouteRef.value
      const component = match?.components?.default
      
      if (!component) {
        return slots.default?.() || null
      }
      
      // 渲染匹配到的组件
      return h(component, {
        ...attrs,
        ref: match.instances?.default,
      })
    }
  }
})

嵌套路由的处理

router-view 通过 depth 参数支持嵌套路由:

html 复制代码
<template>
  <div>
    <h1>用户中心</h1>
    <!-- 默认 depth = 1,会渲染子路由组件 -->
    <router-view></router-view>
  </div>
</template>

每个嵌套的 router-view 都会通过 provide/inject 获得递增的深度值,从而从 matched 数组中取出对应的组件记录。

路由钩子的实现机制

钩子函数分类

Vue Router 提供了三类导航守卫:

  • 全局守卫:beforeEach、beforeResolve、afterEach
  • 路由独享守卫:beforeEnter
  • 组件内守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave

钩子执行流程源码简析

javascript 复制代码
async function navigate(to, from) {
  const guards = []
 
  // 收集所有需要执行的守卫,并按顺序执行
  // 1. 执行 beforeRouteLeave(从最深的路由记录开始)
  const leaveGuards = extractLeaveGuards(from.matched)
  guards.push(...leaveGuards)
  
  // 2. 执行全局 beforeEach
  guards.push(router.beforeEachGuards)
  
  // 3. 执行 beforeRouteUpdate(如果组件复用)
  const updateGuards = extractUpdateGuards(from.matched, to.matched)
  guards.push(...updateGuards)
  
  // 4. 执行路由配置的 beforeEnter
  const enterGuards = extractEnterGuards(to.matched)
  guards.push(...enterGuards)
  
  // 5. 执行全局 beforeResolve
  guards.push(router.beforeResolveGuards)
  
  // 串行执行所有守卫
  for (const guard of guards) {
    const result = await guard(to, from)
    // 如果守卫返回 false 或重定向路径,中断导航
    if (result === false || typeof result === 'string') {
      return result
    }
  }
  
  // 6. 执行全局 afterEach(不阻塞导航)
  callAfterEachGuards(to, from)
}

组件内守卫的实现

组件内守卫通过 Vue 的生命周期钩子集成:

javascript 复制代码
// 组件内守卫的注册
export default {
  beforeRouteEnter(to, from, next) {
    // 在渲染前调用,不能访问 this
    // 可以通过 next 回调访问组件实例
    next(vm => {
      // 通过 `vm` 访问组件实例
    })
  },
  beforeRouteUpdate(to, from, next) {
    // 路由改变但组件复用时调用
    // 可以访问 this
  },
  beforeRouteLeave(to, from, next) {
    // 离开路由时调用
    // 可以访问 this
  }
}

Hash模式 vs History模式

两种模式的本质区别

Vue Router 支持两种路由模式:

模式 创建方式 URL格式 服务器配置 原理
Hash createWebHashHistory() /#/home 不需要 监听 hashchange 事件 + pushState
History createWebHistory() /home 需要 HTML5 History API

Hash模式的实现

javascript 复制代码
// hash.js - Hash模式实现
function createWebHashHistory(base = '') {
  // Hash模式本质上是在 History 模式基础上加了 '#' 前缀
  return createWebHistory(base ? base : '#')
}

// 处理 Hash 路径
function getHashLocation() {
  const hash = window.location.hash.slice(1) // 去掉开头的 '#'
  return hash || '/' // 空 hash 返回根路径
}

// 监听 hash 变化
window.addEventListener('hashchange', () => {
  const to = getHashLocation()
  // 更新路由状态
  changeLocation(to)
})

注:在 Vue Router 4 中,Hash 模式也统一使用 History API 进行导航,hashchange 仅作为兜底监听。

History模式的实现

javascript 复制代码
// html5.js - History模式实现
function createWebHistory(base = '') {
  // 创建状态管理器
  const historyState = useHistoryState()
  const currentLocation = ref(createCurrentLocation(base))
  
  // 监听 popstate 事件
  window.addEventListener('popstate', (event) => {
    const to = createCurrentLocation(base)
    currentLocation.value = to
    // 触发路由更新
  })
  
  function push(to) {
    // 调用 history.pushState
    window.history.pushState({}, '', to)
    currentLocation.value = to
  }
  
  function replace(to) {
    window.history.replaceState({}, '', to)
    currentLocation.value = to
  }
  
  return {
    location: currentLocation,
    push,
    replace
  }
}

History模式的服务器配置

History 模式需要服务器配置支持,否则刷新页面会 404。Nginx 配置示例:

bash 复制代码
location / {
  try_files $uri $uri/ /index.html;
}

createRouter核心逻辑源码简析

createRouter的整体结构

javascript 复制代码
function createRouter(options) {
  // 1. 创建路由匹配器
  const matcher = createRouterMatcher(options.routes)
  
  // 2. 创建响应式路由状态
  const currentRoute = shallowRef(START_LOCATION)
  
  // 3. 根据模式创建 history 实例
  const history = options.history
  
  // 4. 定义路由方法
  const router = {
    // 响应式路由
    currentRoute,
    
    // 导航方法
    push(to) {
      return pushWithRedirect(to)
    },
    
    replace(to) {
      return push(to, true)
    },
    
    // 后退
    back() {
      history.go(-1)
    },
    
    // 前进
    forward() {
      history.go(1)
    },
    
    // 插件安装方法
    install(app) {
      // 提供路由实例
      app.provide(routerKey, router)
      app.provide(routeLocationKey, reactive(currentRoute))
      
      // 注册全局组件
      app.component('RouterLink', RouterLink)
      app.component('RouterView', RouterView)
      
      // 挂载到全局属性
      app.config.globalProperties.$router = router
      app.config.globalProperties.$route = reactive(currentRoute)
      
      // 初始化路由
      if (currentRoute.value === START_LOCATION) {
        // 解析初始路径
        history.replace(history.location)
      }
    }
  }
  
  return router
}

createRouterMatcher的实现

路由匹配器负责将配置的路由表拍平,建立父子关系:

javascript 复制代码
function createRouterMatcher(routes) {
  const matchers = []
  
  // 递归添加路由记录
  function addRoute(record, parent) {
    // 标准化路由记录
    const normalizedRecord = normalizeRouteRecord(record)
    
    // 创建匹配器
    const matcher = createRouteRecordMatcher(normalizedRecord, parent)
    
    // 处理子路由
    if (normalizedRecord.children) {
      for (const child of normalizedRecord.children) {
        addRoute(child, matcher)
      }
    }
    
    matchers.push(matcher)
  }
  
  // 初始化所有路由
  routes.forEach(route => addRoute(route))
  
  // 解析路径,返回匹配的路由记录
  function resolve(location) {
    const matched = []
    let path = location.path
    
    // 找到匹配的 matcher
    for (const matcher of matchers) {
      if (path.startsWith(matcher.path)) {
        matched.push(matcher.record)
      }
    }
    
    return {
      path,
      matched
    }
  }
  
  return {
    addRoute,
    resolve
  }
}

手写简易路由实现

javascript 复制代码
// 简易路由实现
import { ref, shallowRef, reactive, computed, provide, inject } from 'vue'

const ROUTER_KEY = '__router__'
const ROUTE_KEY = '__route__'

// 创建路由
function createRouter(options) {
  // 1. 创建匹配器
  const matcher = createMatcher(options.routes)
  
  // 2. 响应式路由状态
  const currentRoute = shallowRef({
    path: '/',
    matched: []
  })
  
  // 3. 处理历史模式
  const history = options.history
  
  // 4. 监听 popstate
  window.addEventListener('popstate', () => {
    const path = window.location.pathname
    const matched = matcher.match(path)
    currentRoute.value = { path, matched }
  })
  
  // 5. 导航方法
  function push(path) {
    window.history.pushState({}, '', path)
    const matched = matcher.match(path)
    currentRoute.value = { path, matched }
  }
  
  const router = {
    currentRoute,
    push,
    install(app) {
      app.provide(ROUTER_KEY, router)
      app.provide(ROUTE_KEY, reactive(currentRoute))
      
      app.component('RouterLink', {
        props: { to: String },
        setup(props, { slots }) {
          const router = inject(ROUTER_KEY)
          return () => (
            h('a', {
              href: props.to,
              onClick: (e) => {
                e.preventDefault()
                router.push(props.to)
              }
            }, slots.default?.())
          )
        }
      })
      
      app.component('RouterView', {
        setup() {
          const route = inject(ROUTE_KEY)
          const depth = inject('depth', 0)
          provide('depth', depth + 1)
          
          return () => {
            const component = route.value.matched[depth]?.component
            return component ? h(component) : null
          }
        }
      })
    }
  }
  
  return router
}

// 简易匹配器
function createMatcher(routes) {
  const records = []
  
  function normalize(route, parent) {
    const record = {
      path: parent ? parent.path + route.path : route.path,
      component: route.component,
      parent
    }
    
    records.push(record)
    
    if (route.children) {
      route.children.forEach(child => normalize(child, record))
    }
  }
  
  routes.forEach(route => normalize(route))
  
  return {
    match(path) {
      return records.filter(record => path.startsWith(record.path))
    }
  }
}

性能优化与最佳实践

路由懒加载

javascript 复制代码
const routes = [
  {
    path: '/dashboard',
    // 使用动态导入实现懒加载
    component: () => import('./views/Dashboard.vue')
  }
]

避免不必要的响应式开销

javascript 复制代码
// 如果只需要一次性值,可以不用解构
const route = useRoute()
// ❌ 避免:每次路由变化都会重新计算
const id = computed(() => route.params.id)

// ✅ 推荐:直接在需要的地方使用
watch(() => route.params.id, (newId) => {
  // 只在变化时执行
})

路由守卫的最佳实践

javascript 复制代码
// 全局前置守卫:适合做权限验证
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next('/login')
  } else {
    next()
  }
})

// 组件内守卫:适合做数据预加载
beforeRouteEnter(to, from, next) {
  fetchData(to.params.id).then(data => {
    next(vm => vm.data = data)
  })
}

结语

Vue Router 与响应式系统的集成是 Vue 生态中最精妙的设计之一,理解这些原理不仅帮助我们更好地使用 Vue Router,也为处理复杂路由场景(如权限控制、动态路由、嵌套路由等)提供了理论基础。在实际开发中,合理利用路由响应式特性和导航守卫,可以构建出既高效又易维护的单页应用。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
Ruihong2 小时前
《VuReact:下一代 Vue 3 -> React 智能编译工具,支持 SFC 与增量迁移》
vue.js
FansUnion2 小时前
用 AI 自动生成壁纸标题、描述和 SEO Slug
javascript
青青家的小灰灰2 小时前
金三银四面试官最想听的 React 答案:虚拟 DOM、Hooks 陷阱与大型列表优化
前端·react.js·面试
HelloReader2 小时前
深入理解 Tauri 架构与应用体积优化实战指南
前端
lemon_yyds2 小时前
vue 2 升级vue3 : ref 和 v-model 命名为同名
前端·vue.js
codingWhat2 小时前
小程序里「嵌」H5:一套完整可落地的 WebView 集成方案
前端·uni-app·webview
重庆穿山甲2 小时前
Java开发者的大模型入门:Spring AI Alibaba组件全攻略(二)
前端·后端
光影少年2 小时前
在 React 中,什么情况下需要用 useCallback 和 useMemo?它们的区别是什么?
前端·react.js·掘金·金石计划
大雨还洅下2 小时前
前端 JS: async, await; Generator
javascript