theme: geek-black highlight: monokai
Vue3
的稳定版发布了很久了,去阅读官网的文档时发现推荐的一个优秀的 Vue
教程网站:Vue Mastery,里面的一篇Vue 3 Reactivity(Vue3 响应式)讲的是真的不错。跟着学完收获很多,顺着课程的思路总结一篇 Vue3
响应式的笔记,手动还原响应式的原理。
手动实现响应式
响应式,就是一个变量
依赖了其他变量,当被依赖的其他变量更新后该变量
也要响应式的更新。所以从零开始,先实现手动的更新。
单个变量的手动响应
先看几个单词的意思,
dep
是dependence
的缩写,也就是依赖。effect
,指因某种原因导致产生结果,着重持续稳定的影响。track
,指追踪、踪迹。trigger
,指触发。
再看手动实现的代码,这里用到了 Set
,不熟悉的话可参考 MDN:
let price = 5;
let quantity = 2;
let total = 0;
// dep是一个“依赖”集合,用来存放众多的effect
let dep = new Set();
/**
* effect就是“影响”,其实反过来说就是单个的“依赖”
* total “依赖”的就是 price 和 quantity,也就是被 price 和 quantity "影响"
*/
let effect = () => {
total = price * quantity;
};
/**
* track就是“追踪”,其实就是留下记录
* 做的事情就是将 effect函数 添加到 dep 这个集合中
*/
function track() {
dep.add(effect);
}
/**
* trigger是触发
* 就是执行了保存在 dep 这个集合中的所有 effect函数
*/
function trigger() {
dep.forEach((effect) => effect());
}
track();
effect();
console.log(total); // output: 10
上述代码是最最简单的实现,流程就是三步:
- 通过
effect
来表明影响total
的依赖 - 通过
track
来保存effect
- 通过
trigger
来执行effect
最后输出的 total
肯定就是计算后的 10
。
对对象的多个属性手动响应
上个例子中 price
和 quantity
都是放在了不同的变量里,现在更进一步,把他们放到同一个对象里 let product = { price: 5, quantity: 2 }
,现在如果想让 product
对象变为响应式,就需要指定每个键的响应。
depsMap
的意思就是 dependence
的 map
。也就是一个 map
中,每个键都对应某个属性的 dep
。
这里用到了 Map
,不熟悉的话可参考 MDN
// 新建一个Map来存储deps
const depsMap = new Map();
/**
* 与上例中的变化是,指定了个参数key,表明effect存储到对象哪个键对应的的dep中
*/
function track(key) {
let dep = depsMap.get(key);
// 对应dep不存在时就new一个
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 把effect添加进去
dep.add(effect);
}
/**
* 同样,触发的时候也要指定一个key,表明执行的是对象哪个键的对应的dep中的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;
};
// 首次调用计算出total
effect();
console.log(total); // output: 10
// 将effect保存给 quantity 键对应的dep
track('quantity');
product.quantity = 3;
// 执行 quantity 键对应 dep 保存的 effect
trigger('quantity');
console.log(total); // output: 15
在上述代码中,实现了对整个 product 对象的手动响应。
对多个对象的多个属性进行手动响应
继续升级上述代码,如果有多个对象需要响应式,那么就需要给不同的对象设置不同 depsMap
。所以创建一个 WeakMap
类型的变量,命名为 targetMap
来存储多个对象的 depsMap
,target
就是指的需要被响应式的对象。
而之所以用 Map
类型是因为 Map
可以用“对象”作为键,用 WeakMap
方便对键(也就是被响应式的对象)进行垃圾回收。不熟悉 WeakMap
的话可参考 MDN
// 新建一个 WeakMap 来存储 depsMap
const targetMap = new WeakMap();
/**
* 与上例中的变化是,多指定了个参数 target,表明 effect 存储到哪个对象对应的的 depsMap 中
*/
function track(target, key) {
// 新增👇
let depsMap = targetMap.get(target);
// 对应depsMap不存在时就new一个
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 👆
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
/**
* 同样,触发的时候也要指定一个 target,表明执行的是哪个对象的对应的depsMap中的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;
};
// 首次调用计算出total
effect();
console.log(total); // output: 10
// 将 effect 保存给对应 product对象 的 depsMap 中的对应 quantity键 的 dep
track(product, 'quantity');
product.quantity = 3;
// 执行对应 product对象 的 depsMap 中的对应 quantity键 的 dep 中保存的 effect
trigger(product, 'quantity');
console.log(total); // output: 15
上述代码实现了对不同对象的不同键进行手动响应。到了这一步,可以用课程中的一张图来清楚的表示下 targetMap
、depsMap
、dep
之间的关系:
变为自动响应
继续升级代码,给上述代码添加自动响应。在这里用到了 Proxy
和 Reflect
, 不熟悉的话可参考 MDN。
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();
});
}
}
// 新增 👇
/**
* @description: 例用Proxy和Reflect实现自动响应式
* @param {Object} target 要响应的对象
* @return {Proxy} 返回要响应对象的代理
*/
function reactive(target) {
const handlers = {
get(target, key, receiver) {
let result = Reflect.get(target, key, receiver);
// 在访问这个target对象的key键之前,先把effect保存下
track(target, key);
return result;
},
set(target, key, value, receiver) {
let oldValue = target[key];
// 下面两步的顺序不能颠倒,很关键
// 这一步其实就已经赋值成功了
let result = Reflect.set(target, key, value, receiver);
// 到这里再执行get时获取的是新设的值
if (result && oldValue != value) {
// 如果把这个target对象的key键的值改了,就得执行一遍对应的effect
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;
};
// 首次调用计算出total
effect();
console.log(total); // output: 10
// 注意:在这里与前面代码的不同就是我们没有手动调用trigger,实现的自动响应
product.quantity = 3;
console.log(total); // output: 15
这段代码实现了自动响应,最关键的核心部分就是 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
。
下面来优化上述两个问题:
const targetMap = new WeakMap();
let activeEffect = null; // 👈 新增,是否需要添加effect的标志
function track(target, key) {
// 👇 新增,只有再activeEffect为真时才执行保存的操作
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); // 👈 修改,有直接添加effect改为了添加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);
}
// 👇 新增
// 为了用统一的方式,把eff添加到对应的dep中,顺便还执行了一遍设置了初始值
// 这样以后,只有我们手动调用effect那次才会保存dep,用trigger触发的get就不会再保存一遍了
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
// 👆
let product = reactive({ price: 5, quantity: 2 });
let salePrice = 0;
let total = 0;
// 👇 同理也要对effect函数改造,把每一个要保存的dep变为了effect函数的参数
// 手动设定了total呃salePrice的初始值
effect(() => {
total = product.price * product.quantity;
});
// 所以这里就不会把这个eff添加给quantity
effect(() => {
salePrice = product.price * 0.9;
});
// 👆
console.log(total, salePrice); // output: 10, 4.5
// 设置quantity只会重新计算total
product.quantity = 3;
console.log(total, salePrice); // output: 15, 4.5
// 设置price后,total和salePrice对应的effect都会执行,都会被重新计算
product.price = 10;
console.log(total, salePrice); // output: 30, 9
上述代码通过添加了一个 activeEffect
的标志位,解决了无效的重复执行保存的缺点;并把 effect()
变为 effect(eff)
的带参数形式,解决了多个 effect
的问题。
到这里为止,Vue3 Composition API
中 reactive
方法的实现流程已经被我们手动大致实现了一遍!
实现 ref
在 Vue3 Composition API
设计中,reactive
主要用于引用类型
,另外专门提供了一个 ref
方法实现对原始类型
的响应式。
大部分的地方都不用变,基本上就是添加了个 ref
方法,实现的方式就是对象访问器 getter/setter
来模仿 Proxy
:
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);
}
// 👇 新增
/**
* @description: ref使用getter和setter实现,模仿了Proxy的get和set
* @param {Primary} raw
* @return {Object} 返回响应对象
*/
function ref(raw) {
const r = {
get value() {
// 在get之前,先保存到targetMap中
track(r, 'value');
return raw;
},
set value(newVal) {
raw = newVal;
// set了之后,触发effect更新
trigger(r, 'value');
},
};
return r;
}
// 👆
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
let product = reactive({ price: 5, quantity: 2 });
let salePrice = ref(0); // 👈 修改 此时的salePrice自身也是个响应式对象
let total = 0;
// 此时的salePrice自身也是个响应式对象
effect(() => {
salePrice.value = product.price * 0.9; // 👈 修改
});
// 注意这里计算总价的方式变了,使用的是打折后的值来计算
effect(() => {
total = salePrice.value * product.quantity; // 👈 修改
});
console.log(total, salePrice); // output: 9, 4.5
product.quantity = 3;
console.log(total, salePrice); // output: 13.5, 4.5
product.price = 10;
console.log(total, salePrice); // output: 27, 9
其实可以发现,reactive
也能实现对原始类型的响应式,为什么还要专门提供一个 ref
方法?看对尤大的访谈中,尤大回答的是 reactive
还会添加更多处理流程,对于原始类型来说,是一种无用的负担。
实现 computed
跟响应式相关的最后一部分内容就是 computed
了,继续来手动实现它:
// 👇 代码不变
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;
}
// 👆 代码不变
// 👇 新增
/**
* @description: computed实现,其实就是封装了ref
* @param {Function} getter 取值函数
* @return {Object} ref返回的对象
*/
function computed(getter) {
// 创建一个响应式的引用
let result = ref();
// 用effect封装着调用getter,将结果设给result.value的同时,也将eff保存在了targetMap中的对应位置
effect(() => (result.value = getter()));
// 最后把result返回
return result;
}
// 👆
let product = reactive({ price: 5, quantity: 2 });
// 👇 修改
// 此时的salePrice自身也是一个响应式对象
let salePrice = computed(() => {
return product.price * 0.9;
});
// total也是个响应式对象
let total = computed(() => {
return salePrice.value * product.quantity;
});
// 👆
console.log(total.value, salePrice.value); // output: 9, 4.5
product.quantity = 3;
console.log(total.value, salePrice.value); // output: 13.5, 4.5
product.price = 10;
console.log(total.value, salePrice.value); // output: 27, 9
可以发现,computed
本质上就是封装了 ref
方法,用 effect
封装着来调用 getter
,将结果设给 result.value
的同时,也将 eff
保存在了 targetMap
中的对应位置,实现了 computed
的响应式。
Vue3 源码中响应式的实现
Vue3
整体是用 Typescript
写的,reactivity
是一个独立的模块,源代码位于packages/reactivity/src目录下,几个不同的方法分别位于不同文件中:
effect
、track
、trigger
方法,位于 effect.ts。Proxy
的get
和set
这些handler
方法,位于 baseHandlers.ts。reactive
方法位于 reactive.ts,使用了Proxy
。ref
方法位于 ref.ts,使用了对象访问器。computed
方法位于 computed.ts,使用了effect
和ref
。
关于 Vue Mastery 课程
Vue Mastery
课程是收费的,25% 的收入会捐给 Vue
项目,所以大家对课程感兴趣的话可以开会员支持一波。不过它的会员很贵,可以有一些取巧的方法跳过收费验证,可以关注“林景宜的记事本
”公众号发送“Vue3 响应式
”获取方法试看一波。
前端记事本,不定期更新,欢迎关注!
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 林景宜 原文链接:https://juejin.im/post/6938702983014121485