Vue3
的稳定版发布了很久了,去阅读官网的文档时发现推荐的一个优秀的 Vue
教程网站:Vue Mastery,里面的一篇Vue 3 Reactivity(Vue3 响应式)讲的是真的不错。跟着学完收获很多,顺着课程的思路总结一篇 Vue3
响应式的笔记,手动还原响应式的原理。
手动实现响应式
响应式,就是一个变量
依赖了其他变量,当被依赖的其他变量更新后该变量
也要响应式的更新。所以从零开始,先实现手动的更新。
单个变量的手动响应
先看几个单词的意思,
dep
是 dependence
的缩写,也就是依赖。
effect
,指因某种原因导致产生结果,着重持续稳定的影响。
track
,指追踪、踪迹。
trigger
,指触发。
再看手动实现的代码,这里用到了 Set
,不熟悉的话可参考 MDN:
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 33 34
| let price = 5; let quantity = 2; let total = 0;
let dep = new Set();
let effect = () => { total = price * quantity; };
function track() { dep.add(effect); }
function trigger() { dep.forEach((effect) => effect()); }
track(); effect(); console.log(total);
|
上述代码是最最简单的实现,流程就是三步:
- 通过
effect
来表明影响 total
的依赖
- 通过
track
来保存 effect
- 通过
trigger
来执行 effect
最后输出的 total
肯定就是计算后的 10
。
对对象的多个属性手动响应
上个例子中 price
和 quantity
都是放在了不同的变量里,现在更进一步,把他们放到同一个对象里 let product = { price: 5, quantity: 2 }
,现在如果想让 product
对象变为响应式,就需要指定每个键的响应。
depsMap
的意思就是 dependence
的 map
。也就是一个 map
中,每个键都对应某个属性的 dep
。
这里用到了 Map
,不熟悉的话可参考 MDN
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 33 34 35 36 37 38 39 40 41 42 43 44
| const depsMap = new Map();
function track(key) { let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); }
function trigger(key) { let dep = depsMap.get(key); if (dep) { dep.forEach((effect) => { effect(); }); } }
let product = { price: 5, quantity: 2 }; let total = 0;
let effect = () => { total = product.price * product.quantity; };
effect(); console.log(total);
track('quantity'); product.quantity = 3;
trigger('quantity'); console.log(total);
|
在上述代码中,实现了对整个 product 对象的手动响应。
对多个对象的多个属性进行手动响应
继续升级上述代码,如果有多个对象需要响应式,那么就需要给不同的对象设置不同 depsMap
。所以创建一个 WeakMap
类型的变量,命名为 targetMap
来存储多个对象的 depsMap
,target
就是指的需要被响应式的对象。
而之所以用 Map
类型是因为 Map
可以用“对象”作为键,用 WeakMap
方便对键(也就是被响应式的对象)进行垃圾回收。不熟悉 WeakMap
的话可参考 MDN
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| const targetMap = new WeakMap();
function track(target, key) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); }
function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let dep = depsMap.get(key); if (dep) { dep.forEach((effect) => { effect(); }); } } let product = { price: 5, quantity: 2 }; let total = 0; let effect = () => { total = product.price * product.quantity; };
effect(); console.log(total);
track(product, 'quantity'); product.quantity = 3;
trigger(product, 'quantity'); console.log(total);
|
上述代码实现了对不同对象的不同键进行手动响应。到了这一步,可以用课程中的一张图来清楚的表示下 targetMap
、depsMap
、dep
之间的关系:

变为自动响应
继续升级代码,给上述代码添加自动响应。在这里用到了 Proxy
和 Reflect
, 不熟悉的话可参考 MDN。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| const targetMap = new WeakMap(); function track(target, key) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(effect); }
function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let dep = depsMap.get(key); if (dep) { dep.forEach((effect) => { effect(); }); } }
function reactive(target) { const handlers = { get(target, key, receiver) { let result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { let oldValue = target[key]; let result = Reflect.set(target, key, value, receiver); if (result && oldValue != value) { trigger(target, key); } return result; }, }; return new Proxy(target, handlers); }
let product = reactive({ price: 5, quantity: 2 }); let total = 0;
var effect = () => { total = product.price * product.quantity; };
effect(); console.log(total);
product.quantity = 3; console.log(total);
|
这段代码实现了自动响应,最关键的核心部分就是 reactive
函数,它返回了一个 Proxy
,代理了对 target
对象的存取。首先在在 get
返回之前,自动调用 track
将 effect
存到对应的位置。
巧妙的地方是 set
,当我们执行 product.quantity = 3;
时,会先将 quantity
设为 3
,再自动触发 trigger
。这时最关键的地方来了,trigger
调用了存储的对应的 effect
,计算出最新的 total
为 15
,实现了自动响应。
优化自动响应过程
上述代码实现了自动响应,但是现在还有两个明显不如人意的地方:
- 不能设置多种
effect
。
- 在我们设置
quantity
为 3
的时候,trigger
调用了对应的 effect
,这里的 effect
函数执行来计算 total
时,会再走一遍 proxy
中 get
的流程。所以就会触发 track
的流程,但是这里我们并不需要触发 track
再保存一遍 effect
。
下面来优化上述两个问题:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| const targetMap = new WeakMap(); let activeEffect = null;
function track(target, key) { if (activeEffect) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); } }
function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let dep = depsMap.get(key); if (dep) { dep.forEach((effect) => { effect(); }); } }
function reactive(target) { const handler = { get(target, key, receiver) { let result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { let oldValue = target[key]; let result = Reflect.set(target, key, value, receiver); if (result && oldValue != value) { trigger(target, key); } return result; }, }; return new Proxy(target, handler); }
function effect(eff) { activeEffect = eff; activeEffect(); activeEffect = null; }
let product = reactive({ price: 5, quantity: 2 }); let salePrice = 0; let total = 0;
effect(() => { total = product.price * product.quantity; });
effect(() => { salePrice = product.price * 0.9; });
console.log(total, salePrice);
product.quantity = 3; console.log(total, salePrice);
product.price = 10; console.log(total, salePrice);
|
上述代码通过添加了一个 activeEffect
的标志位,解决了无效的重复执行保存的缺点;并把 effect()
变为 effect(eff)
的带参数形式,解决了多个 effect
的问题。
到这里为止,Vue3 Composition API
中 reactive
方法的实现流程已经被我们手动大致实现了一遍!
实现 ref
在 Vue3 Composition API
设计中,reactive
主要用于引用类型
,另外专门提供了一个 ref
方法实现对原始类型
的响应式。
大部分的地方都不用变,基本上就是添加了个 ref
方法,实现的方式就是对象访问器 getter/setter
来模仿 Proxy
:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| const targetMap = new WeakMap(); let activeEffect = null;
function track(target, key) { if (activeEffect) { if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); } }
function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let dep = depsMap.get(key); if (dep) { dep.forEach((effect) => { effect(); }); } }
function reactive(target) { const handler = { get(target, key, receiver) { let result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { let oldValue = target[key]; let result = Reflect.set(target, key, value, receiver); if (result && oldValue != value) { trigger(target, key); } return result; }, }; return new Proxy(target, handler); }
function ref(raw) { const r = { get value() { track(r, 'value'); return raw; }, set value(newVal) { raw = newVal; trigger(r, 'value'); }, }; return r; }
function effect(eff) { activeEffect = eff; activeEffect(); activeEffect = null; }
let product = reactive({ price: 5, quantity: 2 }); let salePrice = ref(0); let total = 0;
effect(() => { salePrice.value = product.price * 0.9; });
effect(() => { total = salePrice.value * product.quantity; });
console.log(total, salePrice);
product.quantity = 3; console.log(total, salePrice);
product.price = 10; console.log(total, salePrice);
|
其实可以发现,reactive
也能实现对原始类型的响应式,为什么还要专门提供一个 ref
方法?看对尤大的访谈中,尤大回答的是 reactive
还会添加更多处理流程,对于原始类型来说,是一种无用的负担。
实现 computed
跟响应式相关的最后一部分内容就是 computed
了,继续来手动实现它:
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| const targetMap = new WeakMap(); let activeEffect = null;
function track(target, key) { if (activeEffect) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); } }
function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) { return; } let dep = depsMap.get(key); if (dep) { dep.forEach((eff) => { eff(); }); } }
function reactive(target) { const handler = { get(target, key, receiver) { let result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { let oldValue = target[key]; let result = Reflect.set(target, key, value, receiver); if (result && oldValue != value) { trigger(target, key); } return result; }, }; return new Proxy(target, handler); }
function ref(raw) { const r = { get value() { track(r, 'value'); return raw; }, set value(newVal) { raw = newVal; trigger(r, 'value'); }, }; return r; }
function effect(eff) { activeEffect = eff; activeEffect(); activeEffect = null; }
function computed(getter) { let result = ref(); effect(() => (result.value = getter())); return result; }
let product = reactive({ price: 5, quantity: 2 });
let salePrice = computed(() => { return product.price * 0.9; });
let total = computed(() => { return salePrice.value * product.quantity; });
console.log(total.value, salePrice.value);
product.quantity = 3; console.log(total.value, salePrice.value);
product.price = 10; console.log(total.value, salePrice.value);
|
可以发现,computed
本质上就是封装了 ref
方法,用 effect
封装着来调用 getter
,将结果设给 result.value
的同时,也将 eff
保存在了 targetMap
中的对应位置,实现了 computed
的响应式。
Vue3 源码中响应式的实现
Vue3
整体是用 Typescript
写的,reactivity
是一个独立的模块,源代码位于packages/reactivity/src目录下,几个不同的方法分别位于不同文件中:

关于 Vue Mastery 课程
Vue Mastery
课程是收费的,25% 的收入会捐给 Vue
项目,所以大家对课程感兴趣的话可以开会员支持一波。不过它的会员很贵,可以有一些取巧的方法跳过收费验证,可以关注“林景宜的记事本
”公众号发送“Vue3 响应式
”获取方法试看一波。
前端记事本,不定期更新,欢迎关注!