目录
1.Vue3 响应式原理
一、 响应式的基本概念
二、 核心机制:Proxy 和依赖追踪
三、 触发更新的过程
四、 代码示例
五、 优势总结
2.如何实现组件间通信?
一、父子组件通信
1. 父传子:Props 传递
2. 子传父:自定义事件
二、兄弟组件通信
1. 通过共同父组件中转
2. 事件总线(Event Bus)
三、跨层级通信
1. Provide/Inject(依赖注入)
2. 全局状态管理(Vuex/Pinia)
四、特殊场景方案
1.$refs 直接访问(慎用)
2.$attrs/$listeners(透传特性)
五、通信方式对比
六、实际案例参考:
3.Composition 的生命周期钩子
一、主要生命周期钩子函数
1.onBeforeMount()
2.onMounted()
3.onBeforeUpdate()
4.onUpdated()
5.onBeforeUnmount()
6.onUnmounted()
7.onErrorCaptured()
二、使用注意事项
三、代码示例
4.Composition API vs Options API
一、Options API(选项式 API)
1. 特点:
2. 示例代码:
3.优点:
4.缺点:
二、 Composition API(组合式 API)
1. 特点:
2. 示例代码:
3. 优点:
4. 缺点:
三、核心对比
四、使用场景:
5.setup() 函数作用
一、 核心作用
二、关键注意事项
6.ref 和 reactive 的区别
一、基本定义
二、主要区别
三、示例
四、使用场景
7.Props 传递机制
一、基本概念
二、 在子组件中声明 Props
声明方式:
Options API:
Composition API:
三、在父组件中传递 Props
示例:
四、 Props 的类型验证和默认值
示例:
五、单向数据流原则
六、高级用法
8.自定义事件 (emit)
一、在子组件中定义和触发事件
二、 在父组件中监听事件
三、注意事项
9.生命周期钩子对比
一、生命周期阶段与钩子对照表
二、关键变化说明
1.重命名的钩子
2.Composition API 特性
3.新增调试钩子
三、执行顺序对比(同一组件)
10.watch 和 watchEffect 的区别?
一、 基本概念
二、主要区别
三、示例
四、适用场景总结
1.watch
2.watchEffect
1.Vue3 响应式原理
Vue3 的响应式原理是其核心特性之一,它允许数据变化时自动更新视图。相比 Vue2,Vue3 使用了 JavaScript 的 Proxy 对象来实现更高效和灵活的响应式系统。下面我将逐步解释其工作机制,帮助你理解整个过程。
一、 响应式的基本概念
- 响应式系统确保当数据(如变量或对象属性)发生变化时,依赖该数据的视图或计算逻辑自动更新。这类似于数学中的函数依赖关系:如果 $y = f(x)$,那么当 $x$ 改变时,$y$ 应自动重新计算。
- Vue3 的核心是创建一个响应式代理对象,它会拦截对数据的访问(get)和修改(set)操作,从而追踪依赖并触发更新。
二、 核心机制:Proxy 和依赖追踪
- Proxy 对象:Vue3 使用 JavaScript 的 Proxy API 来包装原始数据。Proxy 可以定义“陷阱”(traps),如
get
和set
,用于拦截操作。- 当访问属性时(如
obj.a
),get
陷阱被触发,系统记录当前依赖(例如一个渲染函数)。 - 当修改属性时(如
obj.a = 1
),set
陷阱被触发,系统通知所有依赖进行更新。
- 当访问属性时(如
- 依赖追踪:Vue3 通过一个全局的“依赖收集器”来管理依赖关系。每个响应式属性都关联一个依赖集合(称为
Dep
),当属性被访问时,当前运行的“effect”函数(如组件的渲染函数)会被添加到这个集合中。- 这可以用一个简单的数学关系表示:假设有一个响应式对象 $data$,其属性 $x$ 的依赖集合为 $D_x$。当 $x$ 改变时,系统遍历 $D_x$ 并执行每个 effect 函数。
- 公式表示:如果 $effect \in D_x$,那么 $x$ 变化时 $effect()$ 被调用。
三、 触发更新的过程
- 当数据被修改时,Proxy 的
set
陷阱会执行以下步骤:
- 更新原始数据值。
- 通知依赖集合:遍历所有依赖的 effect 函数,并调用它们。
- 如果 effect 函数涉及计算属性(如 $computed = x + y$),系统会重新计算这些值。
- 优势:Proxy 支持深层嵌套对象和数组的响应式,无需像 Vue2 那样递归遍历整个对象,这提高了性能。
四、 代码示例
下面是一个简化版的 Vue3 响应式实现,使用 JavaScript 代码演示核心逻辑。注意,实际 Vue3 源码更复杂,但这里聚焦基本原理。
// 创建一个响应式对象
function reactive(target) {return new Proxy(target, {get(obj, key) {track(obj, key); // 追踪依赖:记录当前 effect 函数return obj[key];},set(obj, key, value) {obj[key] = value;trigger(obj, key); // 触发更新:通知所有依赖return true;}});
}// 依赖收集和触发函数(简化版)
const depsMap = new Map(); // 存储每个对象的依赖映射function track(target, key) {let dep = depsMap.get(target);if (!dep) {dep = new Map();depsMap.set(target, dep);}let effects = dep.get(key);if (!effects) {effects = new Set();dep.set(key, effects);}if (currentEffect) {effects.add(currentEffect); // 添加当前 effect 到依赖集合}
}function trigger(target, key) {const dep = depsMap.get(target);if (dep) {const effects = dep.get(key);if (effects) {effects.forEach(effect => effect()); // 执行所有依赖的 effect 函数}}
}// 定义一个 effect 函数(例如组件的渲染逻辑)
let currentEffect = null;
function effect(fn) {currentEffect = fn;fn(); // 首次执行,触发依赖收集currentEffect = null;
}// 使用示例
const state = reactive({ count: 0 });effect(() => {console.log(`Count is: ${state.count}`); // 依赖 state.count
});state.count = 1; // 输出:Count is: 1(自动触发更新)
五、 优势总结
- 高效性:Proxy 直接拦截操作,避免了 Vue2 中递归定义 getter/setter 的开销,支持动态添加属性。
- 灵活性:可以处理数组索引变化、Map/Set 等复杂数据结构。
- 可组合性:Vue3 的
reactive
和ref
API 使状态管理更模块化。 - 数学上,这类似于一个动态的依赖图:每个响应式属性是一个节点,依赖关系是边,变化时沿边传播更新。
2.如何实现组件间通信?
实现组件间通信是前端开发中的核心问题,不同框架有各自的解决方案。以下是主流的通信方式及代码示例:
一、父子组件通信
1. 父传子:Props 传递
// 父组件
<Child :message="parentData" />// 子组件
export default {props: ['message'],mounted() {console.log(this.message) // 接收父组件数据}
}
2. 子传父:自定义事件
// 子组件
this.$emit('child-event', data) // 触发事件// 父组件
<Child @child-event="handleEvent" />methods: {handleEvent(data) {console.log(data) // 接收子组件数据}
}
二、兄弟组件通信
1. 通过共同父组件中转
// 父组件
<ChildA @send="receiveData" />
<ChildB :data="sharedData" />data() {return { sharedData: null }
},
methods: {receiveData(data) {this.sharedData = data // 中转数据}
}
2. 事件总线(Event Bus)
// 创建事件中心
const bus = new Vue()// 组件A(发送)
bus.$emit('update', data)// 组件B(接收)
bus.$on('update', data => {console.log(data)
})
三、跨层级通信
1. Provide/Inject(依赖注入)
// 祖先组件
provide() {return { theme: 'dark' }
}// 后代组件(任意层级)
inject: ['theme'],
mounted() {console.log(this.theme) // 输出 'dark'
}
2. 全局状态管理(Vuex/Pinia)
// store.js
export default new Vuex.Store({state: { count: 0 },mutations: {increment(state) {state.count++}}
})// 任意组件
this.$store.commit('increment')
console.log(this.$store.state.count)
四、特殊场景方案
1.$refs 直接访问(慎用)
<Child ref="childComp" />methods: {callChild() {this.$refs.childComp.childMethod()}
}
2.$attrs/$listeners(透传特性)
// 父组件
<Child :title="pageTitle" @close="handleClose" />// 中间组件
<Grandchild v-bind="$attrs" v-on="$listeners" />
五、通信方式对比
方式 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
Props/Events | 父子组件 | 简单直接 | 层级深时繁琐 |
Event Bus | 任意组件 | 解耦灵活 | 难以追踪事件源 |
Vuex/Pinia | 中大型应用 | 集中管理,调试工具完善 | 增加项目复杂度 |
Provide/Inject | 深层嵌套组件 | 避免逐层传递 | 数据非响应式(需处理) |
最佳实践建议:
- 优先使用 Props/Events 处理父子通信
- 跨层级使用 Provide/Inject 替代多级 Props
- 复杂应用采用 Vuex/Pinia 管理全局状态
- 避免过度使用 $refs 和事件总线,防止代码混乱
六、实际案例参考:
// 使用Pinia状态管理
// store/counter.js
export const useCounterStore = defineStore('counter', {state: () => ({ count: 0 }),actions: {increment() {this.count++}}
})// 组件A
import { useCounterStore } from '@/store/counter'
const store = useCounterStore()
store.increment() // 组件B
const store = useCounterStore()
console.log(store.count) // 实时获取最新值
3.Composition 的生命周期钩子
在 Vue.js 的 Composition API 中,生命周期钩子提供了一种在组件不同阶段(如创建、更新、销毁)执行自定义逻辑的方式。与 Options API 不同,Composition API 使用函数式钩子(如 onMounted
、onUnmounted
),这些钩子需要在 setup()
函数或 <script setup>
语法中导入和使用。下面我将逐步解释主要钩子及其用法。
一、主要生命周期钩子函数
Composition API 提供了以下核心钩子,每个钩子对应组件生命周期的特定阶段:
1.onBeforeMount()
- 用途:在组件挂载到 DOM 之前调用。适合执行初始化操作,如设置状态或获取数据。
- 触发时机:在
setup()
函数运行后,组件首次渲染前。
2.onMounted()
- 用途:在组件挂载到 DOM 后调用。常用于访问 DOM 元素、发起 API 请求或设置事件监听器。
- 触发时机:组件首次渲染完成。
3.onBeforeUpdate()
- 用途:在组件更新之前调用。适合在状态变化前执行清理或验证逻辑。
- 触发时机:响应式数据变化后,DOM 更新前。
4.onUpdated()
- 用途:在组件更新后调用。用于处理更新后的 DOM 操作或状态同步。
- 触发时机:DOM 重新渲染完成后。
5.onBeforeUnmount()
- 用途:在组件卸载之前调用。适合执行清理工作,如移除事件监听器或取消定时器。
- 触发时机:组件销毁流程开始前。
6.onUnmounted()
- 用途:在组件卸载后调用。用于最终资源释放,如断开网络连接或清除缓存。
- 触发时机:组件从 DOM 中移除后。
7.onErrorCaptured()
- 用途:捕获子组件或当前组件的错误。用于错误处理或日志记录。
- 触发时机:组件树中任何地方抛出错误时。
二、使用注意事项
- 执行顺序:钩子按生命周期顺序执行,例如:
onBeforeMount
→onMounted
→onBeforeUpdate
→onUpdated
→onBeforeUnmount
→onUnmounted
。 - 依赖导入:所有钩子需从
vue
包导入,并在setup()
函数内调用。 - 异步支持:钩子回调可以是异步函数,适用于数据获取等操作。
- 性能优化:避免在频繁更新的钩子(如
onUpdated
)中执行重操作,以防止性能问题。
三、代码示例
以下是一个简单的 Vue 3 组件示例,展示如何使用 Composition API 的生命周期钩子:
<template><div>{{ message }}</div>
</template><script>
import { ref, onMounted, onUnmounted } from 'vue';export default {setup() {const message = ref('组件加载中...');// 挂载后更新消息onMounted(() => {message.value = '组件已挂载!';console.log('DOM 已渲染');});// 卸载时清理资源onUnmounted(() => {console.log('组件已卸载');});return { message };}
};
</script>
4.Composition API vs Options API
在 Vue.js 开发中,Composition API 和 Options API 是两种不同的组件代码组织方式。以下是它们的核心区别和适用场景分析:
一、Options API(选项式 API)
1. 特点:
- 通过预设选项(如
data
,methods
,computed
)组织代码 - 逻辑分散在不同选项中,相同功能可能跨多个选项
- 适合简单场景,学习曲线平缓
2. 示例代码:
export default {data() {return { count: 0 }},methods: {increment() {this.count++}},computed: {doubleCount() {return this.count * 2}}
}
3.优点:
- 结构清晰直观,适合新手
- 选项隔离降低耦合度
- 兼容性好(Vue 2/3 均支持)
4.缺点:
- 复杂组件中逻辑碎片化
- 代码复用依赖 mixins(易命名冲突)
二、 Composition API(组合式 API)
1. 特点:
- 通过
setup()
函数集中管理逻辑 - 基于函数组合(如
ref
,reactive
,computed
) - 逻辑按功能聚合,而非选项类型
2. 示例代码:
import { ref, computed } from 'vue'export default {setup() {const count = ref(0)const doubleCount = computed(() => count.value * 2)function increment() {count.value++}return { count, doubleCount, increment }}
}
3. 优点:
- 逻辑高内聚,复杂组件更易维护
- 更好的 TypeScript 支持
- 灵活的逻辑复用(自定义 Hook)
- 代码更精简(减少
this
依赖)
4. 缺点:
- 学习曲线较陡峭(需理解响应式原理)
- 过度集中可能降低可读性
三、核心对比
维度 | Options API | Composition API |
---|---|---|
代码组织 | 按选项类型分散 | 按功能逻辑集中 |
逻辑复用 | Mixins(易冲突) | 自定义 Hook(解耦性强) |
TS 支持 | 有限 | 完整类型推断 |
适用场景 | 简单组件/新手项目 | 复杂逻辑/大型应用 |
响应式数据 | 通过 data() 返回 | 通过 ref() /reactive() 声明 |
四、使用场景:
Options API :项目简单或团队 Vue 经验较少,需要快速迭代原型,维护旧版 Vue 2 项目
Composition API:组件逻辑复杂(如状态管理、异步流程),需要高度复用逻辑(自定义 Hook),使用 TypeScript 开发,长期维护的大型项目
5.setup()
函数作用
一、 核心作用
- setup()函数主要用于初始化程序环境,包括设置变量初始值、配置硬件参数、定义画布大小等。
- 它只在程序执行时调用一次,之后不再运行
二、关键注意事项
- 执行时机:setup()只在程序启动时运行一次
- 必要性:在支持setup()的框架中,它是必须定义的函数(即使为空),否则程序可能报错
- 常见错误:如果在setup()外部放置初始化代码,可能导致未定义行为或性能问题
6.ref
和 reactive
的区别
一、基本定义
ref
: 用于创建一个响应式引用,适用于基本类型(如数字、字符串、布尔值)或对象。它返回一个带有.value
属性的对象,访问或修改数据需要通过.value
。reactive
: 用于创建一个响应式对象,适用于对象或数组。它直接返回一个代理对象,属性可以直接访问和修改,无需额外语法。
二、主要区别
方面 | ref | reactive |
---|---|---|
适用类型 | 更适合基本类型(如 number , string ),也可用于对象。 | 仅适用于对象或数组,不适用于基本类型。 |
访问方式 | 必须通过 .value 访问或修改数据(例如 myRef.value )。 | 直接访问属性(例如 myReactive.key ),无需 .value 。 |
模板使用 | 在模板中自动解包,无需写 .value (Vue 内部处理)。 | 在模板中直接使用属性名,行为更直观。 |
内部实现 | 包装一个值,使用 Object.defineProperty 或 Proxy 实现响应式。 | 基于 Proxy 代理整个对象,深度监听嵌套属性。 |
解构问题 | 解构后仍保留响应性(因为返回的是引用对象)。 | 解构对象会丢失响应性,需使用 toRefs 辅助函数保持。 |
重新赋值 | 可以重新赋值整个对象(通过 myRef.value = newValue )。 | 不能直接重新赋值整个对象;需修改属性或使用 Object.assign 。 |
三、示例
import { ref, reactive } from 'vue';// 使用 ref 示例
const countRef = ref(0); // 基本类型
console.log(countRef.value); // 输出: 0
countRef.value = 10; // 修改值const userRef = ref({ name: 'Alice', age: 30 }); // 对象类型
console.log(userRef.value.name); // 输出: 'Alice'// 使用 reactive 示例
const userReactive = reactive({ name: 'Bob', age: 25 }); // 对象类型
console.log(userReactive.name); // 输出: 'Bob',无需 .value
userReactive.age = 26; // 直接修改属性// 解构问题演示
const { age } = userReactive; // 解构会丢失响应性
// 正确方式:使用 toRefs 保持响应性
const { name, age: reactiveAge } = toRefs(userReactive);
四、使用场景
ref:
处理基本类型数据,需要频繁重新赋值整个对象
reactive
:处理复杂对象或嵌套数据结构,需要直接访问属性,避免写 .value
的模板代码
总结:
- 用
reactive
管理状态对象,用ref
处理独立的基本值 - 选择时考虑数据结构和代码可读性:简单值用
ref
,复杂对象用reactive
。
7.Props 传递机制
一、基本概念
- Props 允许父组件将数据“注入”到子组件中,子组件通过声明 Props 来接收这些数据。
- 数据流是单向的:父组件更新 Props 会触发子组件重新渲染,但子组件不能直接修改 Props
- 用途:适用于配置子组件、传递静态或动态数据,例如传递用户信息、配置选项等。
二、 在子组件中声明 Props
子组件需要显式声明它可以接收的 Props。这通常在组件的选项或 Composition API 中完成。
-
声明方式:
- 使用
props
选项(Options API)或defineProps
宏(Composition API)。 - 每个 Prop 可以指定类型、默认值和验证规则。
- 使用
-
Options API:
// 子组件 (ChildComponent.vue) export default {props: {// 基本类型声明,例如字符串title: String,// 带默认值的数字类型count: {type: Number,default: 0},// 必填的布尔类型isActive: {type: Boolean,required: true}} }
Composition API:
// 子组件 (ChildComponent.vue) import { defineProps } from 'vue'const props = defineProps({title: String,count: { type: Number, default: 0 },isActive: { type: Boolean, required: true } })
三、在父组件中传递 Props
- 静态传递:直接传递固定值。
- 动态传递:绑定到父组件的数据或计算属性,实现响应式更新。
示例:
<!-- 父组件模板 -->
<template><ChildComponenttitle="欢迎使用 Vue 3" <!-- 静态字符串 -->:count="parentCount" <!-- 动态绑定数字 -->:is-active="isActive" <!-- 动态绑定布尔值 -->/>
</template><script>
import ChildComponent from './ChildComponent.vue'export default {components: { ChildComponent },data() {return {parentCount: 10, // 父组件数据isActive: true}}
}
</script>
四、 Props 的类型验证和默认值
- 类型:可以指定为原生类型(如
String
,Number
,Boolean
,Array
,Object
)或自定义类型。 - 默认值:通过
default
属性设置,当父组件未传递 Prop 时使用。 - 验证:使用
validator
函数进行自定义验证
示例:
props: {age: {type: Number,default: 18,validator: (value) => value >= 0 // 验证年龄非负}
}
五、单向数据流原则
- Props 是只读的:子组件不能直接修改接收到的 Prop。如果需要基于 Prop 派生数据,应使用计算属性。
- 原因:确保数据源单一,避免父子组件间的循环更新。
// 子组件中错误做法:直接修改 Prop // this.count = 20 // 不允许,会触发警告// 正确做法:使用计算属性或本地数据 computed: {doubledCount() {return this.count * 2 // 基于 Prop 派生新值} }
六、高级用法
- 传递对象或数组:使用
v-bind
传递整个对象,子组件通过 Prop 接收。<!-- 父组件 --> <ChildComponent :user-info="{ name: '张三', age: 30 }" />
- Prop 命名约定:建议使用 camelCase 声明,但在模板中使用 kebab-case(HTML 属性不区分大小写)。例如,声明为
userInfo
,传递时用user-info
。 - 响应式更新:父组件数据变化时,子组件的 Prop 会自动更新(得益于 Vue 的响应式系统)。
8.自定义事件 (emit
)
一、在子组件中定义和触发事件
- 使用
defineEmits
声明事件列表。 - 在方法中调用
emit
函数触发事件,并传递数据。 - 在模板中绑定事件触发器(如按钮点击)。
//子组件 ChildComponent.vue
<script setup>
// 导入 defineEmits
import { defineEmits } from 'vue';// 定义事件列表:声明一个名为 'customEvent' 的事件
const emit = defineEmits(['customEvent']);// 定义一个方法,在触发时发送事件
function handleClick() {// 触发 'customEvent' 事件,并传递数据(例如字符串 'Hello from child!')emit('customEvent', 'Hello from child!');
}
</script><template><!-- 在按钮点击时调用 handleClick 方法 --><button @click="handleClick">触发自定义事件</button>
</template>
二、 在父组件中监听事件
- 导入子组件。
- 在模板中使用
@event-name
或v-on:event-name
监听事件。 - 定义一个回调函数来处理事件数据。
//父组件 ParentComponent.vue
<script setup>
// 导入子组件
import ChildComponent from './ChildComponent.vue';// 定义回调函数,接收子组件传递的数据
function onCustomEvent(data) {console.log('事件触发!数据:', data); // 输出:事件触发!数据: Hello from child!// 这里可以添加业务逻辑,例如更新父组件状态
}
</script><template><!-- 监听子组件的 'customEvent' 事件,并绑定回调函数 --><ChildComponent @customEvent="onCustomEvent" />
</template>
三、注意事项
- 事件命名规范:推荐使用 kebab-case(短横线分隔)命名事件,如
custom-event
,以保持与 HTML 属性一致。在模板中监听时使用@custom-event
,但在defineEmits
中声明时使用 camelCase(如customEvent
)。 - 数据传递:
emit
可以传递多个参数,例如emit('event', arg1, arg2)
,父组件回调函数接收这些参数。 - TypeScript 支持:如果使用 TypeScript,可以通过泛型定义事件类型:
<script setup lang="ts"> const emit = defineEmits<{(event: 'customEvent', data: string): void; }>(); </script>
- 错误处理:确保事件名一致,避免拼写错误。如果父组件未监听事件,子组件的
emit
不会报错,但也不会执行任何操作。 - 替代方案:对于简单场景,也可以使用 Props 传递数据,但自定义事件更适合子组件主动通知父组件的场景。
9.生命周期钩子对比
Vue 3 的生命周期钩子在 Options API 和 Composition API 中有不同实现方式,同时部分钩子名称与 Vue 2 有差异。以下是核心对比:
一、生命周期阶段与钩子对照表
阶段 | Vue 2 (Options) | Vue 3 (Options API) | Vue 3 (Composition API) |
---|---|---|---|
初始化 | beforeCreate | beforeCreate | 无(逻辑在 setup() 内) |
created | created | 无(逻辑在 setup() 内) | |
挂载前 | beforeMount | beforeMount | onBeforeMount |
挂载完成 | mounted | mounted | onMounted |
更新前 | beforeUpdate | beforeUpdate | onBeforeUpdate |
更新完成 | updated | updated | onUpdated |
卸载前 | beforeDestroy | beforeUnmount | onBeforeUnmount |
卸载完成 | destroyed | unmounted | onUnmounted |
缓存组件 | activated | activated | onActivated |
deactivated | deactivated | onDeactivated | |
错误捕获 | errorCaptured | errorCaptured | onErrorCaptured |
调试钩子 | 无 | 无 | onRenderTracked |
onRenderTriggered |
二、关键变化说明
1.重命名的钩子
beforeDestroy
→beforeUnmount
(更准确描述组件卸载行为)destroyed
→unmounted
(语义更清晰)
2.Composition API 特性
- 所有钩子以
onXxx
形式导入(如onMounted
) beforeCreate
和created
被setup()
替代:import { onMounted } from 'vue';export default {setup() {// 替代 created 逻辑console.log("初始化逻辑");onMounted(() => {console.log("组件已挂载");});} }
3.新增调试钩子
onRenderTracked
:追踪响应式依赖onRenderTriggered
:诊断重新渲染原因
三、执行顺序对比(同一组件)
Options API: beforeCreate → created → beforeMount → mounted → beforeUpdate → updated → beforeUnmount → unmountedComposition API:setup() → onBeforeMount → onMounted → onBeforeUpdate → onUpdated → onBeforeUnmount → onUnmounted
最佳实践建议:
- 新项目优先使用 Composition API,逻辑更聚合
- 迁移项目可逐步替换重命名钩子(如
beforeDestroy
→beforeUnmount
)- 调试响应式问题时使用
onRenderTracked
/onRenderTriggered
10.watch
和 watchEffect
的区别?
一、 基本概念
watch
: 用于显式观察一个或多个响应式数据源(如 ref、reactive 对象、函数等),并在其变化时执行回调函数。它允许你指定依赖项,并控制监听行为(如深度监听或立即执行)。watchEffect
: 自动跟踪其函数体内部的响应式依赖项,并在任何依赖变化时重新运行该函数。它类似于计算属性(computed),但用于执行副作用(如 DOM 操作、API 调用),而非返回一个值。
二、主要区别
特性 | watch | watchEffect |
---|---|---|
依赖项指定方式 | 需要显式声明依赖源(如 () => data.value )。 | 自动收集函数体中的所有响应式依赖项。 |
初始执行行为 | 默认不立即执行回调;可通过 { immediate: true } 选项启用。 | 在创建时立即执行一次函数体。 |
访问旧值/新值 | 回调函数可接收旧值和新值作为参数(如 (newVal, oldVal) => {} )。 | 无法直接访问旧值;只提供当前值。 |
深度监听支持 | 支持 { deep: true } 选项进行深度监听(如对象嵌套属性)。 | 默认深度监听所有依赖项,无需额外选项。 |
适用场景 | 适合精确控制监听逻辑,如当特定数据变化时触发操作。 | 适合自动响应依赖变化,如执行副作用或初始化任务。 |
停止监听 | 通过返回的停止函数手动停止(如 const stop = watch(...); stop() )。 | 同样通过返回的停止函数手动停止。 |
三、示例
import { ref, reactive, watch, watchEffect } from 'vue';export default {setup() {const count = ref(0);const user = reactive({ name: 'Alice', age: 25 });// 示例 1: 使用 watchwatch(// 显式指定依赖源:count 和 user.name[() => count.value, () => user.name],// 回调函数接收新值和旧值([newCount, newName], [oldCount, oldName]) => {console.log('watch - count 变化:', newCount, '旧值:', oldCount);console.log('watch - name 变化:', newName, '旧值:', oldName);},// 选项:立即执行和深度监听{ immediate: true, deep: true });// 示例 2: 使用 watchEffectwatchEffect(() => {// 自动跟踪 count.value 和 user.ageconsole.log('watchEffect - count:', count.value);console.log('watchEffect - age:', user.age);// 注意:这里无法访问旧值});// 修改数据以触发监听setTimeout(() => {count.value = 1; // 触发 watch 和 watchEffectuser.name = 'Bob'; // 触发 watchuser.age = 26; // 触发 watchEffect}, 1000);return { count, user };}
};
四、适用场景总结
1.watch
- 你需要精确控制监听哪些数据源。
- 需要访问变化前后的值(如比较旧值和新值)。
- 场景示例:表单验证(当特定字段变化时检查)、API 请求(当 ID 变化时重新获取数据)。
2.watchEffect
- 依赖项复杂或动态,希望自动跟踪所有响应式引用。
- 需要立即执行副作用(如初始化日志或设置事件监听器)。
- 场景示例:实时日志输出、自动清理资源(结合
onInvalidate
函数)。
如果依赖项明确且需要旧值,用
watch
;如果依赖项动态或需要简化代码,用watchEffect
。