vue3 响应式原理==proxy

目录

什么是响应式

 手动完成响应过程

自定义设置过程

1) Proxy代理对象

2) 最基本的reactive函数

最基本的响应式

definePrototype vs Proxy

什么是Object.defineProperty

defineProperty和Proxy的区别

1.Proxy不用遍历每一个属性?

2.Proxy不修改原对象?是在对象上面加一层代理?

为什么我又说Proxy不修改原对象也是不准确的。

为什么说Proxy可以监听数组的访问


什么是响应式

当数据改变时, 引用数据的函数会自动重新执行

 手动完成响应过程

首先, 明确一个概念: 响应式是一个过程, 这个过程存在两个参与者: 一方触发, 另一方响应

比如说, 我们家小胖有时候不乖, 我会打他, 他会哭. 这里我就是触发者, 小胖就是响应者

同样, 所谓数据响应式的两个参与者

  • ●触发者: 数据
  • ●响应者: 引用数据的函数

当数据改变时, 引用数据的函数响应数据的改变, 重新执行

我们先手动完成响应过程

<body>
    <div id="app"></div>
</body>
<script>
    // 定义一个全局变量:触发者
    const obj = { name: '张三' }

    // 定义一个函数响引用obj变量,响应者
    function effect() {
        app.innerHTML = obj.name
    }
    effect()

    setTimeout(() => {
        obj.name = "lisa"
        //手动执行
        effect()
    }, 2000)

</script>

为了方便, 我们把引用了数据的函数 叫做 副作用函数 

如果一个函数引用了外部的资源, 这个函数会受到外部资源改变的影响
我们就说这个函数存在副作用. 因此, 也把该函数叫做副作用函数
这里, 大家不要被这个陌生的名字吓唬住
所谓副作用函数就是引用了数据的函数或者说数据关联的函数

自定义设置过程

如果我们能感知数据改变, 拦截到赋值操作. 自定义设置过程

在赋值的同时调用一下数据关联的副作用函数, 就可以实现自动重新执行

理论上可行, 开始动手实践

1) Proxy代理对象

这里我们需要先补充一下Proxy相关的知识. 如果已经知道的小伙伴可以略过

Proxy是ES6推出的一个类,给对象架设一层拦截器,但凡要访问或者修改这个对象上的值或者属性,都必须先经过这层拦截器, Proxy也叫代理器, 它代理了对对象的操作。

new Proxy: 传入一个源对象, 返回一个新对象(代理对象)

当访问代理对象的属性时, 可以自定义访问过程

当设置代理对象的属性时, 可以自定义设置过程

    <script>
        // 定义一个源对象(目标对象)
        const obj = { name: '张三' }
        const proxy = new Proxy(obj, {
            get: (target, key) => {
                // 访问proxy代理对象属性的时候会执行(get) [target]:为目标对象
                // 将 get函数的返回值作为表达式的值 
                return target[key]
            },
            set: (target, key, value) => {
                // 设置proxy代理对象属性的时候会执行(set)
                target[key] = value
                return true
            }
        })
        console.log(proxy);
        console.log(proxy.name);
        console.log(proxy.age);
        proxy.name = 'zs'
        console.log(obj);


    </script>

这样就确定了思路

1先创建代理对象

2再操作代理对象(给代理对象赋值)


2) 最基本的reactive函数

    <script>
        function isObject(value) {
            return typeof value === 'object' && value !== null
        }
        /**
         * 创建响应式数据
         *  @param [object]: 普通对象
         *  @return [Proxy]: 代理对象
         */
        function reactive(data) {
            if (!isObject(data)) return

            return new Proxy(data, {
                get(target, key) {
                    return target[key]
                },
                set(target, key, value) {
                    target[key] = value
                    return true
                },
            })
        }

        const state = { name: 'xiaopang' }
        const p = reactive(state)
        p.name = 'xxp'
        console.log(p.name)
    </script>

最基本的响应式

既然可以自定义set操作, 只需要在自定义set操作时, 重新执行属性关联的副作用函数

<div id="app">hello</div>

<body>
    <script>
        /**
           * 定义响应式
           *  @param [object] : 普通对象
           *  @return [Proxy] : 代理对象
           */
        function reactive(data) {
            // 如果传入的data不是一个普通对象, 不处理
            if (typeof data !== 'object' || data == null) return

            return new Proxy(data, {
                get(target, key) {
                    console.log(`自定义访问${key}`)
                    return target[key]
                },
                set(target, key, value) {
                    console.log(`自定义设置${key}=${value}`)
                    target[key] = value // 先更新值
                    effect() // 再调用effect, 调用effect时会重新获取新的数据
                    return true
                },
            })
        }

        let pState = reactive({ name: 'hello' })

        function effect() {
            app.innerHTML = pState.name
        }


        setTimeout(() => {
            pState.name = 'world'
        }, 1000)
    </script>
</body>

definePrototype vs Proxy

有人不经要问了,这个和Object.definePrototype有什么区别

什么是Object.defineProperty

Object.definePrototype是对对象上的属性进行新增或者修改, 有2种写法,数据描述符或者访问器描述符, IE8不支持(敲黑板, 面试官再问起来为什么Vue不支持IE8,就这么告诉他)

    const obj = {
        name: 'chrome'
    }
    // 数据描述符
    Object.defineProperty(obj, 'age', {
        configurable: true, // 这个定义是否可以被delete
        enumerable: true, // 这个值是否可以被for in 枚举,或者Object.keys获取到
        writable: true, // 定义是否可以被修改
        value: '100'
    })
    // 访问器描述符
    Object.defineProperty(obj, 'child', {
        configurable: true,
        enumerable: true,
        set(value) {
            console.log(value)
        },
        get() {
            console.log(this.value)
        }
    })

    console.log(obj);

defineProperty和Proxy的区别

  • Object.defineProperty对对象自身做修改, 而Proxy只是在Object基础上加一层拦截,不修改原对象(其实并不是这样,对于不支持嵌套对象,如果你想监听嵌套的,那么这个地方就不对了。后面会说到)
  • 监听不了数组的变化
  • 监听手段比较单一,只能监听set和get, Proxy有10几种监听
  • 必须得把所有的属性全部添加defineProperty, Proxy对整个对象都会进行拦截

1.Proxy不用遍历每一个属性?
 

    var needProxyObj = { name: 'chrome', age: '800' }
    var proxyObj = new Proxy(needProxyObj, {
        set(target, key, value, receiver) {
            console.log('proxy修改了', key, value)
        }
    })
    proxyObj.name = 'safari'; // proxy修改了 name safari

Proxy是代理在对象级别的,defineProperty是代理到静态的值级别,所以Proxy的强大就在这里

2.Proxy不修改原对象?是在对象上面加一层代理?

    var needProxyObj = { name: 'chrome', age: '800' }
    var proxyObj = new Proxy(needProxyObj, {
        set(target, key, value, receiver) {
            console.log('proxy修改了', key, value)
        }
    })
    proxyObj.name = 'safari'; // proxy修改了 name safari
    needProxyObj.child = 'sun'; // sun , 没有被拦截
    console.log(proxyObj.child); // sun
    console.log(needProxyObj === proxyObj, needProxyObj, proxyObj)    // false

看到没, 当我修改被代理之前的对象的时候,拦截器没有起作用,并且被代理的新对象proxyObjchild值也跟着变化了, 但是needProxyObj === proxyObj; // false, 这又是蜜汁操作之一了。其实也好理解,代理对象和被代理的对象,他们在表面上是不一样的,其实在底层是同一个对象。

为什么我又说Proxy不修改原对象也是不准确的。

这就涉及到Proxy和defineProperty的一个共同特性,不支持对象嵌套。需要递归去实现。

    var person = {
        name: '阿巴',
        age: '100',
        child: {
            name: '阿巴的儿子',
            age: '88'
        }
    }
    var proxyEvent = {

    }
    var deepProxy = function (obj) {
        if (typeof obj === 'object') {
            Object.entries(obj).forEach(([key, value]) => {
                obj[key] = deepProxy(value);
            })
            return new Proxy(obj, proxyEvent)
        }
        return obj;
    }
    console.log(deepProxy(person));

可以复制以上代码看看
非常不幸的是,这时候原对象的child不在是一个单纯的孩子, 它被Proxy了

image.png

这就是我说为什么不准确的原因了, 所以万不得已,真心不推荐用递归的方式去设置Proxy, 当然,有办法递归设置Proxy,咱们就有办法给它还原

    function proxyToObject(proxyObj) {
        const next = function (obj, mergeObj) {
            if (typeof obj === 'object') {
                Object.entries(obj).forEach(([key, value]) => {
                    if (!value) {
                        mergeObj[key] = value;
                    } else if (value instanceof Array) {
                        mergeObj[key] = next(value, []);
                    } else if (value instanceof Object) {
                        mergeObj[key] = next(value, {});
                    } else {
                        mergeObj[key] = value;
                    }
                })
            }
            if (obj && obj instanceof Array) {
                for (let value of obj) {
                    mergeObj.push(next(value, {}));
                }
            }
            return mergeObj;
        }
        return next(proxyObj, {});
    }
    console.log(proxyToObject(person));// 然后就恢复了

为什么说Proxy可以监听数组的访问

我们都知道在Vue中,由于defineProperty的局限性,数组的push被认为是变异的特性,为什么vue的push可以被监听到呢,是因为vue把数组对象的push给重写了,进行拦截,这导致增加了不少额外的操作

    var arr = [1, 2, 3, 4];
    let arrProxy = new Proxy(arr, {
        get(target, propKey) {
            if (Array.isArray(target) && typeof Array.prototype[propKey] === 'function') {
                Promise.resolve().then(e => {
                    console.log('操作了数组', target, propKey);
                })
            }
            return target[propKey]
        }
    })
    arrProxy.push(5);
    console.log('push结束了', arrProxy, arr);
    // push结束了
    // 操作了数组 push


为什么要使用Promise.resolve(), 是因为push是一个同步操作,在访问 push的时候还没有执行这个函数,所以想在push之后做一些事情,就可以用这个微任务机制来把操作滞后

以上通过代码的方式解释了一下Proxy和definePrototype的区别,至于其他更细致的用发 

猜你喜欢

转载自blog.csdn.net/qq_63358859/article/details/143267396