likes
comments
collection
share

计算属性computed

作者站长头像
站长
· 阅读数 34

本篇为阅读《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实现

  1. 我们之前所实现的effect函数会立即执行传递给它的副作用函数,例如:
effect(
    () => {   // 这个函数会立即执行
        console.log(obj.foo)  
    }
)
  1. 但在有些场景下,不希望它立即执行,希望它在需要的时候执行,例如计算属性 这时我们可以在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()    // 手动执行副作用函数
  1. 但是仅仅只能通过手动执行副作用函数,意义不大,但是我们把传递给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函数的返回值
  1. 接下来可以实现计算属性,如下:
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,就不会重新计算
    • 控制台打印结果如图:

计算属性computed

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

  1. 以上实现的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函数触发响应

计算属性computed

好抽象好抽象,我有点绕晕了sos

自我总结

  1. 基于上一节响应式数据设计与实现,本节一点点实现计算属性,发现缺陷,并在effect函数和computed函数中逐步完善计算属性的实现

  2. 这样子我们通过computed函数可以创建一个计算属性,依赖一个响应式的变量,而且只有当我们读取了计算属性的值,才会触发effect函数,懒执行的过程

  3. 当响应式数据的值发生变化时,计算属性的值会自动更新

        <script setup lang="ts">
        let count = ref(0)
        let doubleCount = computed(() => count.value * 2)
        const countUp = () => {
          count.value++
        }
        </script>
    
转载自:https://juejin.cn/post/7305213173896265754
评论
请登录