使用Proxy实现watch监听对象

  • 手写一个简单的watch函数用来监听某个数据的变化,比如Object
  • 我们来写一个简单的形式的watch
  • 形式:watch(target, (newVal, oldVal)=>{ })
  • target表示监听的对象newVal, oldVal自然就是新值和旧值
  • 如下完整代码:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>变化</button>
    <script>
        function watch(target, callback) {
            let oldObj = JSON.parse(JSON.stringify(target)); // 深拷贝对象保存一份旧值
            const handler = {
                set: function (obj, prop, value, receiver) {
                    /**
                     * Reflect.set(target, propertyKey, receiver, value)
                     * target 要设置其属性的对象
                     * propertyKey 要设置的属性的名称
                     * value 要赋给属性的值
                     * receiver 接收赋值的对象,一般就是target
                     * */ 
                    const result = Reflect.set(obj, prop, value, receiver);
                    callback(obj, oldObj); // 新旧值吐出去
                    oldObj = JSON.parse(JSON.stringify(obj)); // 旧值更新
                    return result;
                }
            };
            return new Proxy(target, handler);
        }

        // 原始对象数据,只是用来代理一下,往后就不用了,vue3的ref也是类似的意思
        let obj = {
            name: '孙悟空',
            age: 500,
        }

        // 代理了一个新对象,只更改这个新对象即可,后续操作都通过这个新对象
        let proxyObj = watch(obj, (newVal, oldVal) => {
            console.log(newVal, oldVal);
        });

        // 按钮的点击修改代理对象的数据值,就会被watch监听到,然后触发新旧值的展示
        let btn = document.querySelector('button');
        btn.onclick = () => {
            proxyObj.age = proxyObj.age + 1;
            setTimeout(() => {
                proxyObj.name = proxyObj.name + '^_^ '
            }, 1000);
        };
    </script>
</body>

</body>
</html>
上述代码不考虑深层次对象,若是深层次,那就是递归操作即可,不赘述

注意,vue3的原始数据,也是经过代理后,就不用了

如下代码:

<el-input v-model="vvv"></el-input>

let initVal = '我是初始值'
const vvv = ref(initVal)
  • 当我们在输入框中输入内容的时候,vvv的值,会发生变化
  • 但是初始值initVal的值,是不会再变化的
  • 即 initVal 只是使用了一次,类似上边的代码

defineProperty监听形式

  • 比如使用Object.defineProperty去监听某个对象的某个属性值变化
  • 这种和上面的区别就是原始对象,依旧及时使用
  • 如下:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button>变化</button>
    <script>
        function watch(obj, cb) {
            let oldObj = JSON.parse(JSON.stringify(obj))
            for (const key in obj) {
                let val = obj[key]
                Object.defineProperty(obj, key, {
                    get() {
                        return val
                    },
                    set(newVal) {
                        cb(newVal, val, key)
                        val = newVal
                    }
                })
            }
        }


        let obj = {
            name: '孙悟空',
            age: 500,
        }

        watch(obj, (newVal, oldVal, key) => {
            console.log('---', newVal, oldVal, key);
        })

        let btn = document.querySelector('button');
        btn.onclick = () => {
            obj.age = obj.age + 1;
            setTimeout(() => {
                obj.name = '猪八戒'
            }, 1000);
        };
    </script>
</body>

</body>

</html>
  • 实际上,Proxy功能更为强大,还可以代理函数,实现功能的重写
  • 就类似于继承后的新加功能
  • 如下:

用Proxy代理函数可添加额外逻辑

假设有一个函数,用来求和,求1~n的和,如下:

 // 求1~n的累加的和
function sum(n) {
    let result = 0
    for (let i = 0; i < n; i++) {
        result = result + i + 1
    }
    return result
}
  • 某些情况下,我们不能修改这个函数
  • 并且也不方便重写这个函数
  • 我们得在这个函数中,添加一些功能
  • 比如:当前的函数返回的是1~n的累加值
  • 要修改成返回一个数组,数组的第一项是1~n的累加值,数组的第2项是要返回1~n的累乘值
  • 那么,这个时候,我们就可以Proxy来代理这个函数,去加功能
  • 如下:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <script>
        // 求1~n的累加的和
        function sum(n) {
            let result = 0
            for (let i = 0; i < n; i++) {
                result = result + i + 1
            }
            return result
        }

        // 代理sum函数,新增相关逻辑
        let proxySum = new Proxy(sum, {
            /**
             * target 是代理的东西,本例中代理的是sum函数
             * proxySumThis 是proxySum执行时传递的this值,用得少,本案例没有用到
             * args proxySum执行时,传来的参数数组,数组存放,代表可能传进来多个参数
             * */
            apply: (target, proxySumThis, args) => {
                // 原始函数执行结果
                let res1 = target.apply(proxySumThis, args)
                // 新增逻辑函数 阶乘 函数
                let res2 = Array.from({ length: args[0] }, (_, i) => i + 1).reduce((acc, cur) => acc * cur, 1);
                // 原有函数返回一个累加值,代理函数改写返回数组,第一项累加值,第二项累乘值
                return [res1, res2]
            }
        })
        console.log('proxySum', proxySum(4));
    </script>
</body>

</html>
  • 上述案例中,我们也可以去新增逻辑代理函数
  • 去计算函数执行时间
  • 如下:
<script>
    let proxySum = new Proxy(sum, {
        apply: (target, proxySumThis, args) => {
            console.time()
            let res = target.apply(proxySumThis, args) // 原始函数执行
            console.timeEnd()
            return res
        }
    })
    console.log('proxySum', proxySum(4));
</script>

这样我们就新增了一个及时

Proxy的一种角度的理解

  • Vue3.0中通过Proxy来替换原本的Object.defineProperty来实现数据响应式
  • Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。
  • tips: ES6是2015年6月发布的
let p = new Proxy(target, handler)

target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

当然,还有别的语法:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refer...

实际上,功能超级强大

如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
  • 实际上,笔者认为,Proxy的兼容性已经不错了
  • 毕竟是2015年发布的ES6里面的东西(现在是2024年了)
  • 可是啊,IE11是微软在2013年10月17日发布的浏览器
  • 所以,2013年还没ES6呢,也就没有Proxy
  • 所以,vue3不兼容IE11,是因为IE11没法用ES6里面的Proxy
  • 历史原因...
A good memory is better than a bad pen. Record it down...

水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负