打开了尘封已久的Vue,在上一篇中讲解了Vue3的响应式原理。接下来考虑一些缺陷问题。
在上一篇中,我们可以封装trigger和track去分别设置set和get方法。
function track(target, key) {
if (!reactiveFns) return target[key];
let targetDepend = busket.get(target);
if (!targetDepend) {
targetDepend = new Map();
busket.set(target, targetDepend);
}
let depend = targetDepend.get(key);
if (!depend) {
depend = new Set();
targetDepend.set(key, depend);
}
depend.add(reactiveFns);
}
function trigger(target, key) {
let targetDepend = busket.get(target);
if (!targetDepend) return;
let depend = targetDepend.get(key);
if (!depend) return;
depend.forEach((fn) => {
fn();
});
}
整个proxy代理就非常简洁:
const objProxy = new Proxy(obj, {
get: function (target, key, receiver) {
track(target, key); //存储当前副作用函数
return target[key];
},
set: function (target, key, newVal, receiver) {
target[key] = newVal;
trigger(target, key);取出副作用函数执行
},
});
基本的响应式完成,当watchFn副作用函数执行,收集响应式依赖的时候会自动收集到bukket中。这一点第一篇也有阐述过。
那如果是这一种情况就另外考虑了:
let obj = {
name: "mjc",
age: 18,
ok: true,
text: "text",
};
watchFn(function () {
document.innerHTML = objProxy.ok ? objProxy.text : "not";
console.log("副作用执行---------");
});
watchFn的执行会收集objProxy.ok副作用函数,同时也会收集objProxy.text的桶中。那么如果我对objProxy.ok设置为false,那么接下来的副作用执行的时候objProxy.text无法被收集依赖。
也就是,一旦objProxy.ok为false,无论副作用如何执行, document.innerHTML的值永远不变。那既然这样,就不叫副作用了,根本没有对任何做出影响。可是,此时我们根本没有取消这个busket的依赖。
如果设置objProxy.text,依然会触发副作用,这是不应该的。
图2
很明显objProxy.text无法造成副作用了,但是set的时候仍然触发副作用,这是多此一举了。
对watcnFn再次改造:
function watchFn(fn) {
const effect = () => { //新增
reactiveFns = effect;//新增
fn && fn();
};
effect.dep = [];//新增 用来存储当前副作用的依赖集合
effect();//新增
}
effect虽然是一个函数,本质其实也是一个对象,所以可以设置dep属性。
effect.deps=[]用来收集包含当副作用函数的所有依赖的集合
什么意思?待会就知道了这样的意义。
对watcnFn再次改造:
function track(target, key) {
...
if (!depend) {
depend = new Set();
targetDepend.set(key, depend);
}
depend.add(reactiveFns);
reactiveFns.deps.push(depend); //新增
}
effect.deps存储了当前的Map中的某个属性的set对象
接下来我们写清理函数,再每次重新执行Fn的时候,先取消所有副作用和响应式对象属性的联系,再通过Fn()重新创建联系,避免分支切换导致的影响。
cleanup函数
function clearup(effectFn) {
//遍历effectFn.deps的数组
for (let i = 0; i < effectFn.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
effectFn.deps.length = 0;
}
}
我们只需要每次副作用执行之前,先执行这个cleanup再执行Fn,确保每次断开联系再重新建立联系。
function watchFn(fn) {
const effectFn = () => {
clearup(effectFn); //新增
reactiveFns = effectFn;
fn();
};
effectFn.dep = [];
effectFn();
}
行,发现一直在循环..,终究是一个forEach遍历的问题。
在forEach遍历Set中,如果一个值已经被访问过了,但是该值被删除后重新加入到集合中,那么本次遍历没有结束,仍然继续被重新访问这个值。
我们得不让原来的set对象来遍历,构建一个新的set来遍历
function trigger(target, key) {
let targetDepend = busket.get(target);
if (!targetDepend) return;
let depend = targetDepend.get(key);
if (!depend) return;
const newDepend = new Set(depend)
newDepend.forEach((fn) => {
fn();
});
}
这样depend的删除和添加不影响newDepend的遍历结束。
至此,分支切换的副作用遗留函数问题就解决了。
我们都知道Vue是组件的嵌套来渲染的,在根组件下,继续嵌套子组件和孙子组件。如:
<App.vue
<template>
<MjcFather>
<MjcSon>
...
</MjcSon>
</MjcFather>
</template>
App.vue下渲染了子组件MjcFather,MjcFather又渲染了子组件MjcSon。那么在vue2中,是用h函数来嵌套渲染的。在vue3中,组件的渲染成了effect函数执行。
比如MjcFather,是由如下:
const MjcFather = {
render(){
return /*... */
}
}
当渲染的时候用effect来渲染
effect(()=>{
MjcFather.redner()
})
那再嵌套呢?
//MjcSon
const MjcSon = {
render(){
/* ... */
}
}
//MjcFather
const MjcFather = {
render(){
return (
<MjcSon />
)
}
}
用effect来表示它的结构:
effect(()=>{
MjcFather.render()
effect(()=>{
MjcSon.render()
})
})
这就很神奇是吧,组件的嵌套居然是effec函数的嵌套渲染,vue3的响应式系统之所以比vue2能在效率上提升,这里也是有独特新奇之处。effect函数不单是响应式数据的收集,也是渲染的一种方式。
但是我们所写的effect函数还不行做到嵌套,问题在于我们的effect函数用全局变量reactiveFns去注册当前激活的副作用,如果发生嵌套,会是这样的效果。
举个例子:
let temp1,temp2;
let reactiveFns= null;
effect(function effect1(){
console.log('effect1')
effect(function effect2(){
console.log('effect2')
temp2 = obj.bar //假设obj为响应式数据
//读取依赖触发收集
})
temp1 = obj.foo//再次读取
})
问题在于effect2入栈后,当前的reactiveFns存储值由effect1变为effect2,obj.bar的桶收集了effect2,接下来的obj.foo的桶收集却依然收集了effect2而不是effec1!这就是嵌套导致的问题。
那么这种方式的处理方法用栈的先入后出的方式去实现。 是不是用栈去解决这种问题很少见?我们在原来的effect副作用设计上进行改造
let reactiveFns
const effectStack = [] //声明一个栈用数组
function watchFn(fn) {
const effectFn = () => {
clearup(effectFn);
reactiveFns = effectFn;
effectStack.push(effectFn) //新增 副作用执行前,推入栈
fn();
effectStack.pop(effectFn) //新增 副作用执行后,弹出栈
reactiveFns = effectStack[effectStack.length -1] //新增 让当前存储副作用函数的重新从栈内退一位
};
effectFn.dep = [];
effectFn();
}
在JavaScript中使用栈,我也是第一次学会。本文思路和设计来源于《Vue设计与实现》的作者霍春阳,非本人原创思路,个人学习思想实操学习,如有侵权,可联系我删除本文。
总结
1.effect嵌套与分支切换的理解和思想。
2.effect的组件嵌套渲染以及Js中栈的概念使用
参考资料
[1] 《Vue设计与实现》作者霍春阳
[2] 博客 blog
mjcelaine.top