文章参考了霍春阳的《Vue.js设计与实现》,是自己在阅读过程中的一些思考和理解
有些场景中,effect函数的执行并不是需要立即的。因此在options中加入lazy属性来判断当前的副作用函数是否需要立即执行。代码如下:
// 新增调度选项挂载
effectfn.options = options
effectfn.deps = []
//非lzay情况下直接执行
if(!options.lazy) { // <---新增条件
effectfn()
}
// 否则返回副作用函数
return effectfn
我们获得了副作用函数的返回,就能按照自己的逻辑来控制其在何处何时应该执行。现在将传入的副作用函数看作一个getter,则该函数就可以获得返回值。代码如下:
const effectfn = effect(
// 视作一个getter,获得返回值
() => proxy.text // <---此处修改
, {
// 调度器
scheduler(fn) {
queue.add(fn)
flushJob()
},
// lazy标记
lazy: true
})
const res = effectfn() // <--这样就能拿到其值
但是effect的实现中并没有返回值,只返回了一个副作用函数的执行,因此需要在effectfn中添加返回值。代码如下:
const effectfn = () => {
clearup(effectfn)
activeEffect = effectfn
// 栈设计
effectStack.push(effectfn)
// 将传入的effectfn函数执行结果保存下来
const value = fn() // <---新增
// 出栈
effectStack.pop()
// 设置当前全局activeEffect为栈顶effectfn
activeEffect = effectStack[effectStack.length-1]
// 返回函数结果 // <---新增
return value
}
到此,通过effectfn拿到真正副作用函数的执行返回。进而能实现计算属性。代码如下:
function computed(getter) {
// 将要获取的数据函数作为getter传入effect函数,同时设置lazy值
const effectfn = effect(getter, {
lazy: true
})
const obj = {
// 当读取其值时,才执行副作用函数
get value() {
return effectfn()
}
}
return obj
}
这样调用时,便可得到其值:
const res = computed(() => proxy.text) // <--得到obj
console.log(res.value) // <--获取其值时调用副作用函数
但是这种情况下,只是做到了懒加载,也就是说达到了我们需要时读取的操作,但是每次的读取依然会调用一次effectfn函数,即使数据没有发生变化,这样就增加了不必要的计算。因此加入了缓存机制。代码如下:
function computed(getter) {
// 存储计算后的值
let value
// 加入标志位,代表是否需要重新计算,刚开始设置为true,代表第一次需要获取
let dirty = true
// 将要获取的数据函数作为getter传入effect函数,同时设置lazy值
const effectfn = effect(getter, {
lazy: true
})
const obj = {
// 当读取其值时,才执行副作用函数
get value() {
// 如果需要重新计算,则调用effectfn,然后赋值给value,否则直接将原数据返回
if(dirty) {
value = effectfn()
dirty = false
}
return value
}
}
return obj
}
这样虽然能达到不用多次执行副作用函数的目的,但是dirty的值经过第一次修改后,就无法感知数据变化进而再次调用effectfn了。因此需要在数据发生变化时,让dirty变为true,也就是触发trigger的时候让dirty发生变化。结合上一模块的调度执行,为effect添加调度函数,因为调度器只会在
trigger函数中执行。代码如下:
// 将要获取的数据函数作为getter传入effect函数,同时设置lazy值
const effectfn = effect(getter, {
lazy: true,
// 新增调度器,数据发生变化时触发
scheduler() {
dirty = true
}
})
这里添加调度器后,第一次执行时,会将effectfn函数添加到backet中。在发生数据变化时,由于会遍历backet中的函数,进而可以调用scheduler函数,从而使得dirty变为true,使得变化后读取值的操作能够再次执行effectfn函数,达到数据响应的功能。
PS:这里我在变化Number数据类型时,输出的数据变为了NaN。发现原因出在track函数中,因为如果直接在全局中改变代理中的值,没有通过effect函数进行的话,全局变量activeEffect值为Null,返回的数据就为了undefine,这时再进行变化操作就会变为NaN存回原数据,代码如下(修改后):
get(target, proxy) {
let res = target[proxy]
if(!activeEffect) return res
可以发现最开始的时候,是判断如果没有activeEffect的话就直接return,这就是导致出现NaN的原因。因为直接修改不进过effect就没有activeEffect。修改后就可以完成修改操作。进而完成computed函数的功能。
但是这样的computed函数有问题。就是当在一个effect中读取computed属性时,这时我们修改代理属性中的值,会发现computed属性并没有更新,也就是没有数据没有同步。这是因为发生了effect嵌套,对于track函数来说,他里面访问的响应式数据只会把computed内部的effect收集,而如果嵌套了一个在外层的话,内部的effect就不会被内部的收集。
解决办法:当读取计算属性的值时,手动调用track函数;当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发。代码如下(这里没懂):
const effectfn = effect(getter, {
lazy: true,
// 新增调度器,数据发生变化时触发
scheduler(dirty) {
dirty = true
// 响应式数据发生变化时,手动触发trigger
trigger(obj, 'value')
}
})
const obj = {
// 当读取其值时,才执行副作用函数
get value() {
// 如果需要重新计算,则调用effectfn,然后赋值给value,否则直接将原数据返回
if(dirty) {
value = effectfn()
dirty = false
}
// 读取数据时,手动调用track进行跟踪
track(obj, 'value')
return value
}
}
这里的逻辑可能是这样的:因为computed是lazy的,只有我们读取其值或者亲自在computed属性上修改数据时,才能看到数据的变化,而在外部或者effect函数中读取或修改时,数据虽然发生了变化,但是在其他读取的地方并没有发生相应的变化。所以,我们在computed中加入了手动的调用操作,让其余地方的数据发生变化。
(应该是这样吧,但是我在测试的时候,堆栈溢出了…逆天,这里需要理解)