普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月2日首页

从零设计一个Vue路由系统:揭秘SPA导航的核心原理

作者 北辰alk
2026年1月2日 11:30

想深入理解Vue路由?自己动手实现一个!本文将带你从零设计完整的路由系统,彻底掌握前端路由的核心原理。

前言:为什么需要前端路由?

在传统的多页面应用中,每次页面跳转都需要向服务器请求新页面,用户体验存在明显的中断感。而现代单页面应用(SPA)使用前端路由,实现了无刷新页面切换,大大提升了用户体验。

今天,我们就来亲手实现一个完整的Vue路由系统,深入理解其工作原理!

一、路由系统核心概念

1.1 路由系统三大核心

  • 路由器(Router):管理所有路由规则和状态
  • 路由表(Routes):定义路径与组件的映射关系
  • 路由视图(RouterView):动态渲染匹配的组件

1.2 两种路由模式

Hash模式:使用URL的hash部分(#后的内容)
  示例:http://example.com/#/home
  优点:兼容性好,无需服务器配置
  
History模式:使用HTML5 History API
  示例:http://example.com/home
  优点:URL更美观,更符合传统URL习惯

二、路由系统架构设计

2.1 系统架构图

graph TD
    A[URL变化] --> B{路由模式}
    B -->|Hash模式| C[监听hashchange事件]
    B -->|History模式| D[监听popstate事件]
    C --> E[解析当前路径]
    D --> E
    E --> F[匹配路由规则]
    F --> G[执行导航守卫]
    G --> H[更新路由状态]
    H --> I[渲染对应组件]
    I --> J[RouterView更新]

2.2 核心类设计

class VueRouter {
  constructor(options) {
    this.mode = options.mode || 'hash'
    this.routes = options.routes || []
    this.current = { path: '/', matched: [] }
    this.routeMap = this.createRouteMap()
    this.init()
  }
}

三、完整实现步骤

3.1 创建路由映射表

class VueRouter {
  constructor(options) {
    this.options = options
    this.routeMap = {}
    this.current = {
      path: '/',
      query: {},
      params: {},
      fullPath: '/',
      matched: []
    }
    
    // 创建路由映射表
    this.createRouteMap(options.routes || [])
    
    // 初始化路由
    this.init()
  }
  
  createRouteMap(routes, parentPath = '') {
    routes.forEach(route => {
      const record = {
        path: parentPath + route.path,
        component: route.component,
        parent: parentPath,
        meta: route.meta || {}
      }
      
      // 存储路由记录
      const normalizedPath = this.normalizePath(record.path)
      this.routeMap[normalizedPath] = record
      
      // 递归处理嵌套路由
      if (route.children) {
        this.createRouteMap(route.children, record.path + '/')
      }
    })
  }
  
  normalizePath(path) {
    // 处理路径格式:确保以/开头,不以/结尾(除了根路径)
    let normalized = path.replace(/\/+$/, '') || '/'
    if (!normalized.startsWith('/')) {
      normalized = '/' + normalized
    }
    return normalized
  }
}

3.2 实现路由模式

class VueRouter {
  init() {
    if (this.options.mode === 'history') {
      this.initHistoryMode()
    } else {
      this.initHashMode()
    }
  }
  
  initHashMode() {
    // 确保hash以/#/开头
    if (!location.hash) {
      location.hash = '/'
    }
    
    // 初始加载
    window.addEventListener('load', () => {
      this.transitionTo(this.getHash())
    })
    
    // 监听hash变化
    window.addEventListener('hashchange', () => {
      this.transitionTo(this.getHash())
    })
  }
  
  initHistoryMode() {
    // 初始加载
    window.addEventListener('load', () => {
      this.transitionTo(this.getPath())
    })
    
    // 监听popstate事件(浏览器前进后退)
    window.addEventListener('popstate', () => {
      this.transitionTo(this.getPath())
    })
  }
  
  getHash() {
    const hash = location.hash.slice(1)
    return hash || '/'
  }
  
  getPath() {
    const path = location.pathname + location.search
    return path || '/'
  }
}

3.3 实现路由匹配算法

class VueRouter {
  match(path) {
    const matched = []
    const params = {}
    
    // 查找匹配的路由记录
    let routeRecord = this.findRouteRecord(path)
    
    // 收集所有匹配的路由记录(包括父路由)
    while (routeRecord) {
      matched.unshift(routeRecord)
      routeRecord = this.routeMap[routeRecord.parent] || null
    }
    
    // 解析路径参数(动态路由)
    if (path.includes(':')) {
      this.extractParams(path, matched[matched.length - 1], params)
    }
    
    // 解析查询参数
    const query = this.extractQuery(path)
    
    return {
      path: this.normalizePath(path.split('?')[0]),
      fullPath: path,
      matched,
      params,
      query
    }
  }
  
  findRouteRecord(path) {
    const pathWithoutQuery = path.split('?')[0]
    const normalizedPath = this.normalizePath(pathWithoutQuery)
    
    // 精确匹配
    if (this.routeMap[normalizedPath]) {
      return this.routeMap[normalizedPath]
    }
    
    // 动态路由匹配(如 /user/:id)
    for (const routePath in this.routeMap) {
      if (this.isDynamicRoute(routePath)) {
        const pattern = this.pathToRegexp(routePath)
        if (pattern.test(normalizedPath)) {
          return this.routeMap[routePath]
        }
      }
    }
    
    return null
  }
  
  isDynamicRoute(path) {
    return path.includes(':')
  }
  
  pathToRegexp(path) {
    // 将路径模式转换为正则表达式
    const keys = []
    const pattern = path
      .replace(/\/:(\w+)/g, (_, key) => {
        keys.push(key)
        return '/([^/]+)'
      })
      .replace(/\//g, '\\/')
    
    return new RegExp(`^${pattern}$`)
  }
  
  extractParams(path, routeRecord, params) {
    const pathParts = path.split('/')
    const routeParts = routeRecord.path.split('/')
    
    routeParts.forEach((part, index) => {
      if (part.startsWith(':')) {
        const key = part.slice(1)
        params[key] = pathParts[index] || ''
      }
    })
  }
  
  extractQuery(path) {
    const query = {}
    const queryString = path.split('?')[1]
    
    if (queryString) {
      queryString.split('&').forEach(pair => {
        const [key, value] = pair.split('=')
        if (key) {
          query[decodeURIComponent(key)] = decodeURIComponent(value || '')
        }
      })
    }
    
    return query
  }
}

3.4 实现路由导航

class VueRouter {
  transitionTo(path, onComplete) {
    const route = this.match(path)
    
    // 导航守卫(简化版)
    const guards = this.runQueue(this.beforeHooks, route)
    
    guards.then(() => {
      // 更新当前路由
      this.current = route
      
      // 触发路由变化
      this.cb && this.cb(route)
      
      // 更新URL
      this.ensureURL()
      
      // 完成回调
      onComplete && onComplete()
    }).catch(() => {
      // 导航取消
      console.log('Navigation cancelled')
    })
  }
  
  push(location) {
    if (this.options.mode === 'history') {
      window.history.pushState({}, '', location)
      this.transitionTo(location)
    } else {
      window.location.hash = location
    }
  }
  
  replace(location) {
    if (this.options.mode === 'history') {
      window.history.replaceState({}, '', location)
      this.transitionTo(location)
    } else {
      const hash = location.startsWith('#') ? location : '#' + location
      window.location.replace(
        window.location.pathname + window.location.search + hash
      )
    }
  }
  
  go(n) {
    window.history.go(n)
  }
  
  back() {
    this.go(-1)
  }
  
  forward() {
    this.go(1)
  }
  
  ensureURL() {
    if (this.options.mode === 'history') {
      if (window.location.pathname !== this.current.path) {
        window.history.replaceState({}, '', this.current.fullPath)
      }
    } else {
      const currentHash = '#' + this.current.path
      if (window.location.hash !== currentHash) {
        window.location.replace(
          window.location.pathname + window.location.search + currentHash
        )
      }
    }
  }
}

3.5 实现RouterView组件

// RouterView组件实现
const RouterView = {
  name: 'RouterView',
  functional: true,
  render(_, { props, children, parent, data }) {
    // 标记为路由组件
    data.routerView = true
    
    // 获取当前路由匹配的组件
    const route = parent.$route
    const matchedComponents = route.matched.map(record => record.component)
    
    // 计算当前渲染深度(处理嵌套路由)
    let depth = 0
    let parentNode = parent
    while (parentNode && parentNode !== parent.$root) {
      if (parentNode.$vnode && parentNode.$vnode.data.routerView) {
        depth++
      }
      parentNode = parentNode.$parent
    }
    
    // 获取对应层级的组件
    const component = matchedComponents[depth]
    
    if (!component) {
      return children || []
    }
    
    // 渲染组件
    return createElement(component, data)
  }
}

3.6 实现RouterLink组件

// RouterLink组件实现
const RouterLink = {
  name: 'RouterLink',
  props: {
    to: {
      type: [String, Object],
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    activeClass: {
      type: String,
      default: 'router-link-active'
    },
    exactActiveClass: {
      type: String,
      default: 'router-link-exact-active'
    },
    replace: Boolean
  },
  render(h) {
    // 解析目标路由
    const router = this.$router
    const current = this.$route
    const { location, route } = router.resolve(this.to, current)
    
    // 生成href
    const href = router.options.mode === 'hash' 
      ? '#' + route.fullPath 
      : route.fullPath
    
    // 判断是否激活
    const isExact = current.path === route.path
    const isActive = this.exact ? isExact : current.path.startsWith(route.path)
    
    // 类名处理
    const classObj = {}
    if (this.activeClass) {
      classObj[this.activeClass] = isActive
    }
    if (this.exactActiveClass) {
      classObj[this.exactActiveClass] = isExact
    }
    
    // 点击处理
    const handler = e => {
      if (e.metaKey || e.ctrlKey || e.shiftKey) return
      if (e.defaultPrevented) return
      e.preventDefault()
      
      if (this.replace) {
        router.replace(location)
      } else {
        router.push(location)
      }
    }
    
    // 创建子元素
    const children = this.$slots.default || [this.to]
    
    const data = {
      class: classObj,
      attrs: {
        href
      },
      on: {
        click: handler
      }
    }
    
    return h(this.tag, data, children)
  }
}

3.7 Vue插件集成

// Vue插件安装
VueRouter.install = function(Vue) {
  // 混入$router和$route
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        // 根实例
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        
        // 响应式定义$route
        Vue.util.defineReactive(this, '_route', this._router.current)
      } else {
        // 子组件
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
    }
  })
  
  // 定义$router和$route属性
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router
    }
  })
  
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route
    }
  })
  
  // 注册全局组件
  Vue.component('RouterView', RouterView)
  Vue.component('RouterLink', RouterLink)
}

四、使用示例

4.1 基本使用

// 1. 定义路由组件
const Home = { template: '<div>Home Page</div>' }
const About = { template: '<div>About Page</div>' }
const User = { 
  template: `
    <div>
      <h2>User {{ $route.params.id }}</h2>
      <router-view></router-view>
    </div>
  `
}
const Profile = { template: '<div>User Profile</div>' }

// 2. 创建路由实例
const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { 
      path: '/user/:id', 
      component: User,
      children: [
        { path: 'profile', component: Profile }
      ]
    }
  ]
})

// 3. 创建Vue实例
const app = new Vue({
  router,
  template: `
    <div id="app">
      <nav>
        <router-link to="/">Home</router-link>
        <router-link to="/about">About</router-link>
        <router-link to="/user/123">User 123</router-link>
      </nav>
      <router-view></router-view>
    </div>
  `
}).$mount('#app')

4.2 导航守卫示例

// 全局前置守卫
router.beforeEach((to, from, next) => {
  console.log(`Navigating from ${from.path} to ${to.path}`)
  
  // 检查是否需要登录
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next('/login')
  } else {
    next()
  }
})

// 全局后置钩子
router.afterEach((to, from) => {
  // 页面标题
  document.title = to.meta.title || 'My App'
  
  // 发送页面浏览统计
  trackPageView(to.path)
})

五、性能优化与高级特性

5.1 路由懒加载

// 动态导入组件(Webpack代码分割)
const User = () => import('./views/User.vue')

const router = new VueRouter({
  routes: [
    { 
      path: '/user/:id', 
      component: User,
      meta: {
        preload: true // 自定义预加载策略
      }
    }
  ]
})

// 实现预加载策略
router.onReady(() => {
  // 预加载匹配的路由组件
  router.getMatchedComponents().forEach(component => {
    if (component && component.preload) {
      component()
    }
  })
})

5.2 滚动行为控制

const router = new VueRouter({
  scrollBehavior(to, from, savedPosition) {
    // 返回滚动位置
    if (savedPosition) {
      return savedPosition
    }
    
    // 锚点导航
    if (to.hash) {
      return {
        selector: to.hash,
        behavior: 'smooth'
      }
    }
    
    // 页面顶部
    return { x: 0, y: 0 }
  }
})

六、完整流程图

graph TB
    subgraph "初始化阶段"
        A[创建VueRouter实例] --> B[创建路由映射表]
        B --> C[选择路由模式]
        C --> D[初始化事件监听]
    end
    
    subgraph "导航过程"
        E[触发导航] --> F{路由模式}
        F -->|Hash| G[hashchange事件]
        F -->|History| H[popstate/API调用]
        G --> I[解析目标路径]
        H --> I
        I --> J[路由匹配]
        J --> K[执行导航守卫]
        K --> L{守卫结果}
        L -->|通过| M[更新路由状态]
        L -->|取消| N[导航中止]
        M --> O[触发响应式更新]
        O --> P[RouterView重新渲染]
        P --> Q[完成导航]
    end
    
    subgraph "组件渲染"
        R[RouterView组件] --> S[计算渲染深度]
        S --> T[获取匹配组件]
        T --> U[渲染组件]
    end

七、总结

通过自己动手实现一个Vue路由系统,我们可以深入理解:

  1. 路由的核心原理:URL与组件的映射关系
  2. 两种模式的区别:Hash与History的实现差异
  3. 导航的生命周期:从触发到渲染的完整流程
  4. 组件的渲染机制:RouterView如何处理嵌套路由

这个实现虽然简化了官方Vue Router的一些复杂特性,但涵盖了最核心的功能。理解了这些基本原理后,无论是使用Vue Router还是排查相关问题时,都会更加得心应手。

Vue 3 深度解析:watch 与 watchEffect 的终极对决

作者 北辰alk
2026年1月1日 22:00

Vue 3 深度解析:watch 与 watchEffect 的终极对决

在 Vue 3 的响应式系统中,watchwatchEffect 是两个强大的工具,但许多开发者对它们的使用场景和区别感到困惑。今天,我们将深入剖析这两个API,帮助你做出明智的选择!

一、基础概念解析

1.1 watch:精准的响应式侦探

watch 是一个相对"精确"的观察者,它需要你明确指定要监听的源(source)和回调函数。

import { ref, watch } from 'vue'

const count = ref(0)
const user = ref({ name: '小明', age: 25 })

// 监听单个ref
watch(count, (newVal, oldVal) => {
  console.log(`计数从 ${oldVal} 变为 ${newVal}`)
})

// 监听响应式对象
watch(
  () => user.value.age,
  (newAge, oldAge) => {
    console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
  }
)

// 监听多个源
watch([count, () => user.value.age], ([newCount, newAge], [oldCount, oldAge]) => {
  console.log(`计数或年龄发生变化`)
})

1.2 watchEffect:自动的依赖收集器

watchEffect 则更加"智能"和"自动",它会自动追踪其回调函数内部访问的所有响应式依赖。

import { ref, watchEffect } from 'vue'

const count = ref(0)
const double = ref(0)

watchEffect(() => {
  // 自动追踪 count.value 和 double.value
  double.value = count.value * 2
  console.log(`count: ${count.value}, double: ${double.value}`)
})

// 自动执行,输出: count: 0, double: 0
count.value++ // 自动触发,输出: count: 1, double: 2

二、核心区别对比

让我用一个流程图来展示它们的工作机制差异:

graph TD
    A[开始监听] --> B{选择哪种方式?}
    
    B -->|精确监听特定数据| C[使用 watch]
    B -->|自动追踪依赖| D[使用 watchEffect]
    
    C --> C1[明确指定依赖源]
    C1 --> C2[初始不执行<br/>除非设置 immediate: true]
    C2 --> C3[回调参数包含<br/>新旧值]
    C3 --> C4[需要手动清理?]
    C4 -->|是| C5[返回清理函数]
    C4 -->|否| C6[监听完成]
    
    D --> D1[执行回调函数]
    D1 --> D2[自动收集依赖]
    D2 --> D3[依赖变化时<br/>重新执行]
    D3 --> D4[无新旧值参数<br/>只有最新值]
    D4 --> D6[监听完成]
    
    C5 --> C6

2.1 依赖追踪方式不同

watch 需要显式声明依赖:

// 必须明确指定要监听什么
watch(count, callback) // 直接监听ref
watch(() => obj.prop, callback) // 监听getter函数
watch([source1, source2], callback) // 监听数组

watchEffect 自动收集依赖:

// 自动发现内部使用的所有响应式数据
watchEffect(() => {
  // 这里用到的所有响应式数据都会被自动追踪
  console.log(count.value + user.value.age)
})

2.2 初始执行时机不同

const data = ref(null)

// watch 默认不会立即执行
watch(data, (newVal) => {
  console.log('数据变化:', newVal)
})
// 需要 immediate: true 才会立即执行
watch(data, callback, { immediate: true })

// watchEffect 总是立即执行一次
watchEffect(() => {
  console.log('数据:', data.value) // 立即执行
})

2.3 访问新旧值的方式不同

const count = ref(0)

// watch 可以访问新旧值
watch(count, (newVal, oldVal) => {
  console.log(`从 ${oldVal} 变为 ${newVal}`)
})

// watchEffect 只能访问当前值
watchEffect(() => {
  console.log(`当前值: ${count.value}`)
  // 无法直接获取旧值
})

2.4 停止监听的方式

两种方式都返回停止函数:

// watch 的停止方式
const stopWatch = watch(count, callback)
stopWatch() // 停止监听

// watchEffect 的停止方式
const stopEffect = watchEffect(callback)
stopEffect() // 停止监听

三、实际应用场景

场景1:表单验证(适合使用 watch)

import { ref, watch } from 'vue'

const form = ref({
  username: '',
  email: '',
  password: ''
})

const errors = ref({})

// 监听用户名变化
watch(
  () => form.value.username,
  (newUsername) => {
    if (newUsername.length < 3) {
      errors.value.username = '用户名至少3个字符'
    } else {
      delete errors.value.username
    }
  },
  { immediate: true } // 立即执行以验证初始值
)

// 监听邮箱格式
watch(
  () => form.value.email,
  (newEmail) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!emailRegex.test(newEmail)) {
      errors.value.email = '邮箱格式不正确'
    } else {
      delete errors.value.email
    }
  }
)

场景2:自动保存(适合使用 watchEffect)

import { ref, watchEffect } from 'vue'

const document = ref({
  title: '未命名文档',
  content: '',
  lastSaved: null
})

let saveTimeout = null

// 自动追踪文档变化并保存
watchEffect((onCleanup) => {
  // 清理之前的定时器
  onCleanup(() => {
    if (saveTimeout) clearTimeout(saveTimeout)
  })
  
  // 设置新的定时器
  saveTimeout = setTimeout(async () => {
    if (document.value.content.trim()) {
      try {
        await saveToServer(document.value)
        document.value.lastSaved = new Date()
        console.log('文档已自动保存')
      } catch (error) {
        console.error('保存失败:', error)
      }
    }
  }, 1000) // 防抖:1秒后保存
})

场景3:组合使用

import { ref, watch, watchEffect } from 'vue'

const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)

// watchEffect:自动管理 loading 状态和请求
const stopEffect = watchEffect(async (onCleanup) => {
  if (!searchQuery.value.trim()) {
    searchResults.value = []
    return
  }
  
  isLoading.value = true
  
  // 清理函数:取消未完成的请求
  let aborted = false
  onCleanup(() => {
    aborted = true
    isLoading.value = false
  })
  
  try {
    const results = await searchAPI(searchQuery.value)
    if (!aborted) {
      searchResults.value = results
    }
  } catch (error) {
    if (!aborted) {
      console.error('搜索失败:', error)
      searchResults.value = []
    }
  } finally {
    if (!aborted) {
      isLoading.value = false
    }
  }
})

// watch:监听特定条件,执行特定操作
watch(
  () => searchResults.value.length,
  (newLength) => {
    if (newLength === 0 && searchQuery.value.trim()) {
      console.log('没有找到相关结果')
    }
  }
)

四、性能优化技巧

4.1 使用 watch 的深度监听

const nestedObj = ref({
  user: {
    profile: {
      name: '小明',
      details: {
        address: '...'
      }
    }
  }
})

// 深度监听
watch(
  nestedObj,
  (newVal) => {
    console.log('对象深度变化:', newVal)
  },
  { deep: true } // 深度监听
)

// 对比:watchEffect 默认就是"深度"的,因为它追踪所有访问
watchEffect(() => {
  console.log(nestedObj.value.user.profile.details.address)
  // 任何层次的访问都会被追踪
})

4.2 使用 flush 选项控制执行时机

// 默认:组件更新前执行
watch(source, callback)

// DOM 更新后执行
watch(source, callback, { flush: 'post' })

// 同步执行(慎用)
watch(source, callback, { flush: 'sync' })

// watchEffect 也有相同的选项
watchEffect(callback, { flush: 'post' })

4.3 使用 reactive 对象的注意事项

import { reactive, watch, watchEffect, toRefs } from 'vue'

const state = reactive({
  count: 0,
  user: { name: '小明' }
})

// ❌ 错误:直接监听 reactive 对象
watch(state, callback) // 可能会得到意外结果

// ✅ 正确:使用 getter 函数
watch(() => state.count, callback)

// ✅ 正确:使用 toRefs
const { count } = toRefs(state)
watch(count, callback)

// ✅ watchEffect 可以直接使用
watchEffect(() => {
  console.log(state.count, state.user.name)
})

五、选择指南:什么时候用什么?

使用 watch 的场景:

  1. 需要新旧值对比时
  2. 只关心特定数据的变化
  3. 需要控制初始执行时机
  4. 数据变化频率高,但不需要每次变化都执行
  5. 需要监听嵌套对象的特定属性

使用 watchEffect 的场景:

  1. 依赖多个数据源,不想一一列出
  2. 副作用逻辑复杂,依赖关系动态变化
  3. 需要立即执行并响应式更新
  4. 逻辑相对独立,自成一体
  5. 进行异步操作,需要自动清理

决策流程图:

graph TD
    A[开始选择] --> B{需要监听什么?}
    
    B -->|明确的特定数据| C[考虑 watch]
    B -->|多个/动态的依赖| D[考虑 watchEffect]
    
    C --> C1{需要新旧值对比?}
    C1 -->|是| C2[选择 watch]
    C1 -->|否| C3{初始需要立即执行?}
    
    D --> D1{需要自动依赖追踪?}
    D1 -->|是| D2[选择 watchEffect]
    D1 -->|否| C3
    
    C3 -->|是且依赖简单| C4[watch + immediate]
    C3 -->|是且依赖复杂| D2
    C3 -->|否| C5[watch]
    
    C2 --> E[最终决定: watch]
    C4 --> E
    C5 --> E
    D2 --> F[最终决定: watchEffect]

六、实战总结

6.1 黄金法则

  1. 明确依赖用 watch,模糊依赖用 watchEffect
  2. 需要旧值用 watch,只要最新值用 watchEffect
  3. 初始不执行用 watch,立即执行用 watchEffect
  4. 简单监听用 watch,复杂副作用用 watchEffect

6.2 代码示例对比

// 场景:用户过滤和排序
const users = ref([])
const filter = ref('')
const sortBy = ref('name')

// 方案1:使用 watch(明确)
watch([filter, sortBy], () => {
  fetchFilteredUsers(filter.value, sortBy.value)
}, { immediate: true })

// 方案2:使用 watchEffect(自动)
watchEffect(() => {
  fetchFilteredUsers(filter.value, sortBy.value)
})

// 结论:两者都能工作,根据喜好选择

6.3 最佳实践建议

  1. 在组合式函数中优先使用 watchEffect,因为它更符合响应式思维
  2. 在需要精确控制时使用 watch,特别是需要防抖或节流时
  3. 记得清理副作用,特别是定时器和异步操作
  4. 谨慎使用 deep 和 flush 选项,它们可能影响性能
  5. 在 Vue 3.2+ 中考虑使用 watchPostEffect 和 watchSyncEffect 作为语法糖

结语

watchwatchEffect 都是 Vue 3 响应式系统的强大工具,没有绝对的优劣,只有适合的场景。理解它们的核心差异,结合具体需求选择,才能写出更优雅、高效的代码。

记住:watch 是"我告诉你监听什么",而 watchEffect 是"你自己发现需要监听什么"。

希望这篇深度解析能帮助你在 Vue 3 开发中做出更明智的选择!如果你有更多疑问,欢迎在评论区留言讨论。

❌
❌