Vue 3.0 的响应式
Vue 3.0 的响应式系统是独立的模块,可以完全脱离 Vue 使用,可以直接在 vue-next packages/reactivity 模块下调试。
首先认识 Proxy
简单调试办法
步骤:
1、clone 项目到本地git clone https://github.com/vuejs/vue-next.git
2、cd 到项目根目录 执行 yarn
3、根目录 执行 yarn dev reactivity
4、cd 到packages/reactivity 目录,可以看到 dist/reactivity.global.js大概 946 行代码,在此目录下创建 index.html
1 | <!DOCTYPE html> |
响应式整体思路
这里直接给结论,简单概括为
- 初始化阶段
- 依赖收集阶段
- 响应阶段
看下 vue3.0 reactivity 暴露了哪些?
通过在 repo 根目录执行 yarn build reactivity –types 可在 temp/reactivity.api.md 处生成 API 报告。(去看一眼代码)
API Report File for “@vue/reactivity”
1 | export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>; |
响应式源码分析
初始化阶段
初始化阶段核心其实就是 reactive 调用和 effect 调用
直接放核心代码:
reactive 函数代码片段
1 | export function reactive(target) { |
reactive 函数干了啥
在这里咱们先不管 handler(后面再详细说), reactive 干了一件事儿,把 origin 对象转化成响应式的 Proxy 对象
effect 函数代码片段
1 | export function effect(fn) { |
当一个普通的函数 fn 被 函数 effect 包裹之后,就会变成一个响应式的 effect 函数,而 fn 也会被立即执行一次。
由于在 fn 里面有引用到 Proxy 对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集。
除此之外,这个 effect 函数也会被压入一个名为”activeReactiveEffectStack“(此处为 effectStack)的栈中,供后续依赖收集的时候使用。
effect 函数干了啥
把函数 fn 作为一个响应式的 effect 函数
依赖收集阶段
依赖收集触发时机
从图上其实可以看出这个阶段的触发时机,就是在 effect 被立即执行,其内部的 fn 触发了 Proxy 对象的 getter 的时候。简单来说,只要执行到类似 state.count的语句,就会触发 state 的 getter。
依赖收集目的
建立一份”依赖收集表“,也就是图示的”targetMap”。
targetMap 是一个 WeakMap,其 key 值是当前的 Proxy 对象(state)代理前的对象(origin),而 value 则是该对象所对应的 depsMap(我叫它观察者的集合)。
强行解释 depsMap: depsMap 是一个 Map,key 值为触发 getter 时的属性值(此处为 count),而 value 则是触发过该属性值所对应的各个 effect。举个栗子:

这样,「target => key => dep」 的对应关系就建立起来了,依赖收集也就完成了
1 | export function track(target, operationType, key) { |
弄明白依赖收集表 targetMap 是非常重要的,因为这是整个响应式系统核心中的核心。
响应阶段
回顾上一章节的例子,我们得到了一个 { count: 0, age: 18 } 的 Proxy,并构造了三个 effect。在控制台上看看效果:
效果符合预期,那么它是怎么实现的呢?首先来看看这个阶段的原理图:
当修改对象的某个属性值的时候,会触发对应的 setter。
setter 里面的 trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,然后把它们推入到 effects 和 computedEffects(计算属性) 队列中,最后通过 scheduleRun() 挨个执行里面的 effect。
由于已经建立了依赖收集表,所以要找到属性所对应的 dep 也就轻而易举了,可以看看具体的代码实现:
1 | export function trigger(target, operationType, key) { |
这里的代码没有处理诸如数组的 length 被修改的一些特殊情况,感兴趣的读者可以查看 vue-next 对应的源码,或者这篇文章,看看这些情况都是怎么处理的。
至此,响应式阶段完成。
API 模拟实现
reactive 实现
1 | /** |
但是可能存在这样的对象
1 | const person = { |
所以我们得继续实现多层对象嵌套情况下的代理:
1 | get(target, key, receiver) { |
我们继续考虑数组的情况
Proxy 默认是可以支持数组,所以我们不需要像 Vue2.x 中一样对数组封装自己的方法并在其中来劫持监听数据改变,但是我们改变数组的时候仍然能够发现问题,那就是数组的改变会触发两次set,分别是数组的长度变化以及索引值的变化,接下来我们就需要屏蔽掉多次触发的问题。
1 | const toProxy = new WeakMap(); // 存放被代理过的对象 |
effect 实现
effect 也就是副作用的意思,这个方法默认会在调用的时候率先执行一次,之后如果数据有变化后则会再次触发此回调函数。
1 | const person = Vue.reactive({ name: "cangshudada" }); //person对象已经成为响应式数据 |
我们先来实现effect函数
1 | /** |
当调用 fn()时可能会触发 get 方法,此时会触发上面 get 中调用的 track 函数
1 | const targetMap = new WeakMap(); |
当更新属性时会触发 trigger 执行,并根据 key 值找到对应的存储集合中的 effect 依次执行
1 | function trigger(target, type, key) { |
这个时候其实还存在 length 的问题,比如我们在 effect 中监听数组的 length,这个时候因为我们上面在 set 函数中设置了 length 改变不触发 trigger 函数的机制,所以还需要在 trigger 中增加判断来兼容这种情况1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => {
deps();
});
}
// 兼容处理当前更新类型是增加时,如果用到数组的length的effect应该也会被执行
if (type === "add") {
const lengthDeps = depsMap.get("length");
if (lengthDeps) {
lengthDeps.forEach(effect => {
effect();
});
}
}
}
ref 实现
ref 可以将原始数据类型同样转换成响应式数据,这个时候需要通过.value 属性获取值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/*
*
* @description 不同类型的数据响应式处理 如果是对象通过reactive函数进行数据绑定否则直接返回
*/
function convert(target) {
return isObject(target) ? reactive(target) : target;
}
function ref(raw) {
raw = convert(raw);
const v = {
_isRef: true, // 标识是ref类型
get value() {
track(v, "get", "");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(v,'set','');
}
};
return v;
}
这个时候问题又来了,假如出现如下情况,则每次调用都得多加一个.value 就会非常麻烦,所以我们也得对这种情况做个兼容1
2
3
4
5const name = ref('cangshudada');
const person = reactive({
c_Name: name
});
console.log(person.c_Name.value); // 每次调用 c.a 都得加上.value 比较麻烦
这个时候需要在 get 函数中兼容1
2
3
4
5
6
7
8
9
10get(target, key, receiver) {
// 取值
const res = Reflect.get(target, key, receiver);
// 兼容 ref 的 value 情况 因为前面的判断所以 ref 不可能为对象 可以直接返回
if(res.\_isRef){
return res.value
}
track(target, 'get', key);//收集依赖
return isObject(res) ? reactive(res) : res; // 懒代理
}
computed 实现
之前版本的 computed 函数会缓存监听变量的值,只有当监听的变量值发生变化函数才会触发,在实际项目中用处非常大,如今 vue3.0 响应式数据机制重写,也导致了 computed 的重写,我们来看看在 vue3.0 computed 是如何实现的,首先我们来看看用法1
2
3
4
5
6
7
8
9
10const person = reactive({name:'cangshudada'});
const _computed = computed(()=>{
console.log('computed执行了')
return `${person.name} --- xixi`;
})
// 不取_computed.value值则回调函数不执行,除非监听对象改变则取n次只执行一次
console.log(_computed.value);// computed执行了 cangshudada --- xixi
console.log(_computed.value);// cangshudada --- xixi
person.name = '仓鼠大大';
console.log(_computed.value);// computed执行了 仓鼠大大 --- xixi
computed 代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function computed(fn){
let dirty = true; // 第一次取值会触发
const runner = effect(fn,{ // 标识这个effect是懒执行
lazy:true, // 懒执行
scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect 依赖不更新则不更新dirty,进而不会触发runner(),缓存机制
dirty = true;
}
});
let value;
return {
_isRef:true,
get value(){
if(dirty){
value = runner(); // 执行runner会继续收集依赖
dirty = false;
}
return value; // value没变化不会执行computed回调
}
}
}
修改 effect 函数 此处建议结合effect实现查看1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function effect(fn,options) {
let effect = createReactiveEffect(fn, options);
if(!options.lazy){
effect();
}
return effect;
}
function createReactiveEffect(fn,options) {
const effect = function() {
return run(effect, fn);
};
effect.scheduler = options.scheduler;
return effect;
}
在 trigger 时判断1
2
3
4
5
6
7deps.forEach(effect => {
if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect
effect.scheduler(); // 将dirty设置为true,下次获取值时变可以重新执行runner方法
}else{
effect(); // 否则正常执行effect即可
}
});
1 | const person = reactive({name:'cangshudada'}); |
至此我们就将 Vue3.0 源码中的 reactivity 部分解析完毕了!了解了 vue 的数据绑定机制对于之后不管是面试还是后期的应用都有着很大的帮助,当然本篇文章只是对这部分进行了简要地解析,清楚了数据绑定这部分的逻辑与思想后再来读源码这部分相信各位会有更多的收获。
提问:
- 提问一:如何判定在什么情况下去收集该值的依赖?
- 提问二:如何判定在什么情况下去触发该值的依赖
- 提问三:该值的依赖收集后要存储在什么地方?
- 提问四:markRaw shallowRef shallowReactive toRefs
通过一个实例化类 Dep 来管理该值的更新机制,在该值被调用的时刻去收集该值的依赖,在该值变更的时刻去触发该值的所有依赖
其他参考链接:
https://segmentfault.com/a/1190000023465134
https://segmentfault.com/a/1190000020629159
https://www.infoq.cn/article/hzmbaolyeqanup0ycpzx
https://juejin.cn/post/6972350540210503693#heading-6
http://www.uigame.net/article/362987.html
https://juejin.cn/post/6972350540210503693
https://jishuin.proginn.com/p/763bfbd385e2
Vue2.0: https://juejin.cn/post/6857669921166491662
Composition Api vs Options API: https://blog.csdn.net/liuliuliuliumin123/article/details/113825310
compoted: https://www.cnblogs.com/xiaoheibanfe/p/14131508.html
computed: https://juejin.cn/post/6979055167228346376
vue3 更新机制的理解:https://juejin.cn/post/6912419591847313422
effect: https://juejin.cn/post/6897109326108819464
《深入浅出vue.js》
《剖析 Vue.js 内部运行机制》
Reflect: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
Symbol: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol