vue开发中核心API

一、watch(侦听器)

1. 核心原理

  • Vue2 :为每个被监听表达式创建一个 Watcher 实例,读取表达式时触发依赖收集,表达式返回的值变化时,调度 Watcher 执行回调。
  • Vue3 :底层依赖 effect 和作用域,支持侦听 refreactive、getter 及多源数组,支持副作用清理。

2. Vue2 Options API 中的 watch

参数(对象形式)

javascript 复制代码
watch: {
  source: {
    handler(newVal, oldVal) { /* ... */ },
    deep: true,        // 深度监听,递归遍历所有子属性
    immediate: true,   // 立即以当前值执行一次回调
    flush: 'pre'       // (Vue3 有) 回调的刷新时机,Vue2 无
  }
}

使用场景:表单监听、路由参数变化请求数据、需要旧值对比的业务。

代码示例

javascript 复制代码
export default {
  data() {
    return {
      keyword: '',
      form: { name: '', age: 0 }
    }
  },
  watch: {
    keyword(newVal, oldVal) {
      // 搜索防抖
      this.search(newVal)
    },
    'form.name': {
      handler(newVal, oldVal) { /* 监听嵌套属性 */ },
      deep: false    // 字符串形式不需要 deep,直接指定路径
    },
    form: {
      handler(newVal, oldVal) {
        // 监听整个对象,需要 deep:true 才能检测内部变化
      },
      deep: true,
      immediate: true
    }
  }
}

Vue2 坑点

  • deep: true 会对对象的所有层级递归添加观察者,大对象性能极差。
  • 无法监听到对象属性的增加/删除 ,除非使用 Vue.set/delete 或直接替换对象。
  • 数组索引直接赋值 arr[0] = val 不会触发侦听器(需要用 spliceVue.set)。

3. Vue3 Composition API 中的 watch()

类型签名

typescript 复制代码
watch(
  source: Ref | Reactive | (() => any) | Array<...>,
  callback: (newVal, oldVal, onCleanup) => void,
  options?: { deep?: boolean, immediate?: boolean, flush?: 'pre' | 'post' | 'sync' }
): StopHandle

参数拆解

  • source :可以是 refreactive 对象、一个 getter 函数,或多源数组。
  • callback
    • newVal / oldVal:当 source 是 reactive 对象时,新旧值指向同一个代理对象,无法区分。
    • onCleanup:注册副作用清理函数,在下一次回调触发或侦听器停止时执行。
  • options
    • deep默认 false ,但对 reactive 对象会隐式强制深度监听 ,无法关闭。侦听 ref 或 getter 返回的基本类型,需手动 deep: true 才会深度。
    • immediate:立即执行回调。
    • flush:控制回调刷新时机。
      • 'pre'(默认):组件更新前执行,此时 DOM 未更新。
      • 'post':组件更新后执行,能访问到最新 DOM(等同 watchPostEffect)。
      • 'sync':同步触发(极少用)。
  • 返回值:调用可停止侦听的函数。

核心使用场景与代码

(1) 侦听 ref 基本类型
vue 复制代码
<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')
watch(keyword, (newVal, oldVal) => {
  // 搜索逻辑,oldVal 是变化前的值
  console.log(`从 ${oldVal} 到 ${newVal}`)
})
</script>
(2) 侦听 reactive 对象
vue 复制代码
<script setup>
import { reactive, watch } from 'vue'

const form = reactive({ name: '', age: 0 })
watch(form, (newVal, oldVal) => {
  // 注意:newVal === oldVal (同一个代理)
  // 只能拿到新值,oldVal 无效
  console.log('对象内部变化', newVal)
})
</script>

坑点watch(reactiveObj, callback) 会自动深侦听,且无法关闭。若你只想监听 form.name,建议使用 getter:watch(() => form.name, (newName) => {})

(3) 侦听 getter 函数
vue 复制代码
<script setup>
import { reactive, watch } from 'vue'
const state = reactive({ user: { name: 'Alice' } })
watch(
  () => state.user.name,
  (newName, oldName) => {
    console.log('name changed', oldName, '->', newName)
  }
)
// 如果需要深度侦听 getter 返回的对象,需 deep:true
watch(
  () => state.user,
  (newUser, oldUser) => {
    // deep: true 使内部属性变化也触发
  },
  { deep: true }
)
</script>
(4) 多源侦听
vue 复制代码
<script setup>
import { ref, reactive, watch } from 'vue'

const a = ref(1)
const b = ref(2)
watch([a, b], ([newA, newB], [oldA, oldB]) => {
  // 任一变化都会触发
})
</script>
(5) 副作用清理(onCleanup

典型场景:防抖请求、忽略过期请求。

vue 复制代码
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
watch(keyword, async (newVal, oldVal, onCleanup) => {
  let cancelled = false
  onCleanup(() => { cancelled = true }) // 注册清理函数

  const res = await fetch(`/api/search?q=${newVal}`)
  if (!cancelled) {
    // 只有未过期才更新
    result.value = res.data
  }
})
</script>

keyword 快速变化时,上一个异步请求的 onCleanup 会被执行,从而忽略过期结果,避免竞态问题。

注意事项与坑点

  • reactive 对象的 watch :回调中 oldValue 无法使用;可以用 watch(() => ({...state}), ...) 浅拷贝一份来获取旧值,但这会创建新对象,小心性能。
  • 直接侦听 reactive 属性的问题watch(state.count, ...) 是无效的,因为 state.count 解构了原始值;必须用 getter () => state.count
  • flush: 'post' 的价值 :需要在侦听器内操作 DOM 或获取元素尺寸时,设置为 'post',否则可能拿到更新前的 DOM。
  • 侦听数组 :如果数组是 reactive 的,直接 watch(array, callback) 自动深侦听,能监听到 push 等;但如果是 ref 包裹的数组,需 deep: true 或使用 getter () => [...arr.value](浅拷贝)。
  • 停止侦听 :组合式 API 中异步创建的侦听器要手动停止,否则会导致内存泄漏;在 setup 作用域内,组件卸载会自动停止,但如果在 setTimeout 中创建则不会。

二、watchEffect(Vue3 新增)

类型签名

typescript 复制代码
watchEffect(
  effect: (onCleanup) => void,
  options?: { flush?: 'pre' | 'post' | 'sync' }
): StopHandle

核心特点

  • 立即执行一次回调,并自动追踪回调内用到的所有响应式依赖。
  • 依赖变化时,重新执行回调,无需手动指定源
  • 不能获取旧值,更适合"执行副作用"的场景。

参数与场景

  • onCleanup:同样支持副作用清理。
  • flush:控制刷新时机,同 watch
  • 使用场景
    • 需要根据多个响应式状态执行副作用,且不关心旧值。
    • 动态同步 DOM 状态(如标题随数据变化:document.title = state.title)。
    • 连接第三方库(如调试日志)。

代码示例

vue 复制代码
<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)
const name = ref('Vue')

const stop = watchEffect((onCleanup) => {
  // 自动追踪 count 和 name
  console.log(`count: ${count.value}, name: ${name.value}`)

  onCleanup(() => {
    // 清理,例如取消上一次操作
  })
})
// 停止:stop()
</script>

watchEffect 与 watch 的核心差异

对比项 watch watchEffect
追踪方式 显式指定源 自动追踪回调中的响应式依赖
立即执行 默认不执行,需 immediate: true 默认立即执行
旧值 可获取 无法获取
适用场景 需要比较新旧值、精确控制监听的场景 多个依赖驱动一个副作用,或连接外部库
副作用清理 支持 onCleanup 支持 onCleanup
初始化时机 immediate: true 时在创建时立即执行 立即执行

坑点

  • watchEffect 必须在 setup 或生命周期钩子中同步调用,不能在异步回调里创建 (除非配合 effectScope),否则无法自动销毁。
  • 注意不要产生无意的循环:修改了内部依赖的数据 → 触发重新执行 → 再次修改 → 死循环。

三、computed(计算属性)

原理

Vue2/3 原理类似:内部创建一个惰性 Watcher/effect,标记 lazy: true。当依赖变化时,标记为脏数据,在下次读取时才重新计算,并缓存结果。

Vue2 Options 写法

javascript 复制代码
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName
  },
  // 带 setter
  fullName2: {
    get() { return this.firstName + ' ' + this.lastName },
    set(val) {
      const names = val.split(' ')
      this.firstName = names[0]
      this.lastName = names[1]
    }
  }
}

Vue3 Composition API

vue 复制代码
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 可写计算
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (val) => {
    [firstName.value, lastName.value] = val.split(' ')
  }
})
</script>

参数说明

  • 第一种:传入一个 getter 函数,返回只读的 ComputedRef
  • 第二种:传入 { get, set } 对象,返回可写的 ComputedRef

使用场景

  • 模板中需要从多个原始数据衍生出展示数据的场景(列表过滤、排序、合计)。
  • 多个地方复用同一逻辑,避免重复计算。
  • 依赖的数据可能不会经常变化,需要缓存机制。

注意事项与坑点

  • 计算属性必须有返回值,且依赖必须是响应式数据,否则不会自动更新。
  • 不要在 getter 中产生副作用 (如修改其他状态、异步请求),这会导致不可预测的行为。如果确实需要,改用 watch
  • 可写计算属性的 setter:修改时会触发,必须正确处理依赖数据。
  • 依赖追踪深度 :计算属性只追踪同步访问的响应式属性,如果 getter 中使用了 try...catch 或条件分支,未访问到的分支不会被追踪。
  • 性能陷阱 :如果计算属性返回一个新对象(如数组 filter),每次依赖变化都会返回新引用,可能导致子组件不必要的重新渲染。在 Vue3 中使用 v-memo 或子组件 memo 优化。

四、ref / reactive / 相关 API

1. refreactive 核心对比

API 使用方式 内部原理 模板解包 替换整个对象
ref 包装基本类型或任意值,.value 访问 class RefImpl,持有 _value,用 getter/setter 触发依赖追踪 模板中自动解包,直接写变量名 ref.value = newVal,仍保持响应式
reactive 传入对象,返回 Proxy 代理 Proxy 深度代理 模板中直接使用属性(代理本身不解包) 直接 obj = newObj 会丢失响应性,必须用 Object.assign 或重新包装

代码示例

vue 复制代码
<script setup>
import { ref, reactive } from 'vue'

const count = ref(0)       // 基本类型
const arr = ref([1,2,3])   // 数组也可用 ref

const state = reactive({
  user: { name: 'Alice' },
  list: []
})

// 模板中
// {{ count }} 自动解包
// {{ state.user.name }} 正常访问
</script>

坑点解析

  • ref 自动解包的边界 :仅在模板渲染上下文和 reactive 对象内部属性中会自动解包。在普通对象或数组里,ref 不会自动解包:

    javascript 复制代码
    const a = ref(1)
    const obj = { a }  // obj.a 是 RefImpl,不是 1,需要 .value
  • 解构 reactive 对象会丢失响应式

    javascript 复制代码
    const { user } = state  // user 是一个普通对象,不再响应式
    // 解决:使用 toRefs
    const { user } = toRefs(state) // user 变成 ref,保持响应式
  • reactive 不能代理基本类型,只能处理对象/数组。

  • reactive 根对象不可替换

    javascript 复制代码
    let state = reactive({ count: 1 })
    state = reactive({ count: 2 }) // 新的响应式对象,原引用丢失,视图不更新
    // 正确:用 Object.assign(state, { count: 2 })
  • ref 可以完全替代 reactive 吗? 可以,但大对象使用 ref 每次需要 .value,模板内深层次访问会显得啰嗦。两者配合使用最佳。

2. toRef / toRefs

作用 :解构 reactive 对象时保持单个属性的响应式。

  • toRef(obj, key):为源对象上的某个属性创建一个 ref,与源对象保持同步(修改 ref 会影响源对象)。
  • toRefs(obj):将对象所有属性转换为 ref 的普通对象,常用于返回组合式函数中的响应式状态。
vue 复制代码
<script setup>
import { reactive, toRefs } from 'vue'

const state = reactive({ count: 1, name: 'Vue' })
// 解构且保持响应式
const { count, name } = toRefs(state)
// count 现在是 Ref,在模板中 {{ count }} 自动解包

// 如果用 toRef 关联单个属性
const countRef = toRef(state, 'count')
</script>

坑点

  • toRef 创建的是一个"引用",修改 countRef.value 会直接修改源对象,但创建的新 ref 不会保持双向绑定?其实是双向的。
  • 如果源对象中该属性不存在,toRef 也会创建一个 ref,但设置值不会影响源对象(因为属性不存在),要用时确保属性已存在。

3. shallowRef / shallowReactive

原理 :只对 .value 的引用本身或根层属性做响应式处理,不进行深层递归代理。

用途:处理大型只读数据(如接口返回的巨型列表),避免深度响应式带来的性能开销。

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

const bigData = shallowRef([...]) // 内部数组元素不是响应式
// 更新:必须通过替换整个 .value 触发更新
bigData.value = newData

// 如果只需修改内部某个元素,需做浅拷贝后整体替换
bigData.value = [
  ...bigData.value.slice(0, idx),
  newItem,
  ...bigData.value.slice(idx + 1)
]

注意

  • shallowRefref 不能混用。如果在 shallowRef 内部包含 ref 元素,不会自动解包,视图中的 .value 不会消失。
  • shallowReactive 只监视第一层属性,深层属性变更不会触发更新。

4. readonly

用于保护数据不被修改,创建只读代理。Vue3 中常用于 provide 向下传递数据时,防止子组件直接修改。

javascript 复制代码
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0 })
const readOnlyState = readonly(state)
// readOnlyState.count++ 会警告并阻止

五、nextTick

原理

将回调推迟到下次 DOM 更新循环之后执行。Vue 内部使用微任务(Promise)队列实现,当数据改变后,视图更新是异步的,nextTick 保证回调在视图更新后执行。

使用场景

  • 在修改数据后立即获取更新后的 DOM 尺寸。
  • 与第三方库(如 Swiper)同步,需要 DOM 已存在时初始化。

代码示例

vue 复制代码
<script setup>
import { ref, nextTick } from 'vue'

const msg = ref('Hello')
const pRef = ref(null)

function updateAndGetHeight() {
  msg.value = 'Updated text'
  // 此时 DOM 尚未更新
  nextTick(() => {
    console.log(pRef.value.offsetHeight) // 获取新高度
  })
}
</script>

注意事项

  • nextTick 可以返回 Promise,支持 await nextTick(),这比回调更方便。
  • Vue2 中 nextTick 实现会尝试降级到 setTimeout(宏任务),Vue3 统一用 Promise(微任务)。
  • 不要在 nextTick 中修改会引起新一轮 DOM 更新的数据,这可能形成"更新-等待-再更新"的循环,虽然不阻塞,但浪费性能。

六、provide / inject

Vue2 与 Vue3 核心差异

  • Vue2:provide 可传递普通对象或 Vue.observable() 包装的对象实现响应式。默认是非响应式的。
  • Vue3:传递 refreactive 对象即可自然响应式,并且推荐结合 readonly 防止子组件直接修改。

参数解析

  • provide(key, value):父组件提供数据。
  • inject(key, defaultValue?):子组件注入。defaultValue 可以是默认值或工厂函数。

使用场景

  • 祖先组件向深层后代组件传递数据,避免层层 props 传递。
  • 封装组件库时,传递全局配置(主题、语言)。
  • 替代 Vue2 中的 EventBus(可配合事件通信)。

Vue3 代码示例

vue 复制代码
<!-- 祖父组件 -->
<script setup>
import { ref, provide, readonly } from 'vue'

const theme = ref('light')
provide('theme', readonly(theme))   // 只读,避免子组件篡改
// 如果需要允许修改,可以提供修改方法
provide('setTheme', (val) => { theme.value = val })
</script>

<!-- 孙子组件 -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light') // 第二个参数为默认值
const setTheme = inject('setTheme', () => {})
// 使用:theme.value, setTheme('dark')
</script>

注意事项与坑点

  • 响应式 :Vue3 中直接传递 refreactive 即可,Vue2 必须用 Vue.observable 或传递对象里包含响应式数据。
  • 可修改性 :如果允许子组件修改,建议通过提供方法(如 updateXxx)进行,而不是把 ref 直接暴露,便于集中控制和验证。
  • 默认值inject 的默认值仅在祖先没有提供该 key 时生效;如果祖先显式提供了 undefined,则不会使用默认值。
  • Symbol 作为 key:推荐使用 Symbol 避免命名冲突,尤其在组件库中。
  • provide 在 setup 中同步调用,不能异步提供。

七、模板 ref 与 defineExpose(Vue3)

模板 ref

用于获取子组件实例或 DOM 元素。

vue 复制代码
<template>
  <input ref="inputRef" />
  <ChildComponent ref="childRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './Child.vue'

const inputRef = ref(null)
const childRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
  // childRef.value.xxx 只能访问子组件通过 defineExpose 暴露的内容
})
</script>

defineExpose

Vue3 的 <script setup> 组件默认是封闭的,不会暴露任何内部属性给模板 ref。需要主动暴露:

vue 复制代码
<!-- Child.vue -->
<script setup>
import { ref, defineExpose } from 'vue'
const message = ref('hello')
const doSomething = () => { /* ... */ }
defineExpose({
  message,    // 暴露后父组件可通过 ref 访问
  doSomething
})
</script>

坑点

  • Vue2 的 $refs 可以访问子组件所有数据和方法,耦合度高;Vue3 默认安全,如果不写 defineExpose,父组件拿不到任何东西。
  • 模板 ref 在初次渲染时可能为 null,需要在 onMounted 或后续更新中访问。
  • ref 用在 v-for 生成的元素上时,绑定的 ref 会是一个数组(Vue3.2.25 以后),需要特别处理。

八、总结:高频面试中 API 的"灵魂拷问"

  • watch vs watchEffect:区别、何时用哪个?------上文已对比。
  • computed 能实现 watch 的功能吗? ------ 不能,computed 应无副作用且必须返回值。
  • 为什么 Vue3 的 ref 在模板中自动解包,在 reactive 对象里也自动解包,但在普通对象里不行? ------ Vue 的响应式系统通过编译和代理规则实现解包,普通对象并非响应式代理,没有 getter/setter 辅助解包。
  • 如果有一个巨大的数据对象只展示不修改,如何避免性能问题? ------ 使用 shallowRefObject.freeze 冻结数据,减少响应式拦截。
  • provide/inject 如何在 Vue2 中实现响应式? ------ 传递 Vue.observable(obj) 或传递一个具有响应式属性的对象实例。
相关推荐
jvxiao1 小时前
你真的懂作用域吗?从编译原理角度深度 JS 的作用域
前端·javascript
Darling噜啦啦1 小时前
二叉树与递归算法实战:从树结构到 LeetCode 爬楼梯,一文吃透前端数据结构与递归思维
前端·javascript·数据结构
星栈1 小时前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
前端·rust
Aolith1 小时前
React 路由守卫:我用一个组件替代了 Vue 的 beforeEach
前端·react.js
Daybreak1 小时前
从 PDD、DDD、SDD 到 TDD:我是如何用一套 Agent 工程方法论推进 My-Notion 的
前端
HjhIron2 小时前
从零实现一个待办事项应用:前端必学的Ajax与Node.js实战
前端·后端
yingyima2 小时前
JavaScript 正则表达式:从零开始的实战对比
前端
xsbcme2 小时前
VueTabRouter 插件实践(一):多标签页不是一排 TabBar
vue.js
Sammyyyyy2 小时前
月之暗面 Kimi Code 0.4.0 发布,终端 AI 编码助手全面采用 TypeScript,实现毫秒级启动
前端·javascript·人工智能·ai·typescript·servbay
范什么特西2 小时前
配置文件xml和properties
xml·前端