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

今天我们来讲讲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. 可以代理数组

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

相关推荐
mm_exploration19 分钟前
halcon三维点云数据处理(六)find_box_3d
前端·数据库·3d
怒放的生命.27 分钟前
《前端web开发-CSS3基础-1》
前端·css·css3
明月看潮生42 分钟前
青少年编程与数学 02-006 前端开发框架VUE 05课题、使用模板
前端·javascript·vue.js·青少年编程·编程与数学
时间sk43 分钟前
HTML——38.Span标签和字符实体
javascript·html
cxsj9991 小时前
Uncaught ReferenceError: __VUE_HMR_RUNTIME__ is not defined
前端·javascript·vue.js
坚强de土豆仔1 小时前
我的创作纪念日
前端
Jiaberrr2 小时前
页面转 PDF 功能的实现思路与使用方法
前端·javascript·vue.js·微信小程序·pdf·uniapp
阿珊和她的猫2 小时前
JavaScript中的内存泄露:识别与避免
开发语言·javascript·ecmascript
XDU小迷弟2 小时前
第2天:Web应用&架构类别&源码类别&镜像容器&建站模版&编译封装&前后端分离
服务器·前端·安全·web安全·架构·安全架构
pchmi2 小时前
C# OpenCV机器视觉:背景减除与前景分离
javascript·opencv·c#