您的位置:首页 > 健康 > 美食 > vue.js的设计与实现(响应系统2)

vue.js的设计与实现(响应系统2)

2024/10/31 14:51:30 来源:https://blog.csdn.net/tinglis/article/details/141466250  浏览:    关键词:vue.js的设计与实现(响应系统2)

文章目录

    • 概要
    • 分支切换与cleanup
    • 嵌套的effect与effect栈
    • 避免无限递归循环
    • 调度执行
    • 小结

概要

接上文,我们已经写出了基础的effect收集,但是还是会有些问题。这一篇,我们就是来解决这些问题的

分支切换与cleanup

首先,我们需要明确分钟切换的定义,看下以下代码:

const data = {text:'hello word',ok:true
}
const obj = new Proxy(data,{/*...*/})
effect(()=>{document.body.innerText = obj.ok?obj.text:'not'
})

当effectFn (effect(effectFn) effectFn是effect传参的函数) 函数内部是一个三元表达式的时候,会根据obj.ok的值执行不同的代码分支,当obj.ok的值发生变化时,代码执行的分支也会跟着变化,这就是所谓的分支切换

执行以上代码,我们发现当obj.ok的值时false的时候,修改obj.text的值。effectFn也会重新执行,这个肯定是有问题的,因为在effectFn中,我们走的分支并没有读取到obj.text的值。原因在于第一次执行的时候读取了obj.text的值,已经把effectFn对应的key值 text 放到了依赖收集的桶中,所以更改obj.text值的时候,就会取出effectFn执行。

我们已经知道原因了,那想必各位同学都有想法了吧?对的我们只要每次执行的副作用函数的时候,把它从之前的所有已关联的依赖集合中删除,当副作用函数执行完毕后,就会建立新的联系。这里就有一个问题,我们要怎么知道它是和哪些依赖集合中呢?所有我们修改effect函数和tarck函数,如以下代码:

let activeEffect
function effect(fn){const effectFn = ()=>{cleanup(effectFn)activeEffect = effectFnfn()}//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
function cleanup(effectFn){for(let i =0 ;i <effectFn.deps.length;i++){const deps = effectFn.deps[i]deps.delete(effectFn)}effectFn.deps.length = 0
}
function tarck(target,key){if(!activeEffect) return const desMap = bucket.get(target)if(!desMap){bucket.set(target,desMap = new Map())}const deps = desMap.get(key)if(!deps){desMap.set(key,deps = new Set())}deps.add(activeEffect)activeEffect.deps.push(deps)
}

意思就是:我们在effectFn副作用函数上添加deps属性来记录所关联的依赖,在每次执行effectFn函数的时候,都会把effectFn.deps清空,我们可以看到在tarck函数的activeEffect.deps.push(deps)这句,deps是一个数组,数组里面存了deps这个Set的数据,Set是一个引用数据类型,所有push进入effectFn.deps的也是一个地址,这个地址指向了一个堆空间,我们不管在Set里面更改还是在effectFn.deps里面更改,更改的都是这个堆里面的内容。

我们执行上面代码,我们会发现,会一直无限循环,这是为什么呢?
我们看下下面这段代码:

const set = new Set([1])
set.forEach(item=>{set.delete(1)set.add(1)console.log('遍历中)
})

在语言规范中有明确的提到,在调用forEach遍历Set集合的时候,如果一个值被访问过,单该值被删除并重新添加到集合,如果此时forEach遍历还没有结束,就会重新访问,所以会进行无限递归。

那么问题又来了,要怎么解决呢?我们可以在用一个Set集合去遍历它,如以下代码:

const set = new Set([1])
const newSet = newSet(set)
newSet .forEach(item=>{set.delete(1)set.add(1)console.log('遍历中)
})

这里,同学们可以理解吗?set和newSet是两个不同的东西了,所以newSet遍历去更改set里面的值是对newSet没有任何影响的。所以就不会无限递归了

所以我们修改修改trigger函数里面的内容了,如下:

function trigger(target,key){const depsMap = bucket.get(target)if(!depsMap) return const effects = depsMap.get(key)const effectsToRun = new Set(effects)effectsToRun && effectsToRun.forEach(effectFn=>effectFn())
}

嵌套的effect与effect栈

我们可以设想一下,如果effect函数不支持嵌套,会出现什么的问题的:我们看下以下代码:

const data = {foo:true,bar:true}
const obj = new Proxy(data,{/*...*/})
let temp1,temp2
effect(function effectFn1(){console.log('effectFn1执行')effect(function effectFn2(){console.log('effectFn2执行')temp2 = obj.bar})temp1 = obj.foo
})

然后我们在修改一下 obj.foo的值,我们会发现 他执行的是 effectFn2,这是为什么呢?我们按照代码走下去:

  1. 创建了一个data对象
  2. 对data对象进行proxy代理
  3. 创建了两个变量
  4. 执行effect函数,这里我们要着重分析一下,我们先回顾一下 effect的函数内容,如下:
function effect(fn){const effectFn = ()=>{cleanup(effectFn)activeEffect = effectFnfn()}//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}

effect函数执行的时候 首先会把effectFn1函数放进来,activeEffect函数执行的就是effectFn1函数,到这里时没有问题,但是在fn执行的时候,我们就把effectFn2函数赋值给了activeEffect函数,这时候我们做了get操作,获取了obj.foo,此事,对应的activeEffect函数对应的effectFn2函数,所以foo这个key值对应的副作用函数就变成了effectFn2而不是effectFn1了。我们已经知道问题所在,那我们就把effect函数改造一下,如下:

//我们新建一个数组,来模拟栈
const  effectStack = []
function effect(fn){const effectFn = ()=>{cleanup(effectFn)effectStack.push(effectFn) activeEffect = effectFnfn()effectStack.pop()activeEffect = effectStack[effectStack.length -1]}//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}

我们在fn调用完成后 在effectStack里面弹出最后一个,再把activeEffect赋值effectStack的最后一项,这么做的目的就是为了保证 当前执行的key值的副作用函数,在执行get操作的时候,可以准确副作用函数添加到Set集合中,我们改完后再来执行一下的嵌套函数,我们再来分析一下:

  1. 创建了一个data对象
  2. 对data对象进行proxy代理
  3. 创建了两个变量
  4. 执行effect函数,先拿到effectFn1,把他加入到effectStack里面,然后执行effectfn1,然后我们就碰到了effectFn2,然后执行effectFn2,这时候,我们的activeEffect就会等于 effectFn2, 我们把effectFn2 push 到了effectStack里面,当effectFn2执行完成后,我们在effectStack弹出了effectFn2,把activeEffect赋值成effectFn1,我们在执行了obj.foo的get操作,我们就把key为foo的副作用函数对应到了effectFn1,这样就满足了我们的要求。

避免无限递归循环

我们在实现一个完美的响应式系统的时候,需要考虑到诸多的细节,我们试下下面的代码会造成什么样的情况

const data = { foo : 1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{obj.foo++})

执行上面的代码,我们会发现栈溢出,这是为什么呢?obj.foo++ 是不是等于 obj.foo = obj.foo +1,我们就会发现一个问题,这个代码执行get操作也执行了set操作,当我们执行set操作的时候,又会把副作用函数拿出来执行一次,就会往复循环,我们会知道在vue中,这种代码的话,就是只会执行一次,那我们就往一次的方向想。

我们会发现 它执行set和get的时候 activeEffect指向的函数都是一样的,那我们是不是可以加一个守卫条件,当set的时候 如果副作用函数和activeEffect时一样的时候,我们就不触发呢?
我们来试下,直接修改trigger函数:

function trigger(target,key){const depsMap = bucket.get(target)if(!depsMap) return const effects = depsMap.get(key)//const effectsToRun = new Set(effects) //直接在遍历赋值的时候,把activeEffect给排除掉const effectsToRun = new Set()effects && effects.forEach((effectFn)=>{if(effectFn != activeEffect){effectsToRun.add(effectFn)}})effectsToRun && effectsToRun.forEach(effectFn=>effectFn())
}

这样我们就可以避免无限递归调用了

调度执行

可调度性也是响应式系统非常重要的的特性,首先我们要知道什么是可调度性。所谓的可调度性,是指trigger函数执行副作用函数的时候,可以决定副作用函数的执行时机、次数以及方式
我们看下以下代码:

const data = { foo : 1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{console.log(obj.foo)
})
obj.foo++
console.log('结束了!')

这样代码输出结果是:

1
2
结束了

如果需求有变,我们输出的顺序要改成:

1
结束了
2

我们需要怎么不改变代码的顺序的情况下来改变执行结果呢?我们这个功能可以给用户自由调配。我们添加一个options来给用户自己配置,那我们需要更改以下代码了,如下:

effect(()=>{console.log(obj.foo)
},{scheduler(fn){//...}
})
function effect(fn,options={}){const effectFn = ()=>{cleanup(effectFn)effectStack.push(effectFn) activeEffect = effectFnfn()effectStack.pop()activeEffect = effectStack[effectStack.length -1]}effectFn.options = options //新增 把他挂载在副作用函数上//effectFn新增属性deps,用来存储所有与该副作用函数相关联的依赖集合effectFn.deps = []effectFn()
}
//在tirgger函数更改以下
function tirgger(target,key){const depsMap = bucket.get(target)if(!depsMap) return const effects = depsMap.get(key)//const effectsToRun = new Set(effects) //直接在遍历赋值的时候,把activeEffect给排除掉const effectsToRun = new Set()effects && effects.forEach((effectFn)=>{if(effectFn != activeEffect){effectsToRun.add(effectFn)}})effectsToRun && effectsToRun.forEach(effectFn=>{if(effectFn.options.scheduler){effectFn.options.scheduler(effectFn)}else{effectFn()}})
}

我们值执行副作用函数的时候,先判断以下是否存在调度器,如果存在就把副作用函数放到调度器中执行,由用户自己控制如何执行。
这样实现要求 我们只要在调用effect方法的时候,传入一个调度器就好了,如以下:

effect(()=>{console.log(obj.foo)
},{scheduler(fn){setTimeout(fn)}
})

这样可以控制副作用函数执行的时机,我们也要控制副作用函数的次数,如以下代码:

const data = { foo: 1}
const obj = new Proxy(data,{/*...*/})
effect(()=>{console.log(obj.foo)
})
obj.foo++
obj.foo++

通过运行以上代码,我们发现会打印

1
2
3

2只是过渡状态,我们只关注结果,而不关注过程,在这里,我们调度器可以很简单的实现:

//定义一个任务队列
const jobQueue = new Set()
//使用promise.resolve()创建一个promise实例,我们用它将一个任务放入微任务执行
const p = promise.resolve()
//给一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob(){if(isFlushing) returnisFlushing = truep.then(()=>{jobQueue.forEach(job => job())}).finally(()=>{isFlushing = false})
}
effect(()=>{console.log(obj.foo)
},{scheduler(fn){jobQueue.add(fn)flushJob()}
})

我们可以看到 我们把副作用函数的执行放到了微任务执行,使用set集合是为了set集合的去重能力,只有当副作用函数的调度器执行完成后,才会进行微任务的执行,这样就把重复的副作用函数过滤掉了

小结

到这里我们就可以发现,我们以及写出了相对完善的响应式系统了,下一章,我们学习一下,vue的计算属性和lazy

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com