computed
作为计算属性其作用是描述响应式数据的复杂逻辑计算,当所依赖的响应式数据发生改变时计算属性会重新计算,更新逻辑计算的结果。
有个体现计算属性特点的便是比较计算属性和方法的区别,比如我们需要计算两个响应式数据的和
const obj = new reactive({foo: 1, bar: 2})
// 使用计算属性获取值
const value = computed(() => {obj.foo + obj.bar})
// 使用方法获取值
const value = () => obj.foo + obj.bar
其区别为computed有缓存机制,当内部依赖的响应式数据没有改变时则直接从缓存中获取结果,而方法则每次都需要执行。当依赖的响应式数据很多并且逻辑很复杂的时候那么计算属性效率会比方法高很多。
对于计算属性我们只有在需要值的时候才需要计算,而computed(getter)
以一个getter
作为参数,即每次计算属性依赖的响应式数据改变的时候都需要重新执行getter
,但是每次getter
的每一次执行却是不必要的,因为只有当获取计算属性的值时才运行getter
获取值,所以对于getter
的计算需要懒惰执行。
在上一章节中已经介绍了调度器的作用,所以我们只需要在调度器中加入一个lazy
的标记就行了
effect(() => {
return obj.foo + obj.bar
}, {
// 调度器中设置lazy标记
lazy: true
})
function effect(fn, options = {}) {
const effectFn = () => {
// 省略代码
}
effectFn.options = options
effectFn.deps = []
// 如过有缓存标记则直接返回
if(options.lazy)
return effectFn
effectFn()
}
function computed(getter) {
// 把getter作为一个副作用函数
const effectFn = effect(getter, {
lazy: true,
})
const obj = {
// 只有当读取到value时才会触发effectFn
get value() {
return effectFn()
}
}
return obj.value
}
这样只有我们在获取计算属性的value
时才会触发副作用函数执行,而不会在响应式数据改变时就直接执行。
假设我们在上述代码的设计基础下有以下代码
const sum = computed(() => obj.foo + obj.bar)
console.log(sum.value)
console.log(sum.value)
console.log(sum.value)
此时effectFn
会接连执行三次,但是每次的结果都是一样的,因为计算属性所依赖的obj.foo \obj.bar
的值并没有改变,所以我们可以直接将计算的结果缓存下来。
function computed(getter) {
// 用来缓存计算的结果
let value
// 用于标记是否需要重新计算值
let dirty = true
let effectFn = effect(getter, {
lazy: true
})
const obj = {
get value() {
// 如过需要重新计算值
if(dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
此时我们多次获取sum.value
的值则不会每次都重新计算了,但是这样写有一个非常明显的问题就是我们所依赖的响应式的值改变的时候不会重新计算,所以我们dirty
这个标记还需要和依赖的响应式数据联系起来。具体做法是将dirty
放入调度器中,这样每次响应式数据被改变的时候都会触发调度器改变dirty
的值
function computed(getter) {
// 用来缓存计算的结果
let value
// 用于标记是否需要重新计算值
let dirty = true
let effectFn = effect(getter, {
lazy: true,
// 当响应式数据修改时,触发副作用函数时修改dirty的值
scheduler: () => {
dirty = true
}
})
const obj = {
get value() {
// 如过需要重新计算值
if(dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
其实还有一个问题就是当计算属性放入effect
形成嵌套时,我们改变obj
的值并不会触发外层的effect
函数,即:
// 省略代码......
const sum = computed(() => obj.foo + obj.bar)
effect(() => {
console.log(sum.value)
})
当改变obj.foo/obj.bar
的值并不会输出sum.value
的值。这是不符合应用场景的,在应用中当我们的计算属性改变的时候页面也会重新渲染。
整个问题分析一下就是effect
嵌套的问题,当内层的响应式数据只和getter
形成了联系,和外层的副作用函数并没有关系,而内部的value
并不是响应式数据还是懒执行的,所以也就不会和外层的副作用函数产生联系了,这里解决的方案就是直接手动将计算属性的结果和外层副作用函数联系起来。
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true
// 计算属性依赖的响应式数据发生改变时,手动触发响应
trigger(obj, 'value')
}
})
const obj = {
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
// 读取value时手动追踪
track(obj, 'value')
return value
}
}
return obj
}