0%

computed原理

effect是用来注册副作用函数,同时它允许指定一些选项参数options,例如指定scheduler调度器控制副作用函数的执行时机和方式,先实现一个懒执行的副作用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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//新增
}

实现了让副作用函数不立即执行的功能,将副作用函数effectFn作为effect函数的返回值,这就意味着当调用effect函数时,通过其返回值能够拿到对应的副作用函数,这样我们能够手动执行副作用函数

1
2
3
4
5
const effectFn = effect(()=>{
console.log(obj.foo)
},{lazy:true})
//手动执行副作用函数
effectFn()

如果把传递给effect的函数看做一个getter,那么这个getter函数可以返回任何值

1
2
3
4
5
const effectFn = effect(
//getter返回obj.foo和obj.bar
() => obj.foo + obj.bar,
{ lazy: true}
)

为了手动执行副作用函数时就能拿到其返回值,改动effect函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function effect(fn,options = {}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
//将fn执行结果存储到res
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
//将res作为effectFn返回值
return res;
}
effectFn.options = options
effectFn.deps = []
if(!options.lazy){
effectFn()
}
return effectFn
}

传递给effect函数的参数fn才是真正的副作用函数,而effectFn是我们包装后的副作用函数,为了通过effectFn得到真正的副作用函数fn的执行结果,我们需要将其保存到res变量,然后将其作为effectFn函数的返回值

接下来就可以定义cmputed函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function computed(getter) {
//把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter,{
lazy: true
})
const obj = {
//当读取value时才执行effectFn
get value() {
return effectFn()
}
}
return obj
}

computed函数接收一个getter函数作为参数,我们把getter函数作为副作用函数,用它创建一个lazy的effect,computed函数的执行会返回一个对象,该对象的value属性是一个访问器属性,只有当读取value值时,才执行effectFn并将结果作为返回值返回

使用computed函数创建一个计算属性:

1
2
3
4
const data ={ foo: 1, bar: 2}
const obj = new Proxy(data, {...})
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)//3

当多次访问sumRes.value的值,会导致effectFn进行多次计算,即使obj.foo和obj,bar的值本身没有变化,利用闭包实现对值进行缓存的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function computed(getter) {
//value用来缓存上一次计算的值
let value
//dirty标志,用来标识是否需要重新计算值,为true意味着“脏”没需要计算
let dirty = true
const effectFn = effect(getter,{
lazy:true
})
const obj = {
get value(){
//只有脏时才计算值,并将得到的值缓存到value
if(dirty){
value = effectFn()
//将dirty设置为false,下一次访问直接使用缓存到value中的值
dirty = false
}
return value
}
}
return obj
}

但是此时改变obj.foo或者obj.bar,再访问sumRes.value会发现访问到的值没有发生变化

1
2
3
4
5
6
7
8
const data = {foo: 1,bar: 2}
const obj = new Proxy(data,{...})
const sumRes = computed(() => obj.foo+obj.bar)
console.log(sumRes.value)//3
console.log(sumRes.value)//3
obj.foo++;
//再次访问得到的仍然是3
console.log(sumRes.value)//3

这是因为,第一次访问sumRes.value的值后,变量dirty被设置为false,代表不需要计算,即使我们修改obj.foo的值,但只要dirty的值为false,就不会重新计算,所有导致我们得到错误的值,因此当obj.foo或者obj.bar的值发生改变时,只要dirty的值重置为true就可以,这时就用到scheduler选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function computed(getter) {
let value
let dirty = true
cont effectFn = effect(getter, {
lazy: true,
scheduler() {
if(!dirty) {
dirty = true
//当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应
trigger(obj,'value')
}
}
})
const obj = {
get value() {
if(dirty){
value = effectFn()
dirty = false
}
//当读取value手动调用track函数进行追踪
track(obj,'value')
return value
}
}
return obj
}

当读取一个计算属性value,需要手动调用track函数,把计算属性返回的对象obj作为target,同时作为第一个参数传递给track函数,当计算属性所依赖的响应式数据变化时会立即调度函数,在调度函数内手动调用trigger函数触发响应即可。