什么是数据劫持与事件委托?有什么用?

今天我们来讲讲es6中比较高级的两个东西,数据劫持与事件委托,当然数据劫持是es6之前就有的。

它们有什么用呢?它们主要是用来进行数据绑定的。比如,我们在使用vue的时候,我们想要获取用户在input框输入的内容,我们可以使用v-model绑定一个变量去获取,这个变量就会存放用户在input框输入的内容,之后我们就可以拿着这个变量去进行操作。这就叫数据绑定,而在vue中,是不是还存在着很多类似的操作,比如用ref可以声明一个响应式变量,这个变量的值只要发生改变就会导致组件更新。你说为什么ref绑定的变量能实现组件的更新呢?它里面进行了一些什么样的操作?

其实就是用数据劫持与事件委托完成的,所以,了解数据劫持与事件委托对我们阅读vue的源代码有很大的帮助。

1. Object.defineProperty()

我们先来聊聊Object.defineProperty(),也叫数据劫持。我们来学一下它的语法。

假如我们有一个对象obj,我们要对它进行数据劫持,我们会这么写:

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    
})

Object.defineProperty函数的第一个参数放你要进行劫持的对象,第二个参数放要劫持的是哪一个属性,第三个参数就放一个对象。这样,我们就说,对象obj被数据劫持了。

被劫持了之后能进行一些什么样的操作呢?

比如:

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    value: 100,
})

console.log(obj.a);

虽然obj.a的值为1,但我们可以在数据劫持中就说它的值为100,就相当于obj.a被绑匪劫持了,它现在值多少钱就由绑匪说了算了。此时,我们再去输出obj.a的值就会输出100。

它还可以设置是否可写:

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    value: 100,
    writable: false,  // 可写
})

obj.a = 200

console.log(obj.a);

我们设置writable为false,就表示这个a是不可以修改值的。设置为true就是可以修改值。我们再去更改一下obj.a的值,看看是否有变化。

还是100,是不可写的。

再来,它还可以设置这个属性是否可枚举:

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    value: 100,
    writable: false,  // 可写
    enumerable: true, // 枚举
})

for (let key in obj) {
    console.log(key);
}

可枚举是什么意思呢?就是这个属性是否能遍历到,是否可读取值。我们设置enumerable为true,就表示它可以枚举。然后我们再去遍历这个对象,就能读取到这个值了:

它还可以设置这个属性是否可配置:

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    value: 100,
    writable: false,  // 可写
    enumerable: true, // 枚举
    configurable: false, // 可配置
})

delete obj.a
console.log(obj.a);

是否可配置是什么意思呢?就是它能不能移除掉。我们设置configurable为false,就表示它不可移除。然后我们delete obj.a,看看是否移除掉了。

输出结果还是100,说明我们无法移除它。

数据劫持中还有两个非常重要的属性,get和set。我们先来认识一下get。

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    // value: 100,
    // writable: false,  // 可写
    // enumerable: true, // 枚举
    // configurable: false, // 可配置
    get() {
        return 1000
    },
})

console.log(obj.a);

这个get是一个函数,它的作用是只要我们去读取属性a的值,get函数就会触发。比如我在get函数中return 1000。现在,当我们去读取a的值时,get函数就会触发,就会输出1000。

你看,get函数就触发了。

我们再来看一下set,set也是一个函数,它的作用是只要我们去修改属性a的值,set函数就会触发。

js 复制代码
let obj = {
    a: 1
}

Object.defineProperty(obj, 'a', {
    // value: 100,
    // writable: false,  // 可写
    // enumerable: true, // 枚举
    // configurable: false, // 可配置
    get() {
        return 1000
    },
    set(val) {
        console.log('set:', val);
    }
})

obj.a = 2000

现在,我去将obj.a的值更改为2000,set函数就会触发,你看看会输出什么:

看,set函数就触发了。我们发现,我们将obj.a的值修改为2000,这个2000就传给了set函数的形参val。

那了解完了这两个函数,我们就可以来完成一个有意思的东西了。只要我们去修改属性a的值,我们就能获取到这个修改后的值。

js 复制代码
let obj = {
    a: 1
}
let value = obj.a

Object.defineProperty(obj, 'a', {
    // value: 100,
    // writable: false,  // 可写
    // enumerable: true, // 枚举
    // configurable: false, // 可配置
    get() {
        return value
    },
    set(val) {
        value = val
    }
})

obj.a = 2000
console.log(obj.a);

我们提前准备一个中间量value,先让它等于obj.a的初始值。然后在get函数中return value,在set函数中将value赋值为val。

现在,只要我们去修改属性a的值,set函数就会触发,就会将val赋值给value,value就接收到了这个修改后的值。然后我们再去读取obj.a时,get函数就会触发,就return value,就会返回这个修改后的值。

现在,这个obj.a就像被全权掌控了,对它进行的操作全都由Object.defineProperty说了算。

我们再来,那了解完了这两个函数,我们能不能完成这样一个操作:

js 复制代码
let obj = {
    a: 1,
    b: 2,
    c: 3
}

console.log(obj.a + obj.b + obj.c);

我们有一个对象obj,里面有3个属性a、b、c。我想要的效果是,只要我去修改obj中任意一个属性的值,就要让console.log(obj.a + obj.b + obj.c)执行一次,怎么用数据劫持完成它呢?

那我们就要对这三个属性都进行数据劫持了,先得去遍历对象obj。

js 复制代码
let obj = {
    a: 1,
    b: 2,
    c: 3
}

for (let key in obj) {
    let value = obj[key]
    Object.defineProperty(obj, key, {
        get() {
        
        },
        set() {
           
        }
    })
}

我们提前准备一个中间量value,让它为属性的初始值。是不是就和刚刚一样的操作啊。当要去修改值时,就触发set函数,我们就将修改后的值赋给value,要去读取值时,就会触发ge函数,就return value就行了。

js 复制代码
let obj = {
    a: 1,
    b: 2,
    c: 3
}

for (let key in obj) {
    let value = obj[key]
    Object.defineProperty(obj, key, {
        get() {
            return value
        },
        set(val) {
            value = val
            log()
        }
    })
}

function log() {
    console.log(obj.a + obj.b + obj.c);
}

obj.a = 10
obj.b = 20
obj.c = 30

你看这样是不是就完成了,我们把输出语句单独封装一个函数log去执行,这样写法更优雅。当我们要去修改值时,set函数就会触发,log函数也会触发,就去输出这三个属性值的和。那log函数触发了,在log函数里我们要读取属性的值,那get函数不就触发了,就return修改后的值。最后输出这三个值的和。

我们看看有没有这个效果:

看,我们每修改一次属性的值,就会得到它们的和。

那你说,这个东西很像什么?是不是很像ref声明的响应式变量啊,只要响应式变量的值更改了,组件就会更新,那不就很像set函数被调用了吗。我只要在set函数中去写让组件更新的代码,那只要这个响应式变量的值更改了,组件是不是就更新了。所以,响应式的原理就是这个,就是基于这个写的,当然这是vue2的响应式原理,vue3的响应式原理就要基于我们接下来要介绍的一个东西了。

2. Proxy

Proxy,也叫事件委托,了解完了数据劫持,接受Proxy起来就非常好接受了。

我们先来看一下它的语法:

js 复制代码
let obj = {
    a: 1,
    b: 2,
    c: 3
}

let proxy = new Proxy(obj, {
    
})

Proxy是一个构造函数,它接收两个参数,第一个参数是你要代理的对象,第二个参数是一个对象,是你要代理这个对象身上的哪些行为。

它里面同样有set函数和get函数,并且它的功能比数据劫持更强大,数据劫持只能劫持对象中的某一个属性,并且能进行的操作也只有那么几种,并且它也只能劫持对象。而Proxy能代理整个对象,并且它除了set和get函数,它还有11个函数,能代理对象的某种操作,并且它还可以代理数组。

我们先来认识一下set和get函数。

js 复制代码
let obj = {
    a: 1,
    b: 2,
    c: 3
}

let proxy = new Proxy(obj, {
    get: function (target, key) {
        return target[key];
    },
    set: function (target, key, value) {
        target[key] = value
    }
})
proxy.a = 2
console.log(proxy.a);

get函数接收两个参数,target代表代理的那个对象,key代表的是对象中的哪个属性。get函数接收三个参数,除了target和key,还有一个value,就是你要修改后的那个值。

接下来,我们要进行的操作就全部都在这个实例对象proxy身上做了,obj已经被代理了。然后我们把a的值修改为2,再去读取a。它就会先触发set函数,将value赋值给 target[key] 之后再触发get函数,return target[key]。

这就是事件委托,将这个对象委托给这些函数。

了解了一下Proxy,我们可以使用Proxy来模拟一个vue中的watch函数。watch函数是用来监听一个变量的,当这个变量的值发生了改变,watch中的回调就会触发。

就是有这样一个场景:

html 复制代码
<div id="app">
        <h1 id="count">1</h1>
        <button id="btn">add</button>
</div>

页面上有一个1,1的下面有一个button按钮。当我们点击这个按钮时,页面上的1就要加1。我们来手写一个watch函数来实现它。

html 复制代码
    <script>
        const btn = document.getElementById('btn');
        const h1 = document.getElementById('count');

        let obj = {
            count: 1
        }

        function watch(obj, cb) {
            
        }  
    </script>

怎么来定义这个watch函数呢,首先接收两个参数,第一个参数是我们要监听的对象,第二个参数是一个回调。然后我们在watch函数里面去调用Proxy得到一个实例对象,再把这个实例对象返回出来。

html 复制代码
    <script>
        const btn = document.getElementById('btn');
        const h1 = document.getElementById('count');

        let obj = {
            count: 1
        }

        function watch(obj, cb) {
             return new Proxy(obj, {
                get(target, key) {
                    return target[key]
                },
                set(target, key, value) {
                    cb(value, target[key]);
                    target[key] = value
                }
            })
        }  
    </script>

当obj.count的值被修改时,set函数就会触发,我们在set函数里将watch的回调函数触发掉。

这样当我们调用watch函数时,就能得到一个实例对象。

html 复制代码
    <script>
        const btn = document.getElementById('btn');
        const h1 = document.getElementById('count');

        let obj = {
            count: 1
        }

        function watch(obj, cb) {
             return new Proxy(obj, {
                get(target, key) {
                    return target[key]
                },
                set(target, key, value) {
                    cb(value, target[key]);
                    target[key] = value
                }
            })
        }
        
        const proxy = watch(obj, (newVal, oldVal) => {
            h1.innerHTML = newVal;
        })

        btn.addEventListener('click', () => {
            proxy.count++;
        })
    </script>

我们在watch的回调里去将h1标签的值修改为新值。当然我们还要给button绑定一个点击事件,每点一次button就让count的值加1。这样我们就完成了这个效果:

当然,肯定和原版的watch不完全一样,只是我们借助这个例子来加深一下对Proxy的认识,看看它有什么作用。

3. 总结

本次我们一起来学习了一下数据劫持与事件委托是什么,以及它们有什么用

Object.defineProperty()

  • 数据劫持
    1. 劫持对象中的某一个属性,可以控制该属性是否可读、可写、可枚举、可配置,还可以指定该属性的值以及当该属性值被读取时会触发get方法,当该属性被修改时会触发set方法
    2. 只能接收对象,不能劫持数组

Proxy

  • 委托
    1. 直接代理整个对象,对象上的读取值、修改值、添加属性、删除属性等13中行为都会被代理到某个函数上
    2. 可以代理数组

如果对你有帮助的话请点个赞吧。

相关推荐
百万蹄蹄向前冲1 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5812 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路2 小时前
GeoTools 读取影像元数据
前端
ssshooter3 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友3 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry3 小时前
Jetpack Compose 中的状态
前端
dae bal4 小时前
关于RSA和AES加密
前端·vue.js
柳杉4 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog5 小时前
低端设备加载webp ANR
前端·算法
LKAI.5 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi