今天我们来讲讲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()
- 数据劫持
- 劫持对象中的某一个属性,可以控制该属性是否可读、可写、可枚举、可配置,还可以指定该属性的值以及当该属性值被读取时会触发get方法,当该属性被修改时会触发set方法
- 只能接收对象,不能劫持数组
Proxy
- 委托
- 直接代理整个对象,对象上的读取值、修改值、添加属性、删除属性等13中行为都会被代理到某个函数上
- 可以代理数组
如果对你有帮助的话请点个赞吧。