这是我参与更文挑战的第7天,活动详情查看:更文挑战
我们知道 vue2 的数据绑定是通过 Object.defineProperty 实现,vue3 则是通过 Proxy 实现。文章介绍 Object.defineProperty 与 Proxy 的具体原理,尝试利用这两种方式实现简单的数据监听。
Object.defineProperty
属性描述符
首先我们先了解一下对象的属性描述符。通常我们给对象定义属性的时候是这样的:
let obj = {
name: 'LvLin'
}
能够直观地看到 obj 对象存在名为 name 的属性,除此之外并没有定义其它的东西,那「属性描述符」是什么?要怎么查看?
事实上对于对象上的每个属性,都有一个「属性描述符」来规定这个属性的相关特性。我们可以通过Object.getOwnPropertyDescriptor()
方法获取到对象上一个自有属性的属性描述符。
Object.getOwnPropertyDescriptor(obj, 'name')
// {value: "LvLin", writable: true, enumerable: true, configurable: true}
可以看到属性描述符是一个对象类型,它总共有 6 个键值可配置:
- configurable:当且仅当该属性的
configurable
键值为true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false
。 - enumerable:当且仅当该属性的
enumerable
键值为true
时,该属性才会出现在对象的枚举属性中。默认为false
。 - value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为
undefined
。 - writable:当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为
false
。 - get:当访问该属性时,会调用此函数。不传入参数。该函数的返回值会被用作属性的值。默认为
undefined
。 - set: 该方法接受一个参数(被赋予的新值)。当属性值被修改时,会调用此函数。默认为
undefined
。
注意:上述默认值指的是在使用
Object.defineProperty
方法定义属性的情况,如果是直接在{}
定义的属性,configurable
、enumerable
、writable
的默认值都为true
。我们将刚刚的示例与下面这个示例比较一下:
let obj = {}; Object.defineProperty(obj, 'name', { value: 'LvLin' }) Object.getOwnPropertyDescriptor(obj, 'name' // {value: "LvLin", writable: false, enumerable: false, configurable: false}
以上键值都是可选的,但是有几个是不能同时配置的。属性描述符的键值配置只有两种存在形式,分别称为「数据描述符」和「存储描述符」,如下所示:
- 数据描述符,可以拥有的属性为
configurable
、enumerable
、value
、writable
; - 存储描述符:可以拥有的属性为
configurable
、enumerable
、get
、set
; - 如果一个描述符不具有
value
、writable
、get
和set
中的任意一个键,则为数据描述符。
如果一个属性的属性描述符同时拥有 value
或 writable
和 get
或 set
键,则会产生一个异常,如下所示:
let obj = {};
Object.defineProperty(obj, "name", {
value: 'LvLin',
get: function() {
return 'LvLin';
}
});
// 报错
// TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
Object.defineProperty() 方法介绍
可以看到上述代码我们使用了 Object.defineProperty() 方法来定义对象的属性和属性描述符,该方法使用语法为Object.defineProperty(obj, prop, descriptor)
。参数介绍如下:
obj
:要定义属性的对象;prop
:要定义或修改的属性名称;descriptor
:要定义或修改的属性描述符
返回值为操作后的对象。简单使用如下所示:
let obj = Object.defineProperty({}, "name", {
value: 'LvLin',
writable: true,
enumerable : true,
configurable : true
});
console.log(obj); // {name: "LvLin"}
let value = 1;
Object.defineProperty(obj, "num", {
get: function(){
console.log('执行了 get 操作')
return value;
},
set: function(newValue) {
console.log('执行了 set 操作')
value = newValue;
}
})
obj.num; // 执行了 get 操作
obj.num = 2; // 执行了 set 操作
我们利用属性描述符中的 set 跟 get 方法,简单实现一个数据监听如下所示:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<title>LvLin test</title>
</head>
<body>
<span id="container">1</span>
<button id="button">+</button>
<script>
let obj = {
value: 1
}
let value = 1;
Object.defineProperty(obj, "value", {
get: function() {
return value;
},
set: function(newValue) {
value = newValue;
document.getElementById('container').innerHTML = newValue;
}
});
document.getElementById('button').addEventListener("click", function() {
obj.value += 1;
});
</script>
</body>
</html>
Object.defineProperty() 不兼容 IE8 及其以下浏览器,这也是 vue2 不支持 IE8 及其以下浏览器的主要原因。
Proxy
proxy 是 ES6 新增的特性,用于创建一个对象的代理,该代理是一个全新的对象,存在一系列对原对象的操作,都能够经过代理对象的拦截,从而实现对对象行为操作的过滤和改写。使用语法如下:
const p = new Proxy(target, handler)
// target: 要使用 Proxy 包装的目标对象
// handler: 一个对象,它的各属性中的函数分别定义了在执行各种操作时代理 p 的行为。即定义拦截行为。
使用示例如下所示:
var proxy = new Proxy({}, {
get: function(obj, prop) {
console.log('设置 get 操作')
return obj[prop];
},
set: function(obj, prop, value) {
console.log('设置 set 操作')
obj[prop] = value;
}
});
proxy.time = 35; // 设置 set 操作
console.log(proxy.time); // 设置 get 操作 // 35
proxy 总共可以拦截 13 种操作,同时搭配 Reflect (ES6 为了操作对象而提供的新 API)可以实现更强大的功能,具体可以翻阅阮一峰的 《ECMAScript 6 入门——Proxy》 和 《ECMAScript 6 入门——Reflect》 进行学习。
下面我们对先前的 HTML 页面的 JS 代码部分进行改写,换成用 Proxy 来实现数据监听,如下所示:
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<title>LvLin test</title>
</head>
<body>
<span id="container">1</span>
<button id="button">+</button>
<script>
let proxy = new Proxy({}, {
get: function (target, prop) {
return target[prop];
},
set: function (target, prop, value) {
target[prop] = value;
document.getElementById('container').innerHTML = value;
}
});
document.getElementById('button').addEventListener("click", function () {
proxy.value += 1;
});
</script>
</body>
</html>
自己简单尝试一下,看看效果如何~
总结
当使用 defineProperty 时,我们是通过修改原来的 obj 对象属性的属性描述符来实现数据监听,而使用 proxy,实际上是返回了一个新的代理对象,通过该代理对象拦截数据操作,实现数据监听,对原始的 obj 对象没有影响。
Proxy 的缺点是存在比较大的浏览器兼容问题,很多效果无法使用 poilyfill 来弥补。
参考
Object.defineProperty(), by MDN
Proxy,by MDN
Proxy, by 阮一峰