zoukankan      html  css  js  c++  java
  • 如何实现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浏览器不支持
      本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理
  • 相关阅读:
    ECharts之柱状图 饼状图 折线图
    Vue自定义指令(directive)
    HDU 1231 最大连续子序列
    POJ 2533 Longest Ordered Subsequence
    HDU 1163 Eddy's digital Roots
    HDU 2317 Nasty Hacks
    HDU 2571 命运
    HDU 4224 Enumeration?
    HDU 1257 最少拦截系统
    HDU 2740 Root of the Problem
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/13096173.html
Copyright © 2011-2022 走看看