• 如何实现vue3.0的响应式呢?本文实战教你


    之前写了两篇vue2.0的响应式原理,链接在此,对响应式原理不清楚的请先看下面两篇

    和尤雨溪一起进阶vue

    和尤雨溪一起进阶vue(二)

    现在来写一个简单的3.0的版本吧

    大家都知道,2.0的响应式用的是Object.defineProperty,结合发布订阅模式实现的,3.0已经用Proxy改写了

    Proxy是es6提供的新语法,Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

    语法:

    const p = new Proxy(target, handler)

    target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
    handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

    handler的方法有很多, 感兴趣的可以移步到MDN,这里重点介绍下面几个

    handler.has()
    in 操作符的捕捉器。
    handler.get()
    属性读取操作的捕捉器。
    handler.set()
    属性设置操作的捕捉器。
    handler.deleteProperty()
    delete 操作符的捕捉器。
    handler.ownKeys()
    Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
    复制代码

    基于上面的知识,我们来拦截一个对象属性的取值,赋值和删除

    // version1
    const handler = {
        get(target, key, receiver) {
            console.log('get', key)
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            console.log('set', key, value)
            let res = Reflect.set(target, key, value, receiver)
            return res
        },
        deleteProperty(target, key) {
            console.log('deleteProperty', key)
            Reflect.deleteProperty(target, key)
        }
    }
    // 测试部分
    let obj = {
        name: 'hello',
        info: {
           age: 20 
        }
    }
    const proxy = new Proxy(obj, handler)
    // get name hello
    // hello
    console.log(proxy.name)
    // set name world
    proxy.name = 'world'
    // deleteProperty name
    delete proxy.name  我是08年出道的前端老鸟,想交流经验可以进我的扣扣裙 519293536 有问题我都会尽力帮大家
    

    上面已经可以拦截到对象属性的取值,赋值和删除了,我们来看看新增一个属性可否拦截

    proxy.height = 20
    // 打印 set height 20
    复制代码

    成功拦截!! 我们知道vue2.0新增data上不存在的属性是不可以响应的,需要手动调用$set的,这就是Proxy的优点之一

    现在来看看嵌套对象的拦截,我们修改info属性的age属性

    proxy.info.age = 30
    // 打印 get info
    复制代码

    只可以拦截到info,不可以拦截到info的age属性,所以我们要递归了,问题是在哪里递归呢?

    因为调用proxy.info.age会先触发proxy.info的拦截,所以我们可以在get中拦截,如果proxy.info是对象的话,对象需要再被代理一次,我们把代码封装一下,写成递归的形式

    function reactive(target) {
        return createReactiveObject(target)
    }
    function createReactiveObject(target) {
        // 递归结束条件
        if(!isObject(target)) return target
        const handler = {
            get(target, key, receiver) {
                console.log('get', key)
                let res = Reflect.get(target, key, receiver)
                // res如果是对象,那么需要继续代理
                return isObject(res) ? createReactiveObject(res): res
            },
            set(target, key, value, receiver) {
                console.log('set', key, value)
                let res = Reflect.set(target, key, value, receiver)
                return res
            },
            deleteProperty(target, key) {
                console.log('deleteProperty', key)
                Reflect.deleteProperty(target, key)
            }
        }
        return new Proxy(target, handler)
    }
    function isObject(obj) {
        return obj != null && typeof obj === 'object'
    }
    // 测试部分
    let obj = {
        name: 'hello',
        info: {
            age: 20
        }
    }
    const proxy = reactive(obj)
    proxy.info.age = 30
    复制代码

    运行上面的代码,打印结果

    get info
    set age 30
    复制代码

    Bingo! 嵌套对象拦截到了

    vue2.0用的是Object.defineProperty拦截对象的getter和setter,一次将对象递归到底, 3.0用Proxy,是惰性递归的,只有访问到某个属性,确定了值是对象,我们才继续代理下去这个属性值,因此性能更好

    现在我们来测试数组的方法,看看能否拦截到,以push方法为例, 测试部分代码如下

    let arr = [1, 2, 3]
    const proxy = reactive(arr)
    proxy.push(4)
    复制代码

    打印结果

    get push
    get length
    set 3 4
    set length 4
    复制代码

    和预期有点不太一样,调用数组的push方法,不仅拦截到了push, 还拦截到了length属性,set被调用了两次,在set中我们是要更新视图的,我们做了一次push操作,却触发了两次更新,显然是不合理的,所以我们这里需要修改我们的handler的set函数,区分一下是新增属性还是修改属性,只有这两种情况才需要更新视图

    set函数修改如下

    set(target, key, value, receiver) {
            console.log('set', key, value)
            let oldValue = target[key]
            let res = Reflect.set(target, key, value, receiver)
            let hadKey = target.hasOwnProperty(key)
            if(!hadKey) {
                // console.log('新增属性', key)
                // 更新视图
            }else if(oldValue !== value) {
                // console.log('修改属性', key)
                 // 更新视图
            }
            return res
        }
    复制代码

    至此,我们对象操作的拦截我们基本已经完成了,但是还有一个小问题, 我们来看看下面的操作

    let obj = {
        some: 'hell'
    }
    let proxy = reactive(obj)
    let proxy1 = reactive(obj)
    let proxy2 = reactive(obj)
    let proxy3 = reactive(obj)
    let p1 = reactive(proxy)
    let p2 = reactive(proxy)
    let p3 = reactive(proxy)
    复制代码

    我们这样写,就会一直调用reactive代理对象,所以我们需要构造两个hash表来存储代理结果,避免重复代理

    function reactive(target) {
       return createReactiveObject(target)
    }
    let toProxyMap = new WeakMap()
    let toRawMap = new WeakMap()
    function createReactiveObject(target) {
        let dep = new Dep()
        if(!isObject(target)) return target
        // reactive(obj)
        // reactive(obj)
        // reactive(obj)
        // target已经代理过了,直接返回,不需要再代理了
        if(toProxyMap.has(target)) return toProxyMap.get(target)
        // 防止代理对象再被代理
        // reactive(proxy)
        // reactive(proxy)
        // reactive(proxy)
        if(toRawMap.has(target)) return target
        const handler = {
            get(target, key, receiver) {
                let res = Reflect.get(target, key, receiver)
                // 递归代理
                return isObject(res) ? reactive(res) : res
            },
            // 必须要有返回值,否则数组的push等方法报错
            set(target, key, val, receiver) {
                let hadKey = hasOwn(target, key)
                let oldVal = target[key]
                let res = Reflect.set(target, key, val,receiver)
                if(!hadKey) {
                    // console.log('新增属性', key)
                } else if(oldVal !== val) {
                    // console.log('修改属性', key)
                }
                return res
            },
            deleteProperty(target, key) {
                Reflect.deleteProperty(target, key)
            }
        }
        let observed = new Proxy(target, handler)
        toProxyMap.set(target, observed)
        toRawMap.set(observed, target)
        return observed
    
    }
    function isObject(obj) {
        return obj != null && typeof obj === 'object'
    }
    function hasOwn(obj, key) {
        return obj.hasOwnProperty(key)
    }
    复制代码

    接下来就是修改数据,触发视图更新,也就是实现发布订阅,这一部分和2.0的实现部分一样,也是在get中收集依赖,在set中触发依赖

    完整代码如下

    class Dep {
        constructor() {
            this.subscribers = new Set(); // 保证依赖不重复添加
        }
        // 追加订阅者
        depend() {
            if(activeUpdate) { // activeUpdate注册为订阅者
                this.subscribers.add(activeUpdate)
            }
    
        }
        // 运行所有的订阅者更新方法
        notify() {
            this.subscribers.forEach(sub => {
                sub();
            })
        }
    }
    let activeUpdate
    function reactive(target) {
       return createReactiveObject(target)
    }
    let toProxyMap = new WeakMap()
    let toRawMap = new WeakMap()
    function createReactiveObject(target) {
        let dep = new Dep()
        if(!isObject(target)) return target
        // reactive(obj)
        // reactive(obj)
        // reactive(obj)
        // target已经代理过了,直接返回,不需要再代理了
        if(toProxyMap.has(target)) return toProxyMap.get(target)
        // 防止代理对象再被代理
        // reactive(proxy)
        // reactive(proxy)
        // reactive(proxy)
        if(toRawMap.has(target)) return target
        const handler = {
            get(target, key, receiver) {
                let res = Reflect.get(target, key, receiver)
                // 收集依赖
                if(activeUpdate) {
                    dep.depend()
                }
                // 递归代理
                return isObject(res) ? reactive(res) : res
            },
            // 必须要有返回值,否则数组的push等方法报错
            set(target, key, val, receiver) {
                let hadKey = hasOwn(target, key)
                let oldVal = target[key]
                let res = Reflect.set(target, key, val,receiver)
                if(!hadKey) {
                    // console.log('新增属性', key)
                    dep.notify()
                } else if(oldVal !== val) {
                    // console.log('修改属性', key)
                    dep.notify()
                }
                return res
            },
            deleteProperty(target, key) {
                Reflect.deleteProperty(target, key)
            }
        }
        let observed = new Proxy(target, handler)
        toProxyMap.set(target, observed)
        toRawMap.set(observed, target)
        return observed
    
    }
    function isObject(obj) {
        return obj != null && typeof obj === 'object'
    }
    function hasOwn(obj, key) {
        return obj.hasOwnProperty(key)
    }
    function autoRun(update) {
        function wrapperUpdate() {
            activeUpdate = wrapperUpdate
            update() // wrapperUpdate, 闭包
            activeUpdate = null;
        }
        wrapperUpdate();
    }
    let obj = {name: 'hello', arr: [1, 2,3]}
    let proxy = reactive(obj)
    // 响应式
    autoRun(() => {
        console.log(proxy.name)
    })
    我是08年出道的前端老鸟,想交流经验可以进我的扣扣裙 519293536 有问题我都会尽力帮大家
    proxy.name = 'xxx' // 修改proxy.name, 自动执行autoRun的回调函数,打印新值 复制代码

    最后总结下vue2.0和3.0响应式的实现的优缺点:

    • 性能 : 2.0用Object.defineProperty拦截对象的属性的修改,在getter中收集依赖,在setter中触发依赖更新,一次将对象递归到底拦截,性能较差, 3.0用Proxy拦截对象,惰性递归,性能好
    • Proxy可以拦截数组的方法,Object.defineProperty无法拦截数组的pushunshift,shiftpop,slice,splice等方法(2.0内部重写了这些方法,实现了拦截), proxy可以拦截拦截对象的新增属性,Object.defineProperty不可以(开发者需要手动调用$set)
    • 兼容性 : Object.defineProperty支持ie8+,Proxy的兼容性差,ie浏览器不支持
      本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理
  • 相关阅读:
    《大话设计模式》读书笔记
    设计模式个人笔记
    多线程的单元测试工具
    设计模式六大原则
    时间复杂度和空间复杂度(转)
    排序算法笔记
    《人月神话》读书笔记
    微信公众号开发踩坑记录(二)
    微信公众号开发踩坑记录
    全栈工程师之路
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/13096173.html
Copyright © 2020-2023  润新知