响应系统的设计与实现(2)
上一章节我们基本实现了一个简单的响应式系统,代码如下,本章节我们将继续优化代码,如果没看过上章节内容的可以先去看下上章节内容 链接
let activeEffect
const bucket=new WeakMap();
function effect(fn){
const effectFn=()=>{ //我们需要把副作用函数和清楚这个逻辑绑定在一起,所以需要写在一块,
//每次set后触发副作用函数又再次触发了get,所以副作用函数又一次进行了收集,每次收集之前都要先进行清除
cleanup(effectFn);
activeEffect=effectFn;
fn()
}
//初始化deps,只有第一次执行的时候才会执行,后面都是响应式的时候执行effectFn函数的时候在进行收集
effectFn.deps=[];
effectFn();
}
function cleanup(fn){
for(let i=0;i<fn.deps.length;i++){
const dep=fn.deps[i]
dep.delete(fn);
}
fn.deps.length=0;
}
function track(target,key){
if(!activeEffect) return;
let depsMaps=bucket.get(target)
if(!depsMaps){
bucket.set(target,(depsMaps=new Map()));
}
let deps=depsMaps.get(key);
if(!deps){
depsMaps.set(key,(deps=new Set()));
}
deps.add(activeEffect)
activeEffect.deps.push(deps)// 进行收集
}
function trigger(target,key){
const depsMaps=bucket.get(target)
if(!depsMaps) return;
const effects=depsMaps.get(key);
const newSet=new Set(effects);
newSet&&newSet.forEach(fn=>fn());
}
//上述代码就可以更改为以下形式了
const data={a:'111'}
const bucket=new WeakMap()
const obj=new Proxy(data,{
get(target,key){
track(target,key)
return target[key]
},
set(target,key,newVal){
target[key]=newVal;
trigger(target,key)
}
})
嵌套的Effect
在vue中的体现就是组件中拥有组件
effect(()=>{
foo.render()
effect(()=>{
bar.render();
})
})
思考一下,下面这段代码的输入
const data={foo:true,bar:true}
const obj=new Proxy(data,...)
let temp1,temp2;
effect(()=>{
console.log('effect1');
effect(()=>{
console.log('effect2');
temp2=obj.bar;
})
temp1=obj.foo;
})
这段代码想实现的想过是obj.foo→effect1,obj.bar→effect2;就是说当我们修改obj.foo的时候会输出effect1,由于effect2嵌套在effect1中所以effect2也会输出,修改bar的时候会输出effect2;
我们可以执行一下上面的代码.
我们会发现输出如下
effect1;
effect2;
修改obj.foo后输出
effect2.
我们发现修改后他的输出不符合我们的预期,我们的预期应该是修改完后输出effect1但由于effect2嵌套在内部所以也会输出effect2.但现在只输出了effect2,问题出在哪里呢?
其实很好理解,问题就是出在我们的activeEffect是唯一的,当执行effect1的时候activeEffect为effect1,当执行到effect2的时候它就变成了effect2,所以obj.foo的桶内存的副作用函数就变成了effect2了。
我们只需要修改effect函数即可,我们可以用一个栈来收集嵌套的副作用,当副作用执行完后则从栈中弹出,让activeEffect为下一个副作用函数即可
const effectStack=[];
function effect(fn){
const effectFn=()=>{ //我们需要把副作用函数和清楚这个逻辑绑定在一起,所以需要写在一块,
//每次set后触发副作用函数又再次触发了get,所以副作用函数又一次进行了收集,每次收集之前都要先进行清除
cleanup(effectFn);
activeEffect=effectFn;
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect=effectStack[effectStack.length-1];
}
//初始化deps,只有第一次执行的时候才会执行,后面都是响应式的时候执行effectFn函数的时候在进行收集
effectFn.deps=[]
effectFn()
}
避免无限递归
effect(()=>{
obj.foo=obj.foo++;
});
上述代码假设obj是一个响应式数据,那么当我们执行上面代码的时候首先会触发track方法进行副作用的收集,再然后我们会发现它还修改了值,这时候就会触发trigger,他在同一个方法里既触发了track,又触发了trigger,这时候就会发现进入了死循环,触发了trigger有执行了副作用,又触发了上一步骤的内容,就会进入死循环,怎么解决这个问题呢,很简单,只要判读当副作用是不是当前的activeEffect,如果是就不执行,说明当前的副作用还在收集的过程中.
我们只需要修改trigger就行了
function trigger(target,key){
const depsMaps=bucket.get(target)
if(!depsMaps) return;
const effects=depsMaps.get(key);
const newSet=new Set();
effect&&effect.forEach((fn)=>{
if(fn!==activeEffect){
newSet.add(fn)
}
})
newSet&&newSet.forEach(fn=>fn());
}
支持调度执行
支持调度顾名思义就是让用户可以自定义函数是什么时候执行了的。
const data={foo:1};
const obj=new Proxy(data,....);
effect(()=>{
console.log(obj.foo)
})
obj.foo++;
console.log('执行了');
上面代码我们的输出肯定是
1
2
执行了
如果我们要把2放在执行了后面我们该怎么做呢,这时候就有个自定义调度的概念了,我们修改effect函数
const effectStack=[];
function effect(fn,options={}){
const effectFn=()=>{ //我们需要把副作用函数和清楚这个逻辑绑定在一起,所以需要写在一块,
//每次set后触发副作用函数又再次触发了get,所以副作用函数又一次进行了收集,每次收集之前都要先进行清除
cleanup(effectFn);
activeEffect=effectFn;
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect=effectStack[effectStack.length-1];
}
effectFn.options=options
//初始化deps,只有第一次执行的时候才会执行,后面都是响应式的时候执行effectFn函数的时候在进行收集
effectFn.deps=[]
effectFn()
}
我们给effect第二个参数options,让他可以传入我们的一些自定义属性,比如这边我们就传入scheduler就可以试试
const data={foo:1};
const obj=new Proxy(data,....);
effect(()=>{
console.log(obj.foo)
},{
scheduler(fn){
setTime(fn)
}
})
obj.foo++;
console.log('执行了');
我们传入调度器属性为一个函数,这个函数接收一个函数参数并把接收到的函数放入宏任务队列中去,所以我们修改trigger函数,如果有调度则直接传入调度器中否则就正常执行即可
function trigger(target,key){
const depsMaps=bucket.get(target)
if(!depsMaps) return;
const effects=depsMaps.get(key);
const newSet=new Set();
effects&&effects.forEach((fn)=>{
if(fn!==activeEffect){
newSet.add(fn)
}
})
newSet&&newSet.forEach(fn=>{
if(fn.options.scheduler){
fn.options.scheduler(fn);
}else{
fn();
}
});
}
计算属性的实现
懒执行的effect,前面的方式执行我们的effect方法会马上执行传入的函数,但是在vue中有部分是不需要立即执行的,比如computed属性,这个时候我们就可以通过options属性来达到目的。
const effectStack=[];
function effect(fn,options={}){
const effectFn=()=>{
cleanup(effectFn);
activeEffect=effectFn;
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect=effectStack[effectStack.length-1];
}
effectFn.options=options
effectFn.deps=[]
//如果有lazy属性,则立即不执行函数。
if(!options.lazy){
effectFn()
}
return effectFn;
}
我们可以在进行一下修改,因为computed的时候一般都是返回一个计算后的值,上面的代码返回计算的函数,还需要我们手动执行才能拿到真正的值,所以这时候我们就要修改上面的代码,将副作用函数计算后的值返回
const effectStack=[];
function effect(fn,options={}){
const effectFn=()=>{
cleanup(effectFn);
activeEffect=effectFn;
effectStack.push(effectFn);
const res=fn();
effectStack.pop();
activeEffect=effectStack[effectStack.length-1];
return res
}
effectFn.options=options
effectFn.deps=[]
//如果有lazy属性,则立即不执行函数。
if(!options.lazy){
effectFn()
}
return effectFn;
}
那么我们的计算属性就可以如下,修改依赖值就会发现sum值也会改变
function computed(getter){
const effectFn=effect(getter,{
lazy:true
});
const obj={
get value(){
return effectFn();
}
}
return obj
}
const sum = computed(() => {
console.log('执行了')
return obj.foo + obj.bar
})
我们还会发现当我们每次执行sum.value的时候都会执行effectFn这个函数,即使依赖值未改变也会执行,这与我们的预期不符合,我们修改代码如下
function computed(getter){
let value;
let dirty=true
const effectFn=effect(getter,{
lazy:true
});
const obj={
get value(){
if(dirty){
value=effectFn();
dirty=false;
}
return value
}
}
return obj
}
我们设置一个dirty变量值在第一次执行时会调用effectFn,后面就不会继续执行effectFn,那么如果我们的依赖值变化后怎么把dirty重新制为true呢,只需要添加调度器就行了
const effectFn=effect(getter,{
lazy:true
scheduler(){
dirty=true;
}
});
我们的计算属性已经接近完美了但还是差一点火候,看下面代码
const sum = computed(() => {
console.log('执行了')
return obj.foo + obj.bar
})
effect(()=>{
console.log(sum.value)
})
当我们修改obj.foo的值时会发现并没有执行后面的输出,就好比vue模版中使用计算属性,但是我们的计算属性的值并不是响应式的,无法进行模版更新,这时候该怎么做呢,这时我们只需要手动收集和触发就行了,如下代码,我们的track其实就是在weakmap中存入,trigger就是取出然后执行,computed不是自动的响应式,他没用proxy来进行代理,而是我们手动执行的。
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(obj, 'value')
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
//此时avtiveEffectFn为
track(obj, 'value')
return value
}
}
return obj
}
这样修改后你就会发现computed的依赖修改后,收集到的副作用也会自动执行了
watch的实现
watch的实现其实就是对调度器的使用
function watch(source,cb){
effect(()=>
source.foo)
,{
scheduler(){
//...自定义操作
cb();
}
})
}
上面代码表示我们每次对source.foo进行修改的时候都会执行cb(),但是foo这个属性是我们硬编码进行的,所以我们需要抽象一个函数出来进行修改.
我们只需要写一个函数,这个函数读取了对象中所有的属性即可。
function traverse(value,seen=new Set()){
//一些不需要继续执行下去的值。
if(typeof value!=='object'||value===null||seen.has(value))return;
for(const key in value){
traverse(value[key],seen);
}
return value
}
watch的第一个参数可以是一个getter也就是函数,这个时候就要对watch进行判断了
function watch(source,cb){
let getter
if(typeof source==='function'){
getter=souce
}else{
getter=traverse(source)
}
effect(getter,{
scheduler(){
//...自定义操作
cb();
}
})
}
如何获取新值与旧值呢?
function watch(source, cb) {
let newVal,oldVal
const effectFn= effect(
() => traverse(source),
{
lazy:true,
scheduler() {
//...自定义操作
newVal=effectFn();
cb(newVal,oldVal);
oldVal=newVal;
}
}
)
oldVal=effectFn()
}