vue3 高级响应式函数 watch watchEffect computed

Vue3 高级响应式函数

Vue3 的响应式系统不仅提供了 reactive/ref 等基础 API,还通过 computedwatchwatchEffect 等高级函数实现了对响应式数据的精细化处理。这些函数基于响应式核心机制(依赖追踪与副作用调度),但各自聚焦不同场景,掌握它们的原理与差异是写出高效 Vue 代码的关键。

一、computed:带缓存的派生状态

computed 用于创建依赖响应式数据的派生状态,其核心特性是缓存机制——只有当依赖的响应式数据变化时,才会重新计算,否则直接返回缓存值。

1. 原理:基于副作用的缓存调度

computed 的底层依赖 Vue3 的 Effect 副作用系统,其工作流程可简化为:

  1. 初始化:创建一个 ComputedRefImpl 实例,内部包含一个 effect(副作用函数),该函数执行用户传入的 getter 逻辑。
  2. 依赖收集:首次访问 computedvalue 时,触发 effect.run(),执行 getter 并收集 getter 中使用的响应式数据作为依赖。
  3. 缓存与更新:当依赖变化时,effect 会被标记为”脏”(dirty: true),但不会立即重新计算;只有再次访问 value 时,才会重新执行 getter 并更新缓存。

核心伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ComputedRefImpl {
constructor(getter, setter) {
this.getter = getter;
this.setter = setter;
// 创建副作用,配置 lazy: true(延迟执行)
this.effect = new ReactiveEffect(getter, () => {
if (!this.dirty) {
this.dirty = true;
// 触发依赖更新(通知使用该computed的地方)
triggerRefValue(this);
}
});
this.effect.lazy = true; // 标记为懒执行
this.dirty = true; // 是否需要重新计算
this.value = undefined;
}

get value() {
if (this.dirty) {
// 首次访问或依赖变化时,重新计算
this.value = this.effect.run();
this.dirty = false; // 标记为已缓存
}
// 收集使用当前computed的依赖
trackRefValue(this);
return this.value;
}

set value(newValue) {
this.setter(newValue); // 可写computed的set逻辑
}
}

2. 参数与用法

computed 有两种使用形式,对应不同参数:

(1)只读 computed

参数为一个getter函数,返回一个只读的 ComputedRef 对象(需通过 .value 访问)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref, computed } from 'vue';

const firstName = ref('张');
const lastName = ref('三');

// 只读计算属性:拼接全名
const fullName = computed(() => {
console.log('计算全名'); // 依赖变化时才执行
return `${firstName.value}${lastName.value}`;
});

console.log(fullName.value); // 输出:"张三"(执行getter)
console.log(fullName.value); // 输出:"张三"(使用缓存,不执行getter)

firstName.value = '李';
console.log(fullName.value); // 输出:"李四"(依赖变化,重新计算)

(2)可写 computed

参数为一个包含 getset配置对象,支持修改计算属性的值(会触发 set 逻辑)。

1
2
3
4
5
6
7
8
9
10
11
12
13
const count = ref(1);

// 可写计算属性:处理加倍逻辑
const doubleCount = computed({
get: () => count.value * 2,
set: (newValue) => {
count.value = newValue / 2; // 反向更新源数据
}
});

console.log(doubleCount.value); // 2
doubleCount.value = 6;
console.log(count.value); // 3(通过set反向更新)

3. 应用场景

  • 派生数据处理:将多个响应式数据组合成新数据(如全名、总价、状态标识等)。
  • 数据格式化:对原始数据进行格式化(如日期格式化、金额千分位处理)。
  • 缓存高频计算:避免在模板或函数中重复执行耗时计算(如大数据过滤、复杂公式)。

反例:不要在 computed 中执行副作用(如修改DOM、发送请求),这违背其”派生状态”的设计初衷,应使用 watchwatchEffect

二、watch:精确监听指定源的变化

watch 用于显式监听一个或多个响应式源,当源变化时执行自定义副作用(如异步请求、DOM操作等)。它的核心特点是精确控制监听对象,并能获取变化前后的值。

1. 原理:基于源的依赖追踪

watch 的底层同样依赖 Effect 系统,但与 computed 不同,它需要先明确”监听什么”,再定义”变化后做什么”。工作流程:

  1. 解析源:将用户传入的”源”(可以是 refreactive 属性、getter 函数等)标准化为一个可追踪的 getter 函数。
  2. 创建副作用:基于源的 getter 创建 effect,并在 effect 的调度器(scheduler)中执行用户传入的回调函数。
  3. 监听与触发:当源依赖的响应式数据变化时,effect 被触发,调度器会获取新旧值并执行回调。

核心伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function watch(source, cb, options) {
// 标准化源为getter函数
const getter = isRef(source)
? () => source.value
: isReactive(source)
? () => source
: source; // 若source是函数,直接作为getter

let oldValue;
// 创建副作用
const effect = new ReactiveEffect(getter, () => {
const newValue = effect.run(); // 重新执行getter获取新值
cb(newValue, oldValue); // 执行回调
oldValue = newValue; // 更新旧值
});

// 初始化:获取初始值
oldValue = effect.run();
}

2. 参数详解

watch 的完整参数格式:watch(source, callback, options?)

(1)source:监听源

可以是以下类型:

  • ref(包括 ComputedRef):直接监听其 .value 变化。
  • reactive对象:监听对象内部所有属性(自动深层监听)。
  • getter函数:监听函数返回值的变化(支持精确监听对象的某个属性)。
  • 源数组:同时监听多个源,任意一个变化即触发回调。

(2)callback:变化回调

格式:(newValue, oldValue, onCleanup) => void

  • newValue:源的新值
  • oldValue:源的旧值
  • onCleanup:清理函数,用于注册副作用的清理逻辑(如取消请求、清除定时器)。

(3)options:配置选项

  • immediate: boolean:是否立即执行一次回调(默认 false)。
  • deep: boolean:是否深层监听(当源是对象且未用 getter 时生效,默认 false,但监听 reactive 对象时自动深层)。
  • flush: 'pre' | 'post' | 'sync':回调执行时机(默认 'pre',即DOM更新前;'post' 为DOM更新后;'sync' 为同步执行)。
  • onTrack/onTrigger:调试用,分别在依赖收集和触发更新时执行。

3. 示例:不同源的监听方式

(1)监听 ref

1
2
3
4
5
6
7
8
const count = ref(0);

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

count.value++; // 输出:count从0变为1

(2)监听 reactive 对象的属性(需用 getter)

1
2
3
4
5
6
7
8
9
10
11
12
const user = reactive({ name: '张三', age: 20 });

// 监听user.age(必须用getter,否则会监听整个user)
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年龄从${oldAge}变为${newAge}`);
}
);

user.age = 21; // 输出:年龄从20变为21
user.name = '李四'; // 不触发回调(未监听name)

(3)监听多个源(数组形式)

1
2
3
4
5
6
7
8
9
const a = ref(1);
const b = ref(2);

// 监听a和b,回调参数为数组
watch([a, () => b.value], ([newA, newB], [oldA, oldB]) => {
console.log(`a变化:${oldA}${newA},b变化:${oldB}${newB}`);
});

a.value = 2; // 输出:a变化:1→2,b变化:2→2

(4)深层监听与清理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = reactive({ nested: { count: 0 } });

watch(
() => obj.nested,
(newVal, oldVal, onCleanup) => {
// 模拟异步请求
const timer = setTimeout(() => {
console.log('嵌套对象变化');
}, 1000);

// 清理函数:下次触发前或组件卸载时执行
onCleanup(() => clearTimeout(timer));
},
{ deep: true } // 深层监听(若源是reactive对象则可省略)
);

obj.nested.count = 1; // 触发回调,1秒后输出

4. 应用场景

  • 数据变化触发异步操作:如用户ID变化后重新请求用户详情。
  • 状态同步:如表单输入变化后同步更新本地存储。
  • 复杂副作用处理:需要精确控制执行时机(如DOM更新后操作)或清理逻辑(如防抖、节流)的场景。

注意:监听 reactive 对象时,watch 会自动深层监听,但性能开销较大;建议用 getter 函数精确监听所需属性(如 () => obj.prop)。

三、watchEffect:自动追踪的副作用

watchEffect 用于自动追踪函数内部的响应式依赖,当依赖变化时重新执行函数。它的核心特点是无需指定监听源,专注于”副作用逻辑”,更简洁但灵活性稍低。

1. 原理:隐式依赖收集

watchEffectwatch 的简化版,它省略了”指定源”的步骤,直接执行副作用函数并自动收集其中的响应式依赖。工作流程:

  1. 立即执行:创建 effect 并立即执行副作用函数。
  2. 自动收集依赖:执行过程中读取的响应式数据(ref.valuereactive 属性等)会被自动标记为依赖。
  3. 依赖变化触发:当依赖变化时,自动重新执行副作用函数。

核心伪代码:

1
2
3
4
5
6
7
8
9
function watchEffect(effectFn, options) {
// 创建副作用,调度器为重新执行effect
const effect = new ReactiveEffect(effectFn, () => {
effect.run(); // 依赖变化时重新执行
});

// 立即执行一次,收集依赖
effect.run();
}

2. 参数与用法

watchEffect 的参数格式:watchEffect(effectFn, options?)

(1)effectFn:副作用函数

格式:(onCleanup) => void

  • 函数内部可直接使用响应式数据,这些数据会被自动追踪。
  • onCleanup:清理函数,用于注册副作用的清理逻辑(与 watchonCleanup 一致)。

(2)options:配置选项

  • flush: 'pre' | 'post' | 'sync':执行时机(默认 'pre',同 watch)。
  • onTrack/onTrigger:调试用,同 watch

3. 示例:自动追踪依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref, watchEffect } from 'vue';

const searchQuery = ref('');
const results = ref([]);

// 自动追踪searchQuery的变化
const stop = watchEffect(async (onCleanup) => {
// 模拟API请求
const controller = new AbortController(); // 用于取消请求
const signal = controller.signal;

console.log(`搜索:${searchQuery.value}`);
const res = await fetch(`/api/search?q=${searchQuery.value}`, { signal });
results.value = await res.json();

// 清理函数:下次执行前或停止时取消请求
onCleanup(() => controller.abort());
});

searchQuery.value = 'vue'; // 触发副作用,输出"搜索:vue"
searchQuery.value = 'react'; // 触发副作用,输出"搜索:react"

// 停止监听(组件卸载时调用)
// stop();

4. 与 watch 的核心区别

特性 watch watchEffect
监听源 需显式指定(精确控制) 自动追踪函数内的依赖(隐式)
执行时机 默认不立即执行(可通过 immediate 开启) 立即执行一次(首次运行收集依赖)
新旧值 可获取 newValueoldValue 无法直接获取,需手动缓存旧值
适用场景 需精确监听、需要新旧值、控制执行时机 简单副作用、自动追踪多依赖

5. 应用场景

  • 响应式数据驱动的副作用:如搜索框输入变化后自动请求接口、表单验证。
  • 资源自动释放:如根据响应式数据动态创建/销毁定时器、事件监听。
  • 简单的状态同步:如响应式数据变化后同步更新DOM样式、日志输出。

四、总结:如何选择?

  • 需要派生状态且有缓存computed(如 fullName = firstName + lastName)。
  • 需要精确监听指定源、需要新旧值、控制执行时机watch(如监听用户ID变化请求数据)。
  • 需要自动追踪多依赖、执行简单副作用watchEffect(如搜索输入变化后请求接口)。

这三个函数共同构建了Vue3响应式系统的上层能力,理解它们的原理与差异,能让你在处理响应式数据时更高效、更贴合场景。