计算属性computed
本篇为阅读《Vue.js设计与实现》第4章过程总结笔记,感受Vue.js非常重要且有特色的能力~计算属性
首先我们来回顾一下上节响应系统作用与实现中的
effect
函数:用来注册副作用函数,同时允许指定一些选项参数options,例如scheduler调度器来控制副作用函数的执行时机和方式track
函数:用来追踪和收集依赖trigger
函数:用来触发副作用函数重新执行
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn;
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.options = options // 将options挂载到effectFn上班
effectFn.deps = []
effectFn()
}
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect) // deps就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps) // 将其添加到activeEffect.deps数组中
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set();
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn) // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
} else {
effectFn() // 否则直接执行副作用函数(之前的默认行为)
}
})
}
computed 与 lazy实现
- 我们之前所实现的effect函数会立即执行传递给它的副作用函数,例如:
effect(
() => { // 这个函数会立即执行
console.log(obj.foo)
}
)
- 但在有些场景下,不希望它立即执行,希望它在需要的时候执行,例如计算属性 这时我们可以在options中添加
lazy
属性来达到目的
effect(
() => { // 指定了lazy选项,这个函数不会立即执行
console.log(obj.foo)
},
// options
{
lazy: true
}
)
- lazy 属性和之前介绍的 scheduler 一样,通过options选项对象指定 此时我们可以修改effect函数的实现逻辑,当options.lazy为true时,不立即执行副作用函数
- 但是要什么时候执行副作用函数呢,从我们修改的effect函数可以看出,我们调用effect得到了effectFn函数,需要我们手动调用,才会执行副作用函数
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 = []
if (!options.lazy) {
effectFn() // 只有lazy为false时,才执行
}
return effectFn // 将副作用函数作为返回值返回
}
const effectFn = effect(() => {
console.log(obj.foo)
}, {
lazy: true
})
effectFn() // 手动执行副作用函数
- 但是仅仅只能通过手动执行副作用函数,意义不大,但是我们把传递给effect的函数看作一个
getter
,让这个getter可以返回任何值,然后在手动执行副作用时,就能够拿到其返回值
需要在effect再做一些修改
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 // 将res作为effectFn的返回值
}
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
const effectFn = effect(() => {
return obj.foo + obj.bar // getter 返回obj.foo和obj.bar的和
}, {
lazy: true
})
// 也可以这样子写
const effectFn = effect(
() => obj.foo + obj.bar,
{
lazy: true
})
const value = effectFn() // value是getter的返回值
console.log(value);
- 传递给effect函数的参数fn才是真正的副作用函数,而effectFn是我们包装后的副作用函数,为了通过effectFn得到真正的副作用函数fn的执行结果,需要将其保存到res变量中,将其作为effectFn函数的返回值
- 接下来可以实现计算属性,如下:
function computed(getter) {
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() {
return effectFn()
}
}
return obj
}
// 我们使用computed函数来创建一个计算属性
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value); // 3
console.log(sumRes.value); // 3
console.log(sumRes.value); // 3 每次访问都会调用effectFn重新计算
-
我们定义了一个computed函数,它接收一个
getter
函数作为参数,把getter函数作为副作用函数,用它创建一个lazy的effect -
computed函数的执行会返回一个对象,该对象的
value
是一个访问器属性,只有当读取value的值(xxx.value)时,才会执行effectFn并将其结果作为返回值返回 -
以上我们实现的计算属性值只做到了懒计算,换句话说,只有当你真正读取sumRes.value的值时,它才会进行计算并得到值
- 但是,还做不到对值进行缓存,即假如我们多次读取sumRes.value,会导致effectFn进行多次计算,即使foo和bar的值没有发生改变
- 我们可以添加对值进行缓存的功能
function computed(getter) {
let value // value 用来缓存上一次计算的值
let dirty = true // dirty标志,用来表示是否需要重新计算值
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() {
if (dirty) { // 只有当dirty为true时才计算值 并将得到的值缓存到value中
value = effectFn()
dirty = false // 将标志置为false 下一次访问直接使用缓存到value中的值
}
return value
}
}
return obj
}
const sumRes = computed(() => {
console.log('查看effectFn执行了几次'); // 只打印一次 说明我们的缓存是可以实现我们想要的功能的
return obj.foo + obj.bar
})
console.log(sumRes.value);
console.log(sumRes.value); // 第2次访问 直接读取缓存的value值
-
但是我们发现还有问题所在,如果我们修改foo或者bar的值,再次访问 sumRes.value 会发现访问到的值没有发生变化
- 原因就是第一次访问sumRes.value后,变量dirty会设置为false,当我们修改foo的值,只要dirty的值为false,就不会重新计算
- 控制台打印结果如图:
console.log(sumRes.value);
console.log(sumRes.value);
obj.foo = 2
console.log(sumRes.value);
- 解决方法,当我们foo的值发生变化时,将dirty的值置为true即可,应该怎么做?
- 这时就需要我们上一节介绍的scheduler选项,调整的computed函数如下
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter,
{
lazy: true,
scheduler() {
dirty = true // 添加调度器 将dirty置为true
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
const sumRes = computed(() => {
console.log('查看effectFn执行了几次');
return obj.foo + obj.bar
})
console.log(sumRes.value);
console.log(sumRes.value);
obj.foo = 2
console.log(sumRes.value);
-
添加scheduler调度器函数
- 它会在getter函数中所依赖的响应式数据变化时执行
- 这样,我们在scheduler函数内将dirty重置为true,下次访问时,就会重新调用effectFn计算值
- 以上实现的computed函数还有缺陷
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
console.log(sumRes.value); // 在该副作用函数中读取值
})
obj.foo++ // 修改foo的值
-
当我们在另外一个effect中读取计算属性的值,修改foo的值,希望副作用函数重新执行,就像我们在Vue.js的模板中读取计算属性值的时候,一旦计算属性发生变化就会触发重新渲染一样
-
但是我们发现并不会触发副作用函数的渲染
-
分析原因:
- 本质上看是一个典型的effect嵌套,一个计算属性内部拥有自己的effect,并且时懒执行的,只有当真正读取计算属性的值时才会执行
- 对于计算属性的getter函数来说,它里面访问的响应式数据只会把computed内部的effect收集为依赖
- 而当把计算属性用于另外一个effect时,就会发生effect嵌套,外层的effect不会被内层中effect中的响应式数据收集
-
解决方法
- 当读取计算属性时,手动调用track函数进行追踪
- 当计算属性依赖的响应式数据发生变化时,可以手动调用trigger函数触发响应
function computed(getter) {
let value
let dirty = true
const effectFn = effect(getter,
{
lazy: true,
scheduler() {
if (!dirty) {
dirty = true
trigger(obj, 'value') // 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value') // 当读取value时,手动调用track函数进行追踪
return value
}
}
return obj
}
- 当读取一个计算属性的value值时,我们手动调用track函数,把计算属性返回的对象obj作为target,并作为第一个参数传给track函数
- 当计算属性所以依赖的响应式数据变化时,会执行调度函数,在调度函数内手动调用trigger函数触发响应
好抽象好抽象,我有点绕晕了sos
自我总结
-
基于上一节响应式数据设计与实现,本节一点点实现计算属性,发现缺陷,并在effect函数和computed函数中逐步完善计算属性的实现
-
这样子我们通过
computed
函数可以创建一个计算属性,依赖一个响应式的变量,而且只有当我们读取了计算属性的值,才会触发effect函数,懒执行的过程 -
当响应式数据的值发生变化时,计算属性的值会自动更新
<script setup lang="ts"> let count = ref(0) let doubleCount = computed(() => count.value * 2) const countUp = () => { count.value++ } </script>
转载自:https://juejin.cn/post/7305213173896265754