响应式数据系统 三
调度执行 与options 参数
可调度性是响应式数据系统的重要特性。
当trigger触发副作用函数时,我们希望用户有能力控制副作用函数执行的行为,以满足更多的需要。
我们为effect函数设置一个配置参数options,允许用户指定调度器。
effect(
//第一个参数为注册的副作用函数
() => {
consoel.log(data.foo)
},
//第二个参数时是配置对象,允许用户设置调度器 scheduler
{
scheduler(fn){
//...
}
}
)
在effect函数内部,我们只需要把options参数挂载到对应的副作用函数上。
function effect(fn,options){ //添加options形参
function effectFunction() {
cleanup(effectFunction)
activeEffect = effectFunction
effectStack.push(effectFunction)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFunction.depsList = []
effectFunction.options = options //将options挂载到effectFunction上
effectFunction()
}
在trigerr函数中,我们把控制权交给options里的scheduler函数。
function trigger(target, key){
let depsMap = bucket.get(target)
if(!depsMap) return
let deps = depsMap.get(key)
if(deps.size === 0) return
const depsToRun = new Set(deps)
console.log(depsToRun)
depsToRun.forEach(fn =>{
if(fn === effectFunction) return
//如果effectFunction的options有scheduler函数,则调用这个函数;否则直接执行副作用函数。
if(effectFunction.options.scheduler){
effectFunction.options.scheduler(fn)
}else{
fn()
}
});
}
有个这些基础,我们就可以尝试一些新的玩法了。考虑下面的例子:
effect(() => {
console.log(data.value)
})
data.value++
data.value++
不出所料,代码执行的结果应该是先输出data.value的值,接着又连续输出两次data.value改变后的值。
我们希望,在响应式数据连续变化的过程中,只执行最后的副作用函数,而忽略变化过程中“过渡的”副作用函数。
因为浏览器的渲染只会下一个消息循环中执行,在一个消息任务中多余的副作用函数的执行是不必要的,我们只需要变化最后的结果一次触发响应式数据就好。
//定义一个消息队列
const jobQueue = new Set();//Set 的特性是自动去重,因此相同的任务不会被重复添加。
const p = Promise.resolve();//一个已经 resolve 的 Promise,用于将任务调度到微任务队列中执行。
let isFlushing = false;
function flushJob() {
if(isFlushing === true) return // 如果正在刷新任务队列,直接返回
isFlushing = true // 标记为正在刷新任务队列
p.then(()=>{
jobQueue.forEach(job => job()) // 执行所有任务
}).finally(() => {
isFlushing = false // 重置状态
})
}
effect(
() => {
console.log(data.value);
},
{
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
}
)
data.value++
data.value++
调度器把一个代码周期中的所有副作用函数都收集到jobQueue中,flushJob函数设置了一个标志位,保证在一个代码周期内无论调用多少次flushJob,只会执行一次。p开启了一个微队列,在下一个消息循环微队列开始执行,批量处理jobQueue中的副作用函数,并且没有函数重复。
这个功能有点类似于Vue.js中连续修改响应式数据但只会触发一次更新的功能,实际上Vue内部实现了一个更为完善的调度器,思路与上文相同。
计算属性computed 与 配置属性lazy
我们希望在注册副作用函数时不会上来就执行一次。
可以在options中添加lazy属性实现。
effect(
() => {
console.log(data.value);
},
// options
{
lazy: true
}
)
function effect(fn,options){
function effectFunction() {
cleanup(effectFunction)
activeEffect = effectFunction
effectStack.push(effectFunction)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFunction.depsList = []
effectFunction.options = options
//如果lasy为true,则不立即执行
if(!options.lasy){
effectFunction()
}
//返回封装的副作用函数,供用户主动调用
return effectFunction
}
如果仅仅能够手动执行副作用函数,意义并不大。
如果我们传递给effect的函数看作一个getter,这getter可以返回任何值。
const effectFunction = effect(
() => obj.foo + obj.bar
// options
{ lazy: true }
)
// value是getter的返回值
const value = effectFunction()
function effect(fn,options){
function effectFunction() {
cleanup(effectFunction)
activeEffect = effectFunction
effectStack.push(effectFunction)
const res = fn() // fn才是真正的副作用函数,也就是getter的返回值。
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res //调用返回的effectFunction得到
}
effectFunction.depsList = []
effectFunction.options = options
//如果lasy为true,则不立即执行
if(!options.lasy){
effectFunction()
}
//返回封装的副作用函数,供用户主动调用
return effectFunction
}
基于以上能够可以懒执行的副作用函数,我们可以构建计算属性computed函数了
function computed(getter) {
const effectFunction = effect(getter,{lasy:true})
// 立即返回一个对象,这个对象的value属性是一个访问器属性。
// 有调用obj.value的值,才会执行effectFunction并返回结果。
const obj = {
get value() {
return effectFunction()
}
}
return obj
}
const data = { foo:1, bar:2}
const obj = reactive(data)
const sumRes = computed(
()=> obj.foo + obj.bar
)
console.log(sumRes.value)
当当当,comuted属性就完成了。