前言
在上一篇文章中,剖析了Vue3响应式系统的底层原理,理解了reactive、Proxy、track、trigger等核心概念。本文将继续深入,探讨Vue3中最常用的三个响应式API:ref、computed和watch的源码实现。
通过本文,你将掌握:
- ref为什么需要.value访问
- computed的缓存机制如何实现
- watch的依赖收集与触发原理
- 三者之间的内在联系与区别
一、ref响应式实现原理
1.1 为什么需要ref
在第一篇文章中,我们知道reactive基于Proxy实现,只能代理对象类型。但JavaScript中还有大量的基本类型数据(string、number、boolean等),这些数据无法被Proxy代理。
// reactive只能处理对象
const state = reactive({ count: 0 }); // 可以
// 基本类型无法使用reactive
const count = reactive(0); // 不行
为了解决这个问题,Vue3提供了ref API,它可以:
- 处理基本类型数据
- 也可以处理对象类型数据
- 统一的.value访问方式
1.2 ref的基本使用
import { ref, effect } from 'vue';
// 创建ref
const count = ref(0);
const user = ref({ name: 'Vue3' });
// 访问和修改需要通过.value
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
// 在模板中会自动解包,不需要.value
// <div>{{ count }}</div>
1.3 RefImpl类的源码实现
ref的核心是RefImpl类,让我们看看简化版的实现:
class RefImpl {
private _value; // 存储实际值
private _rawValue; // 存储原始值(未转换的)
public dep; // 依赖集合
public __v_isRef = true; // ref标识
constructor(value, public __v_isShallow = false) {
// 保存原始值
this._rawValue = __v_isShallow ? value : toRaw(value);
// 如果是对象,转换为reactive
this._value = __v_isShallow ? value : toReactive(value);
// 创建依赖集合
this.dep = new Set();
}
// getter:依赖收集
get value() {
// 收集依赖
trackRefValue(this);
return this._value;
}
// setter:触发更新
set value(newVal) {
// 判断值是否发生变化
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
// 更新原始值
this._rawValue = newVal;
// 更新值(对象转reactive)
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
// 触发依赖更新
triggerRefValue(this);
}
}
}
// ref函数
function ref(value) {
return new RefImpl(value);
}
// 判断是否是ref
function isRef(r) {
return !!(r && r.__v_isRef === true);
}
1.4 toReactive辅助函数
当ref接收对象类型时,会通过toReactive转换为reactive:
function toReactive(value) {
return isObject(value) ? reactive(value) : value;
}
function isObject(value) {
return value !== null && typeof value === 'object';
}
function toRaw(observed) {
// 获取响应式对象的原始对象
const raw = observed && observed.__v_raw;
return raw ? toRaw(raw) : observed;
}
1.5 ref的依赖收集与触发
ref有自己独立的依赖收集和触发机制:
// ref的依赖收集
function trackRefValue(ref) {
if (activeEffect) {
// 将当前副作用函数添加到ref的dep中
trackEffects(ref.dep);
}
}
function trackEffects(dep) {
if (activeEffect) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
// ref的触发更新
function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep);
}
}
function triggerEffects(dep) {
const effects = Array.from(dep);
effects.forEach(effect => {
if (effect.scheduler) {
effect.scheduler();
} else {
effect();
}
});
}
1.6 完整示例
// 创建ref
const count = ref(0);
const user = ref({ name: 'Vue', age: 3 });
// 创建副作用函数
effect(() => {
console.log('count:', count.value);
console.log('user.name:', user.value.name);
});
// 输出:count: 0
// 输出:user.name: Vue
// 修改基本类型
count.value = 10;
// 输出:count: 10
// 输出:user.name: Vue
// 修改对象属性
user.value.name = 'Vue3';
// 输出:count: 10
// 输出:user.name: Vue3
// 替换整个对象
user.value = { name: 'React', age: 10 };
// 输出:count: 10
// 输出:user.name: React
1.7 shallowRef浅层响应式
shallowRef只对.value的访问是响应式的,不会深度转换对象:
function shallowRef(value) {
return new RefImpl(value, true); // 第二个参数为true
}
// 使用示例
const state = shallowRef({ count: 0 });
effect(() => {
console.log(state.value.count);
});
// 这样不会触发更新,因为没有修改.value本身
state.value.count++; // 不会触发
// 这样会触发更新
state.value = { count: 1 }; // 会触发
1.8 toRef和toRefs
toRef用于为响应式对象的某个属性创建ref:
function toRef(object, key, defaultValue) {
const val = object[key];
return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue);
}
class ObjectRefImpl {
public __v_isRef = true;
constructor(
private _object,
private _key,
private _defaultValue
) {}
get value() {
const val = this._object[this._key];
return val === undefined ? this._defaultValue : val;
}
set value(newVal) {
this._object[this._key] = newVal;
}
}
// toRefs批量转换
function toRefs(object) {
const ret = {};
for (const key in object) {
ret[key] = toRef(object, key);
}
return ret;
}
// 使用示例
const state = reactive({ count: 0, name: 'Vue' });
const { count, name } = toRefs(state);
effect(() => {
console.log(count.value, name.value);
});
count.value++; // 会触发更新
二、computed计算属性实现原理
2.1 computed的特点
computed具有以下特点:
- 缓存机制:只有依赖变化时才重新计算
- 懒计算:只有被访问时才会计算
- 支持getter和setter:可以是只读或可写
// 只读computed
const count = ref(1);
const double = computed(() => count.value * 2);
// 可写computed
const firstName = ref('Zhang');
const lastName = ref('San');
const fullName = computed({
get: () => firstName.value + ' ' + lastName.value,
set: (val) => {
const [first, last] = val.split(' ');
firstName.value = first;
lastName.value = last;
}
});
2.2 ComputedRefImpl类实现
computed的核心是ComputedRefImpl类:
class ComputedRefImpl {
public dep; // 依赖集合
private _value; // 缓存的值
public readonly effect; // 内部effect
public readonly __v_isRef = true;
public _dirty = true; // 脏标记,true表示需要重新计算
constructor(getter, private readonly _setter) {
// 创建effect,但不立即执行
this.effect = new ReactiveEffect(getter, () => {
// scheduler调度器
if (!this._dirty) {
this._dirty = true;
// 触发computed的依赖更新
triggerRefValue(this);
}
});
this.dep = new Set();
}
get value() {
// 收集computed的依赖
trackRefValue(this);
// 如果是脏的,重新计算
if (this._dirty) {
this._dirty = false;
// 执行getter,收集依赖
this._value = this.effect.run();
}
return this._value;
}
set value(newValue) {
this._setter(newValue);
}
}
// computed函数
function computed(getterOrOptions) {
let getter;
let setter;
// 判断参数类型
const onlyGetter = typeof getterOrOptions === 'function';
if (onlyGetter) {
getter = getterOrOptions;
setter = () => {
console.warn('Computed property is readonly');
};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
2.3 调度器(scheduler)的作用
调度器是computed实现缓存的关键:
class ReactiveEffect {
constructor(
public fn, // 副作用函数
public scheduler = null // 调度器
) {}
run() {
activeEffect = this;
return this.fn();
}
}
// 触发更新时的逻辑
function triggerEffect(effect) {
if (effect.scheduler) {
// 如果有调度器,执行调度器而不是effect本身
effect.scheduler();
} else {
effect.run();
}
}
2.4 脏检查机制(dirty flag)
脏检查是实现缓存的核心:
// 工作流程示例
const count = ref(1);
const double = computed(() => {
console.log('计算执行');
return count.value * 2;
});
// 第一次访问,_dirty = true,执行计算
console.log(double.value); // 输出:计算执行 2
// 再次访问,_dirty = false,使用缓存
console.log(double.value); // 输出:2(没有"计算执行")
// 修改依赖,触发scheduler,设置_dirty = true
count.value = 2;
// 再次访问,_dirty = true,重新计算
console.log(double.value); // 输出:计算执行 4
2.5 computed的依赖收集链路
computed的依赖收集有两层:
const count = ref(0);
const double = computed(() => count.value * 2);
effect(() => {
console.log(double.value);
});
/**
* 依赖关系链:
*
* effect
* ↓ (收集依赖)
* computed (double)
* ↓ (收集依赖)
* ref (count)
*
* 触发链路:
* count.value = 1
* ↓ (触发)
* computed的scheduler执行
* ↓ (设置_dirty = true,触发)
* effect重新执行
* ↓ (访问double.value)
* computed重新计算
*/
2.6 完整示例
const price = ref(10);
const quantity = ref(2);
// 创建计算属性
const total = computed(() => {
console.log('计算total');
return price.value * quantity.value;
});
// 第一层effect
effect(() => {
console.log('effect执行,total:', total.value);
});
// 输出:计算total
// 输出:effect执行,total: 20
// 多次访问,使用缓存
console.log(total.value); // 20(没有"计算total")
console.log(total.value); // 20(没有"计算total")
// 修改依赖
price.value = 20;
// 输出:计算total
// 输出:effect执行,total: 40
quantity.value = 3;
// 输出:计算total
// 输出:effect执行,total: 60
三、watch侦听器实现原理
3.1 watch的基本用法
watch有多种使用方式:
const count = ref(0);
const state = reactive({ name: 'Vue' });
// 1. 监听ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`);
});
// 2. 监听reactive对象的属性
watch(() => state.name, (newVal, oldVal) => {
console.log(`name: ${oldVal} -> ${newVal}`);
});
// 3. 监听多个数据源
watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('多个数据变化');
});
// 4. 深度监听
watch(state, (newVal, oldVal) => {
console.log('state变化');
}, { deep: true });
// 5. 立即执行
watch(count, (newVal, oldVal) => {
console.log('立即执行');
}, { immediate: true });
3.2 watch核心实现
function watch(source, cb, options = {}) {
return doWatch(source, cb, options);
}
function doWatch(source, cb, { immediate, deep, flush } = {}) {
// 1. 标准化source为getter函数
let getter;
let forceTrigger = false;
if (isRef(source)) {
// 监听ref
getter = () => source.value;
forceTrigger = isShallow(source);
} else if (isReactive(source)) {
// 监听reactive,自动深度监听
getter = () => source;
deep = true;
} else if (Array.isArray(source)) {
// 监听多个数据源
getter = () =>
source.map(s => {
if (isRef(s)) return s.value;
if (isReactive(s)) return traverse(s);
if (typeof s === 'function') return s();
});
} else if (typeof source === 'function') {
// 监听getter函数
getter = source;
} else {
getter = () => {};
}
// 2. 如果是深度监听,需要递归遍历
if (deep && getter) {
const baseGetter = getter;
getter = () => traverse(baseGetter());
}
// 3. 保存旧值
let oldValue;
// 4. 定义job函数
const job = () => {
if (cb) {
// 获取新值
const newValue = effect.run();
// 深度监听或强制触发或新旧值不同时,执行回调
if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
// 执行清理函数
if (cleanup) {
cleanup();
}
// 执行回调
cb(newValue, oldValue, onCleanup);
// 更新旧值
oldValue = newValue;
}
}
};
// 5. 创建effect
const effect = new ReactiveEffect(getter, () => {
// scheduler
if (flush === 'sync') {
job();
} else if (flush === 'post') {
queuePostFlushCb(job);
} else {
// 默认'pre'
queueJob(job);
}
});
// 6. 初始化
if (cb) {
if (immediate) {
job();
} else {
oldValue = effect.run();
}
} else {
effect.run();
}
// 7. 返回停止函数
return () => {
effect.stop();
};
}
3.3 traverse深度遍历
traverse函数用于深度遍历对象,触发所有属性的getter:
function traverse(value, seen = new Set()) {
// 不是对象或已经遍历过,直接返回
if (!isObject(value) || seen.has(value)) {
return value;
}
// 标记已遍历
seen.add(value);
// 处理ref
if (isRef(value)) {
traverse(value.value, seen);
} else if (Array.isArray(value)) {
// 遍历数组
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen);
}
} else if (isSet(value) || isMap(value)) {
// 遍历Set和Map
value.forEach((v) => {
traverse(v, seen);
});
} else if (isPlainObject(value)) {
// 遍历普通对象
for (const key in value) {
traverse(value[key], seen);
}
}
return value;
}
3.4 cleanup清理函数
cleanup用于清理副作用:
function doWatch(source, cb, options) {
let cleanup;
const onCleanup = (fn) => {
cleanup = fn;
};
const job = () => {
const newValue = effect.run();
// 执行上一次的清理函数
if (cleanup) {
cleanup();
}
cb(newValue, oldValue, onCleanup);
oldValue = newValue;
};
// ...
}
// 使用示例
watch(id, async (newId, oldId, onCleanup) => {
let expired = false;
onCleanup(() => {
expired = true;
});
const result = await fetch(`/api/${newId}`);
if (!expired) {
// 只有在未过期时才使用结果
data.value = result;
}
});
3.5 flush调度时机
flush参数控制回调的执行时机:
// 'pre'(默认):在组件更新前执行
watch(source, cb, { flush: 'pre' });
// 'post':在组件更新后执行
watch(source, cb, { flush: 'post' });
// 'sync':同步执行(不推荐)
watch(source, cb, { flush: 'sync' });
// 实现
function doWatch(source, cb, { flush = 'pre' } = {}) {
const job = () => {
// 执行回调
};
const effect = new ReactiveEffect(getter, () => {
if (flush === 'sync') {
job();
} else if (flush === 'post') {
queuePostFlushCb(job); // 推入后置队列
} else {
queueJob(job); // 推入前置队列
}
});
}
3.6 watchEffect简化版
watchEffect是watch的简化版,自动收集依赖:
function watchEffect(effect, options) {
return doWatch(effect, null, options);
}
// 使用示例
const count = ref(0);
watchEffect(() => {
console.log('count:', count.value);
});
// 立即执行,输出:count: 0
count.value++;
// 输出:count: 1
3.7 完整示例
const count = ref(0);
const state = reactive({ user: { name: 'Vue' } });
// 示例1:监听ref
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`);
});
count.value = 1;
// 输出:count从0变为1
// 示例2:深度监听对象
watch(
() => state.user,
(newVal, oldVal) => {
console.log('user对象变化', newVal);
},
{ deep: true }
);
state.user.name = 'Vue3';
// 输出:user对象变化 { name: 'Vue3' }
// 示例3:立即执行
watch(
count,
(newVal, oldVal) => {
console.log('立即执行', newVal, oldVal);
},
{ immediate: true }
);
// 输出:立即执行 1 undefined
// 示例4:cleanup清理
let id = ref(1);
watch(id, async (newId, oldId, onCleanup) => {
let expired = false;
onCleanup(() => {
console.log('清理旧请求');
expired = true;
});
console.log(`开始请求 id=${newId}`);
setTimeout(() => {
if (!expired) {
console.log(`请求完成 id=${newId}`);
}
}, 1000);
});
id.value = 2;
// 输出:开始请求 id=2
setTimeout(() => {
id.value = 3;
// 输出:清理旧请求
// 输出:开始请求 id=3
}, 500);
四、三者对比与使用场景
4.1 ref vs reactive
| 特性 | ref | reactive |
|---|---|---|
| 数据类型 | 任意类型 | 对象类型 |
| 访问方式 | .value | 直接访问 |
| 模板中 | 自动解包 | 直接使用 |
| 替换整个对象 | ✅ 可以 | ❌ 会失去响应性 |
| 底层实现 | RefImpl类 | Proxy代理 |
4.2 computed vs watch
| 特性 | computed | watch |
|---|---|---|
| 返回值 | ✅ 有返回值 | ❌ 无返回值 |
| 缓存 | ✅ 有缓存 | ❌ 无缓存 |
| 懒执行 | ✅ 按需计算 | ✅ 可配置 |
| 异步操作 | ❌ 不支持 | ✅ 支持 |
| 使用场景 | 计算派生数据 | 执行副作用 |
4.3 使用场景建议
使用ref:
- 基本类型数据
- 需要替换整个对象
- 需要在模板中自动解包
使用reactive:
- 复杂对象结构
- 不需要替换整个对象
- 更接近原生JavaScript对象
使用computed:
- 基于响应式数据计算派生值
- 需要缓存计算结果
- 计算逻辑比较复杂
使用watch:
- 执行异步操作
- 执行开销较大的操作
- 需要在数据变化时执行副作用
五、性能优化技巧
5.1 合理使用shallowRef
// 大型对象使用shallowRef避免深度代理
const bigData = shallowRef({
list: new Array(10000).fill(0)
});
// 修改时替换整个对象
bigData.value = { list: [...] };
5.2 使用computed缓存计算结果
// ❌ 每次都重新计算
const total = () => list.value.reduce((sum, item) => sum + item.price, 0);
// ✅ 使用computed缓存
const total = computed(() =>
list.value.reduce((sum, item) => sum + item.price, 0)
);
5.3 避免不必要的深度监听
// ❌ 深度监听整个对象
watch(state, callback, { deep: true });
// ✅ 只监听需要的属性
watch(() => state.user.name, callback);
5.4 使用watchEffect简化代码
// ❌ 手动指定依赖
watch([count, name], () => {
console.log(count.value, name.value);
});
// ✅ 自动收集依赖
watchEffect(() => {
console.log(count.value, name.value);
});
六、总结
本文深入剖析了Vue3三个核心响应式API的实现原理:
ref实现要点:
- RefImpl类通过getter/setter实现响应式
- 对象类型会转换为reactive
- 独立的依赖收集和触发机制
computed实现要点:
- ComputedRefImpl类实现缓存机制
- 脏检查(dirty flag)控制是否重新计算
- 调度器(scheduler)延迟触发更新
watch实现要点:
- traverse函数实现深度遍历
- cleanup清理函数处理副作用
- flush参数控制执行时机
掌握这些原理后,我们就能更好地理解Vue3的响应式系统,写出更高效的代码。在下一篇文章中,我们将探讨编译器和虚拟DOM的实现原理。
相关资源
- Vue3官方文档:https://cn.vuejs.org/
- Vue3响应式API:https://cn.vuejs.org/api/reactivity-core.html
- Vue3 GitHub仓库:https://github.com/vuejs/core
作者:前端技术探索者
本文为Vue3源码解析系列第二篇,专注于ref、computed、watch实现原理。下一篇将带来编译器与虚拟DOM的深度解析,敬请期待!

被折叠的 条评论
为什么被折叠?



