Vue源码解读 深入解读Proxy vue3响应式原理

124 阅读19分钟

Vue源码解读 深入解读Proxy vue3响应式原理

本文中的代码已经同步至GitHub,大家可自行阅读实验

github.com/Feweb-wind/…

1.1前言(副作用函数)

我们都知道,vue3中的响应式是通过proxy的get和set劫持对象属性变化来完成的。 在get阶段,会收集代码中所有与代理对象有依赖的函数到一个set容器中,see4ee54rt阶段会遍历执行所有的函数,我们称这些函数为副作用函数

而vue就是在副作用函数中完成对视图更新的操作的

(如果还不了解proxy,可以下去自行了解)

伪代码:

const obj = new Proxy(data, {
            get(target, key) {
                //收集副作用函数到容器中
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                //执行容器中的副作用函数
                return true
            }
        })

副作用函数be like:

function effect() {
            document.querySelector('.myContainer').innerHTML = data.text
}

我们可以先把副作用函数抽象的理解成修改视图层的函数,例如你在vue模板中使用了一个变量,那么vue就会把这个代理对象的副作用函数收集到一个桶中,等到这个对象的属性变化的时候,副作用函数就会执行,视图层被修改,始终和我们的最新的数据统一。

示例:

<!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>
    <div class="myContainer"></div>
    <button>click to change div</button>

    <script>
        const bucket = new Set()
        const data = { text: 'hello world' }
        const obj = new Proxy(data, {
            get(target, key) {
                bucket.add(effect)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                bucket.forEach(fn => fn())
                return true
            }
        })
        function effect() {
            document.querySelector('.myContainer').innerHTML = data.text
        }
        effect()
    </script>
    <script>
        document.querySelector('button').onclick = function () {
            obj.text += 'h'
        }
    </script>
</body>

</html>

如上图代码所示,当我们点击按钮时,修改代理对象的值,页面也随之更新

1.2完善我们的响应式系统

从上文中我们可以总结出一个响应式系统的工作流程

  • 当读取操作发生时,将副作用函数收集到桶中
  • 当设置操作发生时,从桶中取出副作用函数并执行

但是我们可以发现一些问题,上文中我们设计的响应式系统比较简单,我们把副作用函数的名字写死了(effect),如果副作用函数的名字不叫effect,那么代码就不能够正常工作,为了解决这个问题我们需要提供一个用来注册副作用函数的机制

代码如下:

//用全局变量存储被注册的副作用函数
        let activeEffect
        //effect用来注册副作用函数
        function effect(fn) {
            //调用时将fn赋值给activeEffect
            activeEffect = fn
            //执行副作用函数
            fn()
        }

我们可以这样使用effect函数

effect(
            //匿名的副作用函数
            () => {
                document.querySelector('.myContainer').innerHTML = obj.text
            }
        )

然后把proxy get里添加的effect改成我们全局副作用函数注册变量

old: buckect.add(effect)

new: buckect.add(activeEffect)

这样我们就解决了副作用函数硬编码的问题,也就是不把副作用函数的名字写死,只要使用effect进行注册,那么proxy就可以拿到副作用函数

1.2.1 增加不存在的属性所出现的问题

如果我们对这个响应式系统再进行测试,例如为响应式数据上设置一个不存在的属性

effect(
            //匿名的副作用函数
            () => {
                console.log('effect run')//会打印两次
                document.querySelector('.myContainer').innerHTML = obj.text
            }
        )
        setTimeout(() => {
            obj.newPro = 'hello vue3'
        }, 1000);

如上代码所示,我们一秒钟后为对象添加一个新属性newpro, 理论上,我们副作用函数中只绑定了text一个属性,所以我们预期是打印内容只执行一次,可是实际结果却执行了两次

为什么会这样呢?因为我们没有在副作用函数与被操作字段之间建立一个明确的联系,导致在proxy 的 get中,只要读取任意属性,都会把副作用函数收集到桶中,set中,无论什么属性变化,也都会把桶中的副作用函数全部拿出来执行。因此为了解决这个问题,我们需要重新设计桶的数据结构,不能简单的使用set数据结构来作为桶了。

那么如何设计新的桶的数据结构呢?

我们先仔细观察下面的代码

        effect(
            function effectFn {
                document.querySelector('.myContainer').innerHTML = obj.text
            }
        )

在这段代码中存在三个角色:

  • 被读取属性的代理对象obj
  • 被读取的字段名text
  • 使用effect函数注册的副作用函数effectFn

如果使用target来表示一个代理对象的原始对象,用key来表示被读取的字段名,用effectFn来表示被注册的副作用函数,那么我们可以为这三个角色简历如下关系

4CA47AD8F25477A75B60159290016F0F.png

可以看到这是一个树形结构

如果有两个副作用函数同时读取同一个对象的属性值:

3.png

那么关系如下

2.png

如果有一个副作用函数同时读取了一个对象的两个属性 Snipaste_2023-03-23_21-12-14.png

那么关系如下:

4.png

如果在不同的副作用函数中读取了两个对象的不同属性

6.png

那么关系如下:

7.png

总之,这就是一个树形结构,有了这个关系,我们就可以解决上文中的问题,依照这个关系,如果我们设置obj2.text2的值,就只会导致effectFn2函数重新执行,并不会导致effectFn1函数重新执行。

修改后的代码如下图所示: 大家仔细看注释就好了,注释写的很清楚

        //修改桶的数据结构 为weakmap  //如果有对weakmap还不熟悉的小伙伴自己下去了解
        const bucket = new WeakMap()

        const data = { text: 'hello world' }
        const obj = new Proxy(data, {
            //对get和set进行修改
            get(target, key) {
                //如果没有activeEffect 直接return
                if (!activeEffect) return target[key]
                // 根据target从桶中取得depsMap,它是一个map类型:key----> effects
                let depsMap = bucket.get(target)
                //如果不存在depsMap,那么新建一个map与target关联
                if (!depsMap) {
                    bucket.set(target, (depsMap = new Map()))
                }
                //再根据key 从depsMap中取得deps,它是一个set类型
                //里面存储着所有与当前key相关联的副作用函数,effects
                let deps = depsMap.get(key)
                // 如果key不存在,同样新建立一个set并与key关联
                if (!deps) {
                    depsMap.set(key, (deps = new Set()))
                }
                //最后将当前激活的副作用函数添加到桶里
                deps.add(activeEffect)
                //返回属性值
                return target[key]
            },
            set(target, key, newVal) {
                //设置属性值
                target[key] = newVal
                //根据target从桶中取得depsMap,它是key---->effects
                const depsMap = bucket.get(target)
                if (!depsMap) return
                //根据key取得的所有副作用函数effects
                const effects = depsMap.get(key)
                //执行副作用函数
                effects && effects.forEach(fn => fn())
            }
        })

从这段代码我们可以看出来,我们分别使用了WeakMap,Map,Set 其中:

  • WeakMap 由 target --> Map 构成
  • Map 由 key --> Set 构成

他们之间的关系如下图所示:

9.png

经过我们的完善,现在只有对代理对象中与副作用函数有依赖的属性操作时,副作用函数才会执行。

最后我们对get 和 set 中的代码做一些封装处理

  const obj = new Proxy(data, {
            //对get和set进行修改
            get(target, key) {
                //将副作用函数添加到桶中
                track(target, key)
                //返回属性值
                return target[key]
            },
            set(target, key, newVal) {
                //设置属性值
                target[key] = newVal
                //将副作用函数从桶中拿出来执行
                trigger(target, key)
            }
        })
        //将副作用函数添加到桶中
        function track(target, key) {
            if (!activeEffect) return target[key]
            // 根据target从桶中取得depsMap,它是一个map类型:key----> effects
            let depsMap = bucket.get(target)
            //如果不存在depsMap,那么新建一个map与target关联
            if (!depsMap) {
                bucket.set(target, (depsMap = new Map()))
            }
            //再根据key 从depsMap中取得deps,它是一个set类型
            //里面存储着所有与当前key相关联的副作用函数,effects
            let deps = depsMap.get(key)
            // 如果key不存在,同样新建立一个set并与key关联
            if (!deps) {
                depsMap.set(key, (deps = new Set()))
            }
            //最后将当前激活的副作用函数添加到桶里
            deps.add(activeEffect)
        }
        //将副作用函数从桶中拿出来执行
        function trigger(target, key) {
            //根据target从桶中取得depsMap,它是key---->effects
            const depsMap = bucket.get(target)
            if (!depsMap) return
            //根据key取得的所有副作用函数effects
            const effects = depsMap.get(key)
            //执行副作用函数
            effects && effects.forEach(fn => fn())
        }

好啦,现在我们的响应式系统就已经有模有样啦

1.3分支切换与cleanup

1.3.1什么是分支切换

如下代码所示:

effect(function effectFn(){
    documnet.querySelector('.myContainer').innerHTML = obj.ok?obj.text:'not'
})

在副作用函数中,根据字段obj.ok字段的不同,会执行不同的代码分支,这就是分支切换。

分支切换有可能产生遗留的副作用函数,拿上面的代码举例,若obj.ok的初始值是true,那么副作用函数首先会和ok和text两个属性建立联系。

当obj.ok变为false时,代表obj.text这一代码分支不会执行,讲道理这个时候副作用函数应该与obj.text断开联系,也就是说obj.ok为false时,obj.text值的变化不应该导致副作用函数的重新执行。

但是我们的程序目前还做不到这一点。

下面尝试来解决这个问题。

解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。

也就是说,每次副作用函数执行,先把该副作用函数从所有包含它的set集合(key--->Map)中删除掉,然后副作用函数的执行会重新收集依赖,这样就不会出现遗留的副作用函数依然留在“桶”中。

要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确的知道哪些依赖集合中包含它,因此我们需要重新设计副作用函数,在副作用函数内部定义新的effectFn函数,并为其添加effectFn.dep属性,该属性是一个数组,用来存储所有包含当前副作用函数的依赖集合。

重新设计后的副作用函数:

        function effect(fn) {
            const effectFn = () => {
                //当effetFn执行时,将其设置为当前激活的副作用函数
                activeEffect = effectFn
                fn()
            }
            //deps用来存储所有与该副作用函数相关联的依赖
            effectFn.deps = []
            //执行副作用函数
            effectFn()
        }

下一步我们要修改track函数,对effectFn.deps数组中的依赖集合进行收集。

修改后的track函数,其实只多了一行代码:

        function track(target, key) {
            if (!activeEffect) return target[key]
            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)
            //新增
            activeEffect.deps.push(deps)
        }

下一步实现cleanup函数,它的作用是把副作用函数从依赖集合中删除 实现的思路其实很简单,就是遍历effectFn.dep数组,把副作用函数从依赖集合中删除

        function cleanup(effectFn) {
            for (let i = 0; i < effectFn.deps.length; i++) {
                const deps = effectFn.deps[i]
                //从依赖集合中删除副作用函数
                deps.delete(effectFn)
            }
            //最后重置dep数组
            effectFn.deps.length = 0
        }

最后再一次修改effect函数,调用cleanup函数完成清除工作

         function effect(fn) {
            const effectFn = () => {
                //当effetFn执行时,将其设置为当前激活的副作用函数
                cleanup(effectFn)
                activeEffect = effectFn
                fn()
            }
            //deps用来存储所有与该副作用函数相关联的依赖
            effectFn.deps = []
            //执行副作用函数
            effectFn()
        }

但是此时我们会发现目前的代码会导致副作用函数的无限执行

问题出在trigger函数的这行代码上:

effects && effects.forEach(fn => fn())

这行代码遍历effect集合,它是一个set数据结构,里面存储着副作用函数,当副作用函数执行时,会调用cleanup进行清楚,但是副作用函数的执行会导致其重新被收集到集合中。

造成这个现象的原因是:js语言规范中说明,当通过forEach遍历set集合时,如果一个值被访问过但是又被重新添加到集合中,如果此时遍历还没有结束,那么该值会被重新访问。

const set = new Set([1])
set.forEach(item =>{
    set.delete(1)
    set.add(1)
    console.log('遍历中')
})

上面这段代码就会无限次的执行。

解决方法也很简单,我们可以构造另外一个set集合并且遍历它

修改后的trigger函数如下:

        function trigger(target, key) {
            const depsMap = bucket.get(target)
            if (!depsMap) return
            const effects = depsMap.get(key)

            const effectsToRun = new Set(effects) //新增
            effectsToRun.forEach(effectFn => effectFn()) //新增
            // effects && effects.forEach(fn => fn())
        }

这样,我们新构建了一个effectsToRun集合并遍历他,代替直接遍历effects集合,从而避免了无限执行。

1.4当副作用函数发生嵌套

1.4.1前言

我们经常在vue中使用render渲染函数

实际上render就是在一个effect中执行的

//Foo组件
const Foo = {
    render(){
        return ....
    }
}

上面的这段代码等同于:

effect(()=>{
    Foo.render()
})

那么我们可以想象这样一种场景:当我们的组件发生嵌套时,例如Foo组件渲染了Bar组件

// Bar 组件

const Bar = {

    render() { /* ... */ },

}

// Foo 组件渲染了 Bar 组件

const Foo = {

    render() {

        return <Bar /> // jsx 语法

    }

}

那么这个时候就会发生effec嵌套,它相当于:


effect(() => {

    Foo.render()

    // 嵌套

    effect(() => {

        Bar.render()

    })

})

因此我们的响应式系统要设计成可嵌套的,显然,我们目前的响应式系统并不支持这个功能。

1.4.2继续完善我们的响应式系统

我们用以下的数据测试一下当发生嵌套会发生什么:

//原始数据
        const data = { foo: true, bar: true }
        const obj = new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
        let temp1, temp2
        effect(function fn1() {
            console.log('fn1 执行')
            effect(function fn2() {
                console.log('fn2 执行')
                temp2 = obj.bar
            })
            temp1 = obj.foo
        })

我们对这段代码的预期是,foo与副作用函数fn1建立联系,bar与副作用函数fn2建立联系,当foo变化时会执行fn1,fn2(为什么会执行fn2?因为fn2在fn1函数的内部,所以fn1执行时会顺带执行fn2),bar变化时会执行fn2

但是实际情况却是foo和bar的变化都只会导致fn2的执行

这是为什么呢?其实就出在我们实现的effect函数与activeEffect上。

        let activeEffect
        function effect(fn) {
            const effectFn = () => {
                cleanup(effectFn)
                activeEffect = effectFn
                fn()
            }
            effectFn.deps = []
            effectFn()
        }

观察上面的代码,我们发现activeEffect是我们用来全局注册存储effect函数的变量,它当然只能存储一个值。再结合我们上面的代码来看,当副作用函数发生嵌套时,还没有等foo的get完成函数依赖注册时,activeEffect的值就已经被覆盖成fn2了,因此foo和bar的值改变都只会导致fn2的执行。

为了解决这个问题,我们需要一个副作用函数栈effectStack,在副作用函数执行时,我们将当前副作用函数压入栈中,等到副作用函数执行完后将其从栈中弹出,并让activeEffect始终指向栈顶的副作用函数。这样就能解决上面的问题了。

      let activeEffect
        //effect栈
        const effectStack = []
        function effect(fn) {
            const effectFn = () => {
                cleanup(effectFn)
                activeEffect = effectFn
                //入栈
                effectStack.push(effectFn)
                fn()
                //出栈
                effectStack.pop()
                //指向栈顶
                activeEffect = effectStack[effectStack.length - 1]
            }
            effectFn.deps = []
            effectFn()
        }

这样的话,响应式数据就只会收集直接读取其值的副作用函数作为依赖,从而避免发生错乱

image.png

可以看到现在程序执行的结果已经符合我们的预期了

1.5避免无限递归循环

目前我们的响应式系统已经很完善了,但是依然有一些小问题:

我们在副作用函数中对代理对象的属性进行自增运算

        const data = { foo: 1 }
        const obj = new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
        let temp1, temp2
        effect(function fn1() {
            obj.foo++
        })

image.png

可以发现我们的浏览器报错了:大概意思就是说递归太多导致了栈溢出

为什么会出现这种情况呢?

obj.foo++ 可以等价为 obj.foo = obj.foo + 1

我们可以尝试分析一下代码的流程:首先读取foo的值,这个操作会被proxy的get捕获到,执行track函数,紧接着对obj.foo的赋值又会被proxy的set捕获到,执行trigger函数,而在trigger函数中会执行副作用函数,也就是会调用当前的函数(自己调用自己,递归),问题是当前副作用函数还没有执行完毕,这样会无限的调用自己,于是就导致了栈溢出。

解决这个问题不难,我们可以在trigger函数中增加一个判断条件,如果trigger触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。


      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 => effectFn())
        }

这样,我们就解决了无限递归的问题

1.6调度执行

1.6.1什么是可调度性

可调度性是响应式系统中非常重要的特性,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机,次数,和方式。

假如现在我们有如下代码:

image.png

输出结果是:1 2 结束了 如果用户需求有变,用户期望输出结果变成:1 结束了 2,那这个时候我们要怎么做呢?

1.6.2实现可调度性

我们可以为effect函数设计一个选项参数options,允许用户指定调度器:

        effect(
            function fn1() {
                console.log(obj.foo)
            },
            //options
            {
            //调度器scheduler是一个函数
                scheduler(fn) {

                }
            }
        )

如上所示,用户在调用effect函数注册副作用函数时,可以传递第二个参数options,它是一个对象,其中允许指定scheduler调度函数,同时在effect函数内部,我们需要把options选项挂载到对应的副作用函数上:

        function effect(fn,options) {
            const effectFn = () => {
                cleanup(effectFn)
                activeEffect = effectFn
                effectStack.push(effectFn)
                fn()
                effectStack.pop()
                activeEffect = effectStack[effectStack.length - 1]
            }
            //新增,将options挂载到effectFn
            effectFn.options = options
            effectFn.deps = []
            effectFn()
        }

有了调度函数,我们在trigger函数中触发副作用函数执行时,就可以直接调用用户传递的调度器函数,从而把控制权交给用户

        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()
                }
            })
        }

这样我们就实现了一个调度器,把副作用函数执行的控制权交给用户,实现了上面的需求,输出1 结束了 2

    const data = { foo: 1 }
        const obj = new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
        effect(
            function fn1() {
                console.log(obj.foo)
            },
            //options
            {
                //调度器scheduler是一个函数
                scheduler(fn) {
                    setTimeout(fn)
                }
            }
        )
        obj.foo++
        console.log('结束了')

image.png

除了控制副作用函数的执行顺序,通过调度器我们还可以做到控制它的执行次数,这一点也尤其重要,我们思考下面的例子:

        effect(
            function fn1() {
                console.log(obj.foo)
            }
        )
        obj.foo++
        obj.foo++

在副作用函数中打印obj.foo的值,然后对其进行两次自增操作,在没有指定调度器的情况下,输出结果为:1,2,3

如果我们只关心结果,不关心过程,那么中心打印2就是多余的,因为这只是一个过渡状态,我们期望打印的结果是:1,3

基于调度器我们很容易就可以实现这个功能

// 定义一个任务队列
        const jobQueue = new Set()
        //使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
        const p = Promise.resolve()
        // 一个标志代表是否正在刷新队列
        let isFlushing = false
        function flushJob() {
            //如果队列正在刷新,则什么也不做
            if (isFlushing) return
            isFlushing = true
            p.then(() => {
                jobQueue.forEach(job => job())

            }).finally(() => {
                isFlushing = false
            })
        }
        
        effect(
            function fn1() {
                console.log(obj.foo)
            },
            {
                scheduler(fn) {
                    //每次调度时,把副作用函数添加到jobQueue队列中
                    jobQueue.add(fn)
                    //调用flushJob刷新队列
                    flushJob()
                }
            }
        )
        obj.foo++
        obj.foo++

这个功能有点类似于vue.js中连续修改多次响应式数据,但是只会触发一次更新,实际上vue.js内部实现了一个更加完善的调度器,思路和上文介绍的相同。

1.7计算属性的原理

大的要来啦!前面我们实现了副作用函数,和选项参数options,track函数,trigger函数等等,利用这些,我们就可以实现vue.js中一个非常重要并且十分有特色的能力,计算属性。

1.7.1懒加载的effect

在深入了解计算属性之前,我们先来了解一下懒加载的effect,即lazy的effect,什么意思呢?举个例子,现在我们所实现的effect函数会立即执行传递给它的副作用函数

effect(
//这个副作用函数会立刻执行
    ()=>{
        console.log(obj.foo)
    }
)

但是在有些场景下,我们不希望它立刻执行,而是希望它在需要的时候才执行,如计算属性,这个时候我们就可以通过在options中添加lazy属性来达到目的

effect(
//指定了lazy属性,函数不会立刻执行
    ()=>{
        console.log(obj.foo)
    },
    //options
    {
        lazy:true
    }
)

lazy选项和我们之前介绍的scheduler调度器一样,通过options选项指定,有了它我们就可以修改effect函数的逻辑了,当options.lazy为true时,不立即执行副作用函数:

        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 = []
            //新增
            //只有options.lazy不为true的时候才执行
            if (!options.lazy) {
                effectFn()
            }
            //把副作用函数作为返回值返回
            return effectFn
        }

在代码中我们可以看到,我们根据lazy的值来判断是否立刻执行副作用函数,并且把副作用函数作为返回值返回,这就意味着我们调用effect函数的时候,通过返回值就可以拿到对应的副作用函数,这样我们就可以手动执行该副作用函数 了

const effectFn = effect(()=>{
    console.log(obj.foo)
},{lazy:true}
)

//手动执行副作用函数
effectFn()

如果仅仅能手动执行副作用函数,那意义其实不大,但是如果我们把传递给effect的函数看作一个getter,那么这个getter函数可以返回任何值

const effectFn = effect(
    ()=> obj.foo + obj.bar,
    {lazy:true}
)

//这样我们手动执行的时候,就可以拿到其返回值

const value = effectFn()

为了达到这个效果,我们要给我们的effect函数做一些修改,把里面的effectFn函数添加返回值

        const effectFn = () => {
                cleanup(effectFn)
                activeEffect = effectFn
                effectStack.push(effectFn)
                //新增
                const res = fn()
                effectStack.pop()
                activeEffect = effectStack[effectStack.length - 1]
                //新增
                return res
            }

        //计算属性
        function computed(getter) {
            const effectFn = effect(getter, {
                lazy: true
            })
            const obj = {
                get value() {
                    return effectFn()
                }
            }
            return obj
        }

现在我们的计算属性就基本完成了,我们测试一下,会发现,getter中的数据发生变化时,手动调用计算属性.value会返回更新的值

    const data1 = { foo: 1 }
        const obj1 = new Proxy(data1, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
        const data2 = { bar: 1 }
        const obj2 = new Proxy(data2, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
        const result = computed(() => obj1.foo + obj2.bar, { lazy: true })

但是,现在还存在一个问题,我们发现,如果我们为计算属性的value与副作用函数进行绑定时,如果getter中的值变了,并不能导致副作用函数执行(obj1.foo的值变化,或者obj2.bar的值变化)

        effect(
            () => {
                document.querySelector('.myContainer').innerHTML = result.value
            }
        )

这是因为我们在计算属性中并没有执行track和trigger的代码,导致副作用函数不能和result.value关联起来

将track和trigger函数的代码添加上,我们发现,上述问题就顺利解决了

          function computed(getter) {
            const effectFn = effect(getter, {
                lazy: true,
                scheduler() {
                    console.log(2)
                    trigger(obj, 'value')
                }
            })
            const obj = {
                get value() {
                    console.log(1)
                    track(obj, 'value')
                    return effectFn()
                }
            }
            return obj
        }