7.computed原理
利用前面的响应系统,就可以实现vue最有特色的computed,在这之前,还有一个知识就是关于懒执行的effec,即lazy的effect.什么是懒执行的effect
effect(()=>{
console.log(obj.foo)//这个函数会立即执行
})
在一些场景下,我们并不希望它立即执行,而是等到我们需要时再执行,例如计算属性.可以通过options中添加lazy属性达到目的
effect(()=>{//指定lazy选项,函数不会立即执行
console.log(obj.foo)
},
{//options
lazy:true
})
按照这lazy,修改effect函数的实现逻辑,当lazy为true时,不立即执行副作用函数
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
}
这样副作用函数就不会立即执行,我们返回了副作用函数,这样我们就可以控制副作用函数什么时候执行了
const effectFn=(()=>{
console.log(obj.foo)
},{lazy:true})
//手动执行副作用函数
effectFn()
我们还可以把传递给effect函数看作一个getter,这样这个getter函数可以返回任何值
const effectFn=effect(()=>obj.foo+obj.bar,{lazy:true})
我们手动执行副作用函数时,就可以拿到值
const effectFn=effect(()=>obj.foo+obj.bar,{lazy:true})
const value = effectFn()
我们这里还需要对effec函数做修改
function effect(fn,options={}){
const effectFn=()=>{
cleanup(effectFn)
activeEffect=effectFn
effectStack.push(effectFn)
const res=fn() //将fn的执行结果存进res中
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
return res
}
effectFn.options=options
effectFn.deps=[]
//只有非lazy时才执行
if(!options.lazy){
effectFn()
}
//将副作用函数返回
return effectFn
}
可以从代码看出,fn是真正的副作用函数,effectFn是包装过的副作用函数,通过调用fn返回的结果作为effectFn调用返回的结果,才能实现getter 接下来就是实现计算属性
function computed(getter){
//把getter作为副作用函数,创建一个lazy的effect
const effectFn=effect(getter,{lazy:true})
}
cosnt obj={
get value(){
//读取value时才执行effectFn
return effectFn()
}
}
retuen obj
这里我们定义了一个computed函数,将getter作为参数,把getter作为副作用函数,创建一个lazy的effect,computed函数返回一个对象,对象中value属性是一个访问器属性,只有读取value值,才会执行effectFn将其结果返回 利用computed函数创建一个计算属性
const data = {foo:1,bar:2}
const obj = new Proxy(data,{})
const sums = computed(()=>obj.foo+obj.bar)
console.log(sums.value) //3
这个计算属性还有缺陷,当我们多次访问,这个计算属性会进行多次计算,没有对值进行缓存,下面就是对computed增加缓存功能
function computed(getter){
//value用来缓存上一次的值
let value
//标记,为true时,则需要计算
let dirty
const effectFn = effect(getter,{
lazy:true
})
const obj={
get value(){
//当dirty为true时,需要重新计算,将新值存进value中
if(dirty){
value = effectFn()
//将dirty设为false,下一次直接访问value中的值
dirty=false
}
return value
}
}
return obj
}
这段代码问题很明显,dirty什么时候设置为true,先看这样写有什么问题
const data = {foo:1,bar:2}
const obj = new Proxy(data,{})
const sums = computed(()=>obj.foo+obj.bar)
console.log(sums.value) //3 正常输出
//修改obj.foo
obj.foo++
console.log(sums.value) //3 依然还是3
这是因为第一次访问sums.value时,dirty设置为false,代表不需要计算了,我们修改了obj.foo,里面的dirty依然为false,不执行副作用函数,也就不更新新值了,所以当obj.foo和obj.bar修改时,dirty要重置为true,前面的调度器scheduler就可以实现
function computed(getter){
//value用来缓存上一次的值
let value
//标记,为true时,则需要计算
let dirty
const effectFn = effect(getter,{
lazy:true,
scheduler(){
dirty = true
}
})
const obj={
get value(){
//当dirty为true时,需要重新计算,将新值存进value中
if(dirty){
value = effectFn()
//将dirty设为false,下一次直接访问value中的值
dirty=false
}
return value
}
}
return obj
}
我们添加了调度器,当getter所依赖数据发生变化时,执行调度函数,将dirty设为true,下一次访问sums.value就会调用effectFn计算. 现在的计算属性并不完美,当发生下面这个情况
const sumRes = computed(()=>obj.foo+obj.bar)
effect(()=>{
//在副作用函数中读取sumres.value
console.log(sumRes.value)
})
修改obj.foo的值
obj.foo++
sumRes是一个计算属性,正常逻辑来说,修改sumRes的值是会触发副作用函数执行,但是上面代码并不会因为numRes改变而执行,这时,我们需要在读取计算属性时,手动调用track函数追踪,进行依赖收集
function computed(getter){
//value用来缓存上一次的值
let value
//标记,为true时,则需要计算
let dirty
const effectFn = effect(getter,{
lazy:true,
scheduler(){
dirty = true
//依赖数据发生变化,调用trigger函数触发响应
triggrt(obj,'value')
}
})
const obj={
get value(){
//当dirty为true时,需要重新计算,将新值存进value中
if(dirty){
value = effectFn()
//将dirty设为false,下一次直接访问value中的值
dirty=false
}
//读取value时,调用track函数追踪
track(obj,'value')
return value
}
}
return obj
}
新增的两行代码其实就是为这个sumRes补一下get()和set()操作,和proxy代理对象原理一样