手动实现Vue的响应式原理,并详解Vue2和Vue3响应式实现的区别

本文通过逐步解析的方式,手动实现了Vue2和Vue3的响应式原理,从创建响应式函数、管理依赖到使用Object.defineProperty或Proxy进行数据劫持和依赖收集。在Vue2中,通过Object.defineProperty监听数据的set和get,而在Vue3中则使用Proxy进行更灵活的代理。文章最后总结了响应式系统的本质,即通过数据变化自动更新视图的关键过程。
摘要由CSDN通过智能技术生成

Vue的响应式原理

  • 面试的时候经常会问到,Vue的Vue2和Vue3的响应式原理有什么不同?
    • 那我今天就来自己手动实现一下Vue2和Vue3的响应式,然后再去看看它们两个有什么不同

Vue的响应式的目的是什么?我们自己怎样实现它?

  • Vue开发出来响应式系统想要做到什么样的事情

    • 首先我们要知道,在Vue中,我们写在template中的HTML元素,在Vue源码中不是直接被通过append方法添加进那个id为app的元素中的

    • 它是会先通过createVNode()这个函数,将我们编写在tempalte中的元素转换成VNode,再将VNode渲染到页面上的

    • 所以,如果我们在template中使用了某个变量的话,它底层做的事就是,当我们改变了那个变量的值时,它再调用一次createVNode函数,生成一个新的VNode,再通过diff算法比较,最后将其渲染到页面上

  • 所以,Vue响应式系统的目的就是:当变量发生改变时,自动再执行一次那些依赖这个变量的代码

那我们想要手动实现Vue响应式,就有以下几步:

第一步:当name发生变化的时候,再执行一次打印name的操作

<script>

    const name = "Judy"
    console.log(name)

    name = "kobe"
    console.log(name)

</script>

第二步:我们在第一步中,只是打印一下name,所以只有一行代码,可是如果我们依赖name的代码比较多呢?我们每次都一句一句调用就太麻烦了

  • 所以,我们可以把那些对变量有依赖的代码都放进一个函数里面,这样在变量发生改变的时候,只调用那个函数就好了

    <script>
    
        const obj = {
            name: "Judy",
            age: 18
        }
    
        function foo() {
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        }
    
        // 默认调用一次
        foo()
    
        obj.name = "kobe"
        // 当依赖的obj对象发生变化了,foo需要被重新执行一次
        console.log("-----------changed--------------")
        foo()
    
    </script>
    

第三步:但又有一个问题,在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?

  • 很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出响应

  • 但是下面的baz函数是一个完全独立于obj的函数,它在foo变化时,并不需要被重新执行

    function foo() {
        console.log("name:", obj.name)
        console.log("age:", obj.age)
    }
    
    function baz() {
        const result = 20 + 30
        console.log(result)
    }
    
  • 所以我们如何区分呢?

    • 我们可以再声明一个watchFn()
    • 凡是传入watchFn中的函数,都是需要响应式的
    <script>
    
        const reactiveFns = []
        // 第一步:声明watchFn,传入watchFn中的函数都是需要响应式的
        function watchFn(fn) {
            // 第三步:在watchFn中默认调用一次foo
            fn()
    
            // 第四步:将传入watchFn中的函数添加进一个全局的数组中,以便在obj发生变化的时候,对其进行调用
            reactiveFns.push(fn)
        }
    
    
        const obj = {
            name: "Judy",
            age: 18
        }
    
        // 第二步:因为foo需要响应式,所以把它传入watchFn中
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
    
        obj.name = "kobe"
        // 第五步:当依赖的obj发生变化了,就调用reactiveFns中的函数
        console.log("-----------changed--------------")
        reactiveFns.forEach(item => item())
    
    </script>
    

第四步:可是现在又有一个问题,在开发中我们不可能只有一个需要响应式的函数,如果还有其他依赖的不是obj对象,是依赖user对象,依赖info对象的函数呢?把它们全都添加进reactiveFns这个数组中的话,那么obj对象改变,也会执行依赖user和info对象的函数,这样肯定是不行的。

  • 所以我们可以创建一个类,对于不同的对象,让它们将依赖于自己的函数存在不同的reactiveFns中

  • 并且在类中写一个addReactiveFn方法,这样在把那些需要响应式的函数添加进reactiveFns时,直接调这个方法就好了,很方便

  • 并且还可以再写一个方法notify方法,这样在obj对象发生改变时,调用notify方法,通知一下dep对象即可,dep对象的notify方法就会自动调用那些已经添加进自己reactiveFns中的函数

    <script>
    
        // 第一步:创建一个类,对于不同的响应式对象就可以有不同的reactiveFns存储依赖于自己的函数了
        class depend {
            constructor() {
                this.reactiveFns = []
            }
    
            addReactiveFn(fn) {
                this.reactiveFns.push(fn)
            }
            
            notify() {
                this.reactiveFns.forEach(item => item())
            }
        }
    
        // 第二步:new一下depend类,拿到一个dep对象,将依赖于obj对象的函数添加进reactiveFns中
        const dep = new depend()
        function watchFn(fn) {
            fn()
            dep.addReactiveFn(fn)
        }
    
    
        const obj = {
            name: "Judy",
            age: 18
        }
    
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
    
        obj.name = "kobe"
        console.log("-----------changed--------------")
        // 第三步:现在调用需要响应式的函数时,就需要在dep的reactiveFns中拿这些函数了
        dep.notify()
    
    </script>
    

第五步:到这一步的时候,相信有小伙伴已经能想到了,在这里就可以使用Vue2的Object.defineProerty()方法Vue3的Proxy代理对象

  • 既然每次修改obj的name之后,都要调用一次notify方法,那么在监听到obj对象的name属性被设置的时候就调用这个方法即可

    <script>
    
        class depend {
            constructor() {
                this.reactiveFns = []
            }
    
            addReactiveFn(fn) {
                this.reactiveFns.push(fn)
            }
    
            notify() {
                this.reactiveFns.forEach(item => item())
            }
        }
    
        const dep = new depend()
        function watchFn(fn) {
            fn()
            dep.addReactiveFn(fn)
        }
    
    
        const obj = {
            name: "Judy",
            age: 18
        }
    
        // 第一步:使用Vue2的Object.defineProperty()监听obj属性的set
        Object.keys(obj).forEach(key => {
            let value = obj[key]
    
            Object.defineProperty(obj, key, {
                set: function(newValue) {
                    value = newValue
                    dep.notify()
                },
                get: function() {
                    return value
                }
            })
        })
    
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
    
        obj.name = "kobe"
    </script>
    

第六步:这一步就是整个Vue响应式原理最核心的一步了:如何收集依赖

  • 目前我们又遇到一个新的问题,那就是如果依赖obj的函数不止foo一个,还有一个bar函数,但是bar函数只依赖obj的age属性,并不依赖obj的name属性,那么这就意味着,当obj的name属性改变的时候,bar函数并不应该被调用

  • 可是现在我们实现的是,只要依赖obj对象,不管你依赖的是哪个属性,都放进一个reactiveFns中。那么这就意味着,目前无论obj对象的哪个属性被改变,依赖obj对象的所有函数都会被调用

  • 要解决这个问题有什么办法呢?

    • **办法就是:**为obj对象的每一个属性都创建一个dep对象
    • 我们刚才做的是,为obj对象创建一个dep对象,所以造成的问题就是,无论依赖obj对象的哪个属性,都会被放进一个reactiveFns中
    • 但是现在,我们为obj对象的每一个属性都创建一个dep对象的话,那么当某个属性发生变化的时候,去对应这个属性的dep中找reactiveFn就好了,现在找到的reactiveFns中存储的就只有依赖当前属性的函数了
  • 可是将它们如何存储起来呢?

    • 我们可以使用map存储它们,obj对象对应的就是一个map对象了,在它里面存储key为属性,value为dep对象的映射关系
  • 可是如果不止一个obj还有一个user对象呢?那obj和user又应该被怎样存储?

    • 我们可以再搞一个map对象,这个map对象存储key为obj/user,value为它们所对应的那个map对象的映射关系
  • 这样说比较抽象,所以我画图说明:

    Vue响应式如何收集依赖详解

  • 代码实现:和图片对应着看,会更容易理解

    • 在obj对象中的属性在第一次被使用的时候,我们就应该给每个属性创建好对应的dep对象
    • 所以我们就在get方法中收集依赖(当属性第一次被使用的时候,就会触发get操作,从而为每一个属性都创建一个对应的dep)
    <script>
    
        class depend {
            constructor() {
                this.reactiveFns = []
            }
    
            addReactiveFn(fn) {
                this.reactiveFns.push(fn)
            }
    
            notify() {
                this.reactiveFns.forEach(item => item())
            }
        }
    
        // 第四步:因为此时dep在watchFn中是拿不到的,所以需要把传入的这个函数保存起来,在能拿到dep的地方将这个函数保存在reactiveFns中,并且这个地方必须是在修改属性之前的。所以我们就可以在get方法中进行这一步操作
        let reactiveFn = null
        function watchFn(fn) {
            reactiveFn = fn
            fn()
            reactiveFn = null
        }
    
        // 第一步:实现给每个对象都添加一个dep的操作
        const objMap = new WeakMap()
        function getDepend(obj, key) {
            let map = objMap.get(obj)
            if (!map) {
                map = new Map()
                objMap.set(obj, map)
            }
    
            let dep = map.get(key)
            if (!dep) {
                dep = new depend()
                map.set(key, dep)
            }
    
            return dep
        }
    
    
        const obj = {
            name: "Judy",
            age: 18
        }
    
        // 使用Vue2的Object.defineProperty()监听obj属性的set、get
        Object.keys(obj).forEach(key => {
            let value = obj[key]
    
            Object.defineProperty(obj, key, {
                set: function(newValue) {
                    value = newValue
                    // 第三步:这个时候就没有全局的dep了,所以当obj的某个属性被重新设置时,需要拿到dep对象才能调用notify
                    const dep = getDepend(obj, key)
                    dep.notify()
                },
                get: function() {
                    // 第二步:收集依赖,如果有哪句代码使用到了obj对象的某个属性时,就为这个属性创建一个dep对象
                    const dep = getDepend(obj, key)
    
                    // 第五步:在这里可以拿到dep,并且可以将需要响应式的函数添加进reactiveFns中
                    dep.addReactiveFn(reactiveFn)
                    return value
                }
            })
        })
    
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
        watchFn(function bar() {
            console.log("age:", obj.age)
        })
    
    
        // obj.name = "kobe"
        obj.age = 20
    </script>
    

第七步:现在还有一个问题,那就是目前我们写的这个响应式系统是只针对于obj对象的

  • 所以我们就可以将Vue2的监听set、get操作的代码封装进一个函数中,然后哪个对象需要成为响应式的,就调用我这个函数,并且把对象传进来即可

    <script>
    
        class depend {
            constructor() {
                this.reactiveFns = []
            }
    
            addReactiveFn(fn) {
                this.reactiveFns.push(fn)
            }
    
            notify() {
                this.reactiveFns.forEach(item => item())
            }
        }
    
        let reactiveFn = null
        function watchFn(fn) {
            reactiveFn = fn
            fn()
            reactiveFn = null
        }
    
        const objMap = new WeakMap()
        function getDepend(obj, key) {
            let map = objMap.get(obj)
            if (!map) {
                map = new Map()
                objMap.set(obj, map)
            }
    
            let dep = map.get(key)
            if (!dep) {
                dep = new depend()
                map.set(key, dep)
            }
    
            return dep
        }
    
        // 第一步:将Vue2的defineProperty封装进一个函数中,并且将这个对象返回出来,不然其他地方没法用这个对象了
        function reactive(obj) {
            Object.keys(obj).forEach(key => {
                let value = obj[key]
    
                Object.defineProperty(obj, key, {
                    set: function(newValue) {
                        value = newValue
                        const dep = getDepend(obj, key)
                        dep.notify()
                    },
                    get: function() {
                        const dep = getDepend(obj, key)
    
                        dep.addReactiveFn(reactiveFn)
                        return value
                    }
                })
            })
    
            return obj
        }
    
        // ===================================业务代码======================================
        // 第二步:将这个对象传入reactive方法中,并且reactive方法会返回一个对象,供我们使用
        const obj = reactive({
            name: "Judy",
            age: 18
        })
    
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
        watchFn(function bar() {
            console.log("age:", obj.age)
        })
    
        obj.name = "kobe"
        // obj.age = 20
    </script>
    

第八步:这也是最后一步了,Vue2的响应式系统,目前为止基本就已经完成了。如果你能看到这里,就为自己点个赞吧!!!

  • 最后我们再做三个小小的优化:

    • 第一:如果foo函数中用到了两次key,比如name,那么这个函数就会被往reactiveFns中添加两次
      • 解决方法:reactiveFns不使用数组了,使用Set
      • set的特性之一就是里面不能保存重复的数据,它会自动去重
    • 第二:我们的objMap不应该使用Map,因为Map是强引用,这样的话,如果obj对象被设置为了null了,但是Map对象还引用着它,所以obj对象就不会被销毁
      • 解决方法:使用WeakMap即可,它对对象的引用是弱引用
    • 第三:我并不想把添加reactiveFn的操作放在get方法中,没有为什么,就是看着不舒服
      • 解决方法:在dep中重新写一个添加reactiveFn的实例方法,让它自己去全局里面找reactiveFn进行添加
  • 最终代码

    <script>
    
        class depend {
            constructor() {
                // this.reactiveFns = []
                // 第一步:使reactiveFns的值为Set,自动去重
                this.reactiveFns = new Set()
            }
    
            // addReactiveFn(fn) {
            //   this.reactiveFns.push(fn)
            // }
    
            notify() {
                this.reactiveFns.forEach(item => item())
            }
    
            // 第二步:重写addReactiveFn方法
            addReactiveFn() {
                if (reactiveFn) {
                    this.reactiveFns.add(reactiveFn)
                }
            }
        }
    
        let reactiveFn = null
        function watchFn(fn) {
            reactiveFn = fn
            fn()
            reactiveFn = null
        }
    
        // 第三步:这步在前面我已经下意识完成了,使用WeakMap做弱引用
        const objMap = new WeakMap()
        function getDepend(obj, key) {
            let map = objMap.get(obj)
            if (!map) {
                map = new Map()
                objMap.set(obj, map)
            }
    
            let dep = map.get(key)
            if (!dep) {
                dep = new depend()
                map.set(key, dep)
            }
    
            return dep
        }
    
        // 将Vue2的defineProperty封装进一个函数中,并且将这个对象返回出来,不然其他地方没法用这个对象了
        function reactive(obj) {
            Object.keys(obj).forEach(key => {
                let value = obj[key]
    
                Object.defineProperty(obj, key, {
                    set: function(newValue) {
                        value = newValue
                        const dep = getDepend(obj, key)
                        dep.notify()
                    },
                    get: function() {
                        const dep = getDepend(obj, key)
    
                        // dep.addReactiveFn(reactiveFn)
                        dep.addReactiveFn()
                        return value
                    }
                })
            })
    
            return obj
        }
    
    
        // ==================业务代码======================================
        const obj = reactive({
            name: "Judy",
            age: 18
        })
    
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
        watchFn(function bar() {
            console.log("age:", obj.age)
        })
    
        obj.name = "kobe"
        // obj.age = 20
    </script>
    

Vue3响应式原理

  • 想要把代码改成Vue3的响应式原理,现在就很简单了

  • 只需要将reactive方法中的Object.defineProperty()改成Proxy代理对象即可

    <script>
    
        class depend {
            constructor() {
                this.reactiveFns = new Set()
            }
    
            notify() {
                this.reactiveFns.forEach(item => item())
            }
    
            addReactiveFn() {
                if (reactiveFn) {
                    this.reactiveFns.add(reactiveFn)
                }
            }
        }
    
        let reactiveFn = null
        function watchFn(fn) {
            reactiveFn = fn
            fn()
            reactiveFn = null
        }
    
        const objMap = new WeakMap()
        function getDepend(obj, key) {
            let map = objMap.get(obj)
            if (!map) {
                map = new Map()
                objMap.set(obj, map)
            }
    
            let dep = map.get(key)
            if (!dep) {
                dep = new depend()
                map.set(key, dep)
            }
    
            return dep
        }
    
        // 使用Vue2的defineProperty
        // function reactive(obj) {
        //   Object.keys(obj).forEach(key => {
        //     let value = obj[key]
    
        //     Object.defineProperty(obj, key, {
        //       set: function(newValue) {
        //         value = newValue
        //         const dep = getDepend(obj, key)
        //         dep.notify()
        //       },
        //       get: function() {
        //         const dep = getDepend(obj, key)
    
        //         dep.addReactiveFn()
        //         return value
        //       }
        //     })
        //   })
    
        //   return obj
        // }
    
        // 使用Vue3的new Proxy()
        function reactive(obj) {
    
            const objProxy = new Proxy(obj, {
                set: function(target, key, newValue, receiver) {
                    Reflect.set(target, key, newValue, receiver)
                    const dep = getDepend(target, key)
                    dep.notify()
                },  
    
                get: function(target, key, receiver) {
                    const dep = getDepend(target, key)
                    dep.addReactiveFn()
                    return Reflect.get(target, key, receiver)
                }
            })
    
            return objProxy
        }
    
    
        // ==================业务代码======================================
        const obj = reactive({
            name: "Judy",
            age: 18
        })
    
        watchFn(function foo() {
            console.log("name:", obj.name)
            console.log("name:", obj.name)
            console.log("age:", obj.age)
        })
    
        watchFn(function bar() {
            console.log("age:", obj.age)
        })
    
        obj.name = "kobe"
        // obj.age = 20
    </script>
    

总结

  • 其实Vue的响应式原理就是通过 ES5 的 Object.defindeProperty 或者ES6的Proxy代理对象中的set和get方法,进行数据的劫持
  • 在数据第一次被使用的时候,就会自动调用get方法,并且通过get方法收集每个数据的依赖,看看都是哪些代码在依赖这个数据,然后将这些代码保存起来
  • 最后当数据被修改的时候,就会自动调用set方法,在set方法中再通过某种方式调用在get方法中已经收集好的那些依赖这个数据的代码,并且将它们再执行一遍,生成新的VNode
  • 最后通过diff算法比对新旧VNode,将修改过的部分记录下来,最后将其渲染到页面上
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值