本文只针对vue3的设计与实现进行展开。后续称
vue
computed的实现
调度执行
在讲解计算属性computed之前,我们需要先了解一下调度执行的概念,其实很简单,就是有能力决定副作用函数的执行时机,次数,方式。
比如下面代码:
const data = {obj: 1}
const proxy = new Proxy(data,{...}) // 响应式拦截,当proxy.obj改变的时候,触发effect函数
effect(()=> {
console.log(proxy.obj)
})
proxy.obj++
console.log(‘end’)
// 打印结果
// 1
// 2
// end
如果我们希望打印的顺序调整为1、end、2,这个时候,就需要进行调度,这个时候就需要针对effect函数增加新的选项参数options,如下:
effect(()=> {
console.log('log')
}, {
scheduler(fn) {
...
}
})
options是一个对象,其中允许指定的scheduler调度函数,同时我们需要在effect函数内部,把options选项挂载到对应的副作用函数
effect(fn, options={}) {
activeEffect = fn
activeEffect.options = options
fn()
}
有了调度函数,我们就可以把控制权交给用户。
....
set(target,key,value) {
target[key] = value
let despMap = bucket.get(target)
if(!despMap) return
let effects = despMap.get(key)
effects && effects.forEach(fn => {
if(fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
return true
}
有了上面的实现后,我们就可以实现最初的需求了
const data = {obj: 1}
const proxy = new Proxy(data,{...})
effect(()=> {
console.log(proxy.obj)
}, {
scheduler(fn) => {
setTimeOut(fn)
}
})
proxy.obj++
console.log(‘end’)
// 打印结果
// 1
// end
// 2
以上我们通过调度器,实现了对副作用函数的顺序的控制,此外,我们可以进一步考虑如何实现对执行次数的控制。
const data = {obj: 1}
const proxy = new Proxy(data,{...})
effect(()=> {
console.log(proxy.obj)
})
proxy.obj++
proxy.obj++
// 打印结果
// 1
// 2
// 3
如果我们希望打印的结果是两次,只有1、3呢,去掉中间2的打印,改如何实现?只关注结果。
const data = {obj: 1}
const proxy = new Proxy(data,{...})
effect(()=> {
console.log(proxy.obj)
})
// 定义一个任务队列, set数据结构有去重能力。
const jobQueue = new Set();
// 使用Promise.resolve()创建一个promise实例,用他把任务加到微任务里面
const p = Promise.resolve()
// 一个标志,代表是否正在刷新队列
let isFlushing = false
function flushJob() {
// 如果队列正在刷新,则什么都不做。
if(isFlushing){
return
}
isFlushing = true
p.then(() =>{
jobQueue.forEach(job => {
job()
});
}).finally(() => {
isFlushing = false
})
}
effect(() => {
console.log(proxy.obj);
}, {
scheduler(fn) {
jobQueue.add(fn)
flushJob()
}
})
proxy.obj++
proxy.obj++
当proxy.obj++执行两次的时候,就会执行两次调度函数,也就是jobQueue.add(fn)会执行两次,但是由于set的去重能力,jobQueue只含有一项,然后调用flushJob函数,flushJob函数也会连续执行两次,但是因为有isFlushing的存在,第二次执行的时候,isFlushing为true。所以就直接返回了。
有因为微任务的执行时间,是在proxy.obj是3以后了,所以就满足我们最初的期望了。
Lazy
我们目前的给副作用函数传递的函数,都会立即执行
effect(
// 这个函数会立即执行
()=> {
console.log('123')
}
)
如果我们希望是在需要他的时候,才执行,比如计算属性,这个时候,我们可以在options增加lazy属性达到目的。
effect(
// 这个函数会立即执行
()=> {
console.log('123')
},
{
lazy: true
}
)
通过lazy属性的传递,当为ture的时候,则不立即执行副作用函数,参考下面代码:
function effect(fn, options={}){
const effectFn = fn
effectFn.options = options
if(!effectFn.options.lazy) {
effectFn() // lazy:false的时候,立即执行函数
}
return effectFn // 将需要执行的函数返回
}
const effectFn = effect(() => {
console.log('123')
}, {
lazy: true
})
// 手动的执行副作用函数
effectFn()
此时我们能够实现手动执行副作用函数了,但是为了实现computed的效果,我们需要把传递effect的函数看作一个getter。那么这个getter就可以返回任何值。
const effectFn = effect(
// getter返回obj.bar+obj.foo
() => {
return obj.bar+obj.foo
}, {
lazy: true
})
此时对effect函数再次做修改
function effect(fn, options={}){
const effectFn = () => {
const res = fn() // 将res作为effectFn函数的返回值
return res
}
effectFn.options = options
if(!effectFn.options.lazy) {
effectFn() // lazy:false的时候,立即执行函数
}
return effectFn // 将需要执行的函数返回
}
const effectFn = effect(() => {
return obj.bar+obj.foo
}, {
lazy: true
})
const value = effectFn() // 就可以拿到副作用函数执行结果的目标值了。
computed
有了上面的基础,我们就可以去实现计算属性了,如下所示:
function computed(getter) {
const effectFn = effect(getter, {lazy: true})
const obj = {
get value() {
return effectFn()
}
}
return obj
}
我们定义一个computed函数,他接受一个getter函数作为参数,用它创建一个lazy的effect, computed函数返回一个对象,当访问对象的时候,就会执行effectFn,并把执行结果返回。
const data = {foo: 1, bar: 2}
const proxy = new Proxy(data, {...})
const sum = computed(()=> proxy.foo + proxy.bar)
console.log(sum.value) // 3
但此时我们去访问sum.value的时候,函数都会重新执行,但是结果都是3,所以可以做一下缓存优化
function computed(getter) {
let value
let dirty = true // 通过dirty来控制返回结果是否需要重新计算。
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true // 调度器函数,当依赖的属性改变的时候,就会执行调度器scheduler
}
})
const obj = {
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
以上就是一个较为完善computed了。