vue3计算属性原理

143 阅读3分钟

vue3计算属性原理和vue2思想也大致相同,几个特点:缓存,懒加载,当我们修改其依赖项时重新获取计算属性的值,并且如果页面中有使用到计算属性,页面上也要渲染成最新的值。针对以上特点我们来实现下,由于大部分代码在上一篇响应式原理中已经介绍过,计算属性只是在创建副作用函数时添加了一个新的参数options,其实对应到vue2源码来理解,以下的createEeffct和vue2中的Watcher类似 然后bucket和vue2中的Dep类似。

如果在这之前对vue2的原理有了解的话,以下代码阅读起来更容易。vue2计算属性原理

完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        const data = {
            name: '啊锋',
            msg: '写代码'
        }

        const obj = new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })

        const bucket = new WeakMap()

        function track(target, key) {
            if (!activeEffect) return
            let targetMap = bucket.get(target)
            if (!targetMap) {
                bucket.set(target, (targetMap = new Map()))
            }
            let deps = targetMap.get(key)
            if (!deps) {
                targetMap.set(key, (deps = new Set()))
            }
            deps.add(activeEffect)
            activeEffect.deps.push(deps)
        }

        function trigger(target, key) {
            const targetMap = bucket.get(target)
            if (!targetMap) return
            const deps = targetMap.get(key)
            let cloneDeps = new Set()
            deps.forEach(effect => {
                if (effect !== activeEffect) {
                    cloneDeps.add(effect)
                }
            })
            cloneDeps && cloneDeps.forEach(effect => {
                if (effect.options.scheduler) {
                    effect.options.scheduler(effect)
                } else {
                    effect()
                }
            })
        }

        let activeEffect
        let effectStack = []

        function createEffect(fn, options = {}) {
            const effectFn = () => {
                cleanup(effectFn)
                activeEffect = effectFn
                effectStack.push(effectFn)
                const res = fn()
                effectStack.pop()
                activeEffect = effectStack[effectStack.length - 1]
                return res
            }
            effectFn.deps = []
            effectFn.options = options
            if (!options.lazy) {
                effectFn()
            }
            return effectFn
        }

        function cleanup(effectFn) {
            effectFn.deps.forEach(deps => {
                deps.delete(effectFn)
            })
            effectFn.deps.length = 0
        }

        function computed(fn) {
            let value
            let dirty = true

            const getter = createEffect(fn, {
                lazy: true,
                scheduler() {
                    if (!dirty) {
                        dirty = true
                        trigger(computedValue, "value")
                    }
                }
            })

            const computedValue = {
                get value() {
                    if (dirty) {
                        value = getter()
                        dirty = false
                    }
                    track(computedValue, "value")
                    return value
                }
            }
            return computedValue
        }

        const sumRes = computed(() => obj.name + obj.msg)

        console.log(sumRes.value) // 啊锋写代码
        console.log(sumRes.value) // 啊锋写代码

        
        createEffect(() => {
            document.body.innerText = sumRes.value
        })

        setTimeout(() => {
            obj.msg = "写博客"  // 3秒后页面由 "啊锋写代码" 变成 "啊锋写博客"
        },3000)
    </script>
</body>

</html>

代码解读

function computed(fn) {
     let value
     let dirty = true

     const getter = createEffect(fn, {
            lazy: true,
            scheduler() {
                    if (!dirty) {
                        dirty = true
                        trigger(computedValue, "value")
                    }
                }
            })

     const computedValue = {
            get value() {
                if (dirty) {
                   value = getter()
                   dirty = false
                }
                track(computedValue, "value")
                return value
            }
     }
     return computedValue
}

这就是用来创建计算属性的函数,可以看到内部初始化了两个变量value,dirty = true,value就是用来保存计算属性的结果的,然后dirty的作用就是一个阀门,结合下面的代码可以看出来只有当dirty=true时,才会去调用getter函数也就是重新去获取值,并且在获取到新值后,立马将dirty = false,那么下次再访问计算属性时,直接将老的value返回,而不用去重新计算,但是如果计算属性中依赖的属性有变化的话,则需要重新获取计算属性的值,也就是需要将dirty=true。

然后去使用createEffect创建一个副作用函数,并传入了第二个参数,参数里边有个lazy:true,scheduler函数,在createEffect内部可以看到如果options.lazy = true的话,直接将函数返回,也就实现了懒加载效果,不会立马去执行他,然后computed函数最后返回了一个computedValue,其作用就是在使用计算属性是,会去调用computedValue的value函数,该value函数内部做了几件事:

1>. 根据dirty的状态来判断是否需要重新获取新值.

2>. track(computedValue, "value")这个的作用,当计算属性在其他的副作用函数里面使用时,也就是副作用函数,那么需要将此副作用函数和计算属性建立响应式联系.可以看到上面完整代码里边的

    createEffect(() => { document.body.innerText = sumRes.value })

该代码创建了一个副作用函数,并且在副作用函数内部使用了计算属性,那么在后期如果计算属性的依赖发生了改变,计算属性也会重新计算,那么该副作用函数我们期望的也要重新执行。所以track函数的作用就是将这个与计算属性有关联的副作用函数存储起来。后期去重新执行他。

现在我们就按照当我们使用这个计算属性时他的执行过程是什么样的:

1>. console.log(sumRes.value)会去触发computedValue的value函数,次数dirty是true,所以需要去获取值value = getter() 2>. 调用getter函数也就是去调用createEffect函数返回的effectFn,然后回去调用初始化计算属性时传入的函数,并在此期间会,收集依赖,将当前的activeEffect = effectFn ,收集到name,msg的副作用集合中,最后返回其结果。

最后当修改了计算属性中依赖的属性时,obj.msg = "写博客",会触发set函数,从而去执行集合中的副作用函数,但此时集合中的副作用函数是有effect.options.scheduler,所以去调用effect.options.scheduler,从而将计算属性中阀门dirty重新变为true,并且去触发track(computedValue, "value")阶段收集到的副作用函数,使其相关副作用函数中的计算属性更新到最新值。也就是createEffect(() => { document.body.innerText = sumRes.value }),所以在3秒后页面由 "啊锋写代码" 变成 "啊锋写博客"。