关于 Vue.js 双向数据绑定基本实现认知

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》

写在前面


  • 很早的一篇博客,整理了部分,蹭假期整理完
  • 博文内容涉及:
  • 双向数据绑定 实现方式简单介绍
  • 基于发布订阅数据劫持双向数据绑定两种不同实现(ES5/ES6) Demo,以及代码简单分析
  • Object.defineProperty && Proxy API 介绍以及特性对比
  • 理解不足小伙伴帮忙指正 :),生活加油

对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》


双向数据绑定介绍

在前端框架中,特别是响应式框架(如Vue.js, Angular等)中,双向数据绑定(Two-way data binding)是一个核心特性,它允许开发者在UI和数据之间建立直接的、双向的联系(MVVM)。下面是一些实现双向数据绑定的常见做法:

脏值检查(Dirty Checking)

脏值检查是一种简单的双向数据绑定策略。它周期性地检查数据模型(Model)是否发生了变化,如果发生了变化,则更新视图(View)。脏值检查通常涉及一个“检查周期”或“轮询间隔”,在这个间隔内,框架会遍历所有绑定,并检查是否有任何变化。

然而,脏值检查并不高效,因为它可能需要对整个数据模型进行不必要的遍历,即使数据实际上并没有改变。此外,它也不能立即反映变化,因为它依赖于轮询间隔。

数据劫持(Data Interception)

数据劫持(也称为数据代理或对象劫持)是一种更高效的双向数据绑定策略。它依赖于JavaScriptObject.defineProperty()方法(在ES5中引入),该方法允许你定义或修改对象的属性,包括getter和setter方法。

Vue.js 的早期版本中,当一个对象被用作数据模型时,Vue 会遍历它的所有属性,并使用 Object.defineProperty() 将它们转化为getter/setter,以便在数据变化时能够立即感知到。当视图需要读取数据模型时,getter方法会被调用;当视图需要更新数据模型时,setter方法会被调用,并且可以在这里触发视图的更新。

Vue.js 3.0 开始,引入了更高效的响应式系统,称为Proxy-based reactive systemVue.js 3.0 及以后的版本使用ES6的Proxy来实现双向数据绑定。通过使用Proxy,Vue.js可以更灵活地劫持整个对象,并监视对象的新增和删除属性操作,以及数组的索引和长度变化。

发布者-订阅者模式(Publisher-Subscriber Pattern)

发布者-订阅者模式是一种软件设计模式,它允许一个或多个发布者(Publisher)发布事件,而零个或多个订阅者(Subscriber)会监听这些事件,并在事件发生时执行相应的操作。

在双向数据绑定的上下文中,数据模型可以被视为发布者,而视图则是订阅者。当数据模型发生变化时,它会发布一个事件(通常是一个“change”事件),而所有订阅了这个事件的视图都会收到通知,并更新自己以反映新的数据。

这种模式允许数据模型和视图之间实现松散的耦合,因为它们之间不需要直接通信;它们只需要知道如何发布和监听事件即可。此外,这种模式还具有良好的可扩展性,因为你可以轻松地添加新的发布者或订阅者,而无需修改现有的代码。

MVVM

Vue.js 双向绑定的简单实现

Vue.js 使用了数据劫持(通过Object.defineProperty()、ES6的Proxy)和发布者-订阅者模式(通过自定义的Dep类和Watcher类)来实现其双向数据绑定机制。而Angular则使用了脏值检查Zone.js库(它类似于数据劫持,但工作方式略有不同)来实现类似的功能。

在这里插入图片描述

Object.defineProperty 数据劫持 Demo

下面的 Demo 简化了 Vue.js 实现,通过数据劫持、订阅者和发布者的机制,实现了将数据和DOM节点进行绑定,并在数据变化时自动更新相关的DOM节点,从而实现了简单的双向数据绑定功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Two-way data-binding</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
<br />
{{ text }}
</div>
<script>
/*
*@Time : 2024/05/03 10:59:55
*@Desc : 数据劫持 或者叫数据监听
*/
function observe(obj, vm) {
// 获取所有的数据,构造数据劫持
Object.keys(obj).forEach((key) => {
console.log(1, "劫持的数据对象为", key, "值为", obj[key])
defineReactive(vm, key, obj[key]);
});
}

/*
*@Time : 2024/05/03 10:44:59
*@Desc : 响应式处理函数
*/
function defineReactive(obj, key, val) {
let dep = new Dep();
console.log(2, "创建发布者:", dep)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 使用getter/setter 来实现数据劫持
get: _ => {
// 注册订阅者,如果发布者存在
if (Dep.target) {
dep.addSub(Dep.target);
}
return val // 返回属性值
},
set: (newVal) => {
if (newVal === val) {
return
}
val = newVal;
console.log(5, "更新监听的数据", key, newVal)
// 通知相关的订阅者进行更新
dep.notify();
}
});
}

/*
*@Time : 2024/05/03 10:48:55
*@Desc : 简化的虚拟DOM编译和更新的示例
*/
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;
console.log(3, "编译虚拟 Dom 节点")
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child);
}
// 返回虚拟 Dom
return flag;
}

/*
*@Time : 2024/05/03 10:52:42
*@Desc : 编译DOM节点
*/
function compile(node, vm) {
// 节点类型为元素
if (node.nodeType === 1) {
// 获取所有的属性
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
// 添加 input 事件
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的set方法
vm[name] = e.target.value;
debugger
});
// 将data的值赋给该node,这里触发 getter 方法
// 但是不进行注册
node.value = vm[name];
node.removeAttribute('v-model');
// 构造订阅者
new Watcher(vm, node, name, 'input');
}
}

}
let reg = /\{\{(.*)\}\}/;
// 节点类型为 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
// 构造订阅者
new Watcher(vm, node, name, 'text');
}
}
}
/*
*@Time : 2024/05/03 11:53:27
*@Desc : 订阅者
*/
function Watcher(vm, node, name, nodeType) {
// this为watcher函数
Dep.target = this;
// console.log(this);
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update() {
this.get();
// 如果是文本,直接更新 `nodeValue`
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
// 如果是输入标签,更新 value 的值
if (this.nodeType == 'input') {
this.node.value = this.value;
}
console.log(6.2, "通知 Dom", this.nodeType, "数据为", this.value)
},
// 获取 data 中的属性值
get() {
// 触发相应属性的 get,这里会进行订阅者注册
this.value = this.vm[this.name];
console.log(6.1, "获取", this.nodeType, "数据最新的值:", this.value)
}
}
/*
*@Time : 2024/05/03 11:48:38
*@Desc : 发布者
*/
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
console.log(4, "suds:", this.subs.length, "注册订阅者:", sub)
},
notify() {
console.log(6, "通知订阅者:", this.subs)
this.subs.forEach((sub) => {
sub.update();
});
}
};

/*
*@Time : 2024/05/03 11:07:01
*@Desc :
*/

function Vue(options) {
this.data = options.data;
let data = this.data;
// 构造数据劫持
observe(data, this);
let id = options.el;
let dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将dom返回到app中进行挂载
document.getElementById(id).appendChild(dom);
}

/*
*@Time : 2024/05/03 11:09:06
*@Desc : 定义 Vue 对象,传递需要挂载的元素,双向绑定的数据对象
*/
let vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>

在这里插入图片描述

简单分析一下干了什么:

observe 函数用于数据劫持,它接收一个对象和Vue实例作为参数。它通过遍历对象的属性,并调用defineReactive 函数来定义属性的getter和setter,从而实现对属性的劫持和监视。

1
2
3
4
5
6
7
8
function observe(obj, vm) {
// 获取所有的数据,构造数据劫持
Object.keys(obj).forEach((key) => {
console.log(1,"劫持的数据对象为", key, "值为",obj[key])
defineReactive(vm, key, obj[key]);

});
}

defineReactive 函数定义了属性的 getter和setter。它创建了一个Dep对象作为发布者,getter 中注册订阅者(Watcher),setter中更新属性的值并通知相关的订阅者进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function defineReactive(obj, key, val) {
let dep = new Dep();
console.log(2,"创建发布者:",dep)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 使用getter/setter 来实现数据劫持
get: _=> {
// 注册订阅者,如果发布者存在
if (Dep.target) {
dep.addSub(Dep.target);
}
return val // 返回属性值
},
set: (newVal) => {
if (newVal === val) {
return
}
val = newVal;
console.log(5,"更新监听的数据",key,newVal)
// 通知相关的订阅者进行更新
dep.notify();
}
});
}

nodeToFragment 函数用于将DOM节点转换为虚拟DOM(DocumentFragment)。它遍历DOM节点的子节点,并调用compile函数来解析和编译节点。

1
2
3
4
5
6
7
8
9
10
11
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;
console.log(3,"编译虚拟 Dom 节点")
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child);
}
// 返回虚拟 Dom
return flag;
}

compile 函数用于编译DOM节点。对于元素节点,它解析其属性,并处理带有v-model属性的输入节点,实现双向数据绑定。对于文本节点,它解析其中的双括号表达式({{...}}),并创建一个订阅者(Watcher)来监听相关的数据变化。

addEventListener 用于挂载 input 监听事件,当数据发生变化时,会触发 VM 中的 set 方法的数据劫持,从而调用 dep.notify() 方法,实现对所有订阅的通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function compile(node, vm) {
// 节点类型为元素
if (node.nodeType === 1) {
// 获取所有的属性
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
// 添加 input 事件
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的set方法
vm[name] = e.target.value;
debugger
});
node.value = vm[name]; // 将data的值赋给该node
node.removeAttribute('v-model');
// 构造订阅者
new Watcher(vm, node, name, 'input');
}
}

}
let reg = /\{\{(.*)\}\}/;
// 节点类型为 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
// 构造订阅者
new Watcher(vm, node, name, 'text');
}
}
}

Watcher对象表示一个订阅者。在构造函数中,它将自身赋值给Dep.target,然后通过调用update方法来获取数据并更新DOM节点的值。update方法根据节点类型(文本或输入)更新节点的nodeValue或value属性。在第一次获取值的时候会进行订阅者注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Watcher(vm, node, name, nodeType) {
// this为watcher函数
Dep.target = this;
// console.log(this);
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update () {
this.get();
// 如果是文本,直接更新 `nodeValue`
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
// 如果是输入标签,更新 value 的值
if (this.nodeType == 'input') {
this.node.value = this.value;
}
console.log(6.2,"通知 Dom",this.nodeType, "数据为",this.value)
},
// 获取 data 中的属性值
get () {
// 触发相应属性的 get,这里会进行订阅者注册
this.value = this.vm[this.name];
console.log(6.1,"获取",this.nodeType,"数据最新的值:",this.value)
}
}

Dep对象表示一个发布者,用于管理订阅者(Watchers)。它有一个subs数组用于存储订阅者,在addSub方法中添加订阅者,而在notify方法中通知所有订阅者进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub (sub) {
this.subs.push(sub);
console.log(4, "suds:", this.subs.length, "注册订阅者:", sub)
},
notify () {
console.log(6, "通知订阅者:", this.subs)
this.subs.forEach((sub) => {
sub.update();
});
}
};

Vue对象是自定义的框架的入口点。它接收一个选项对象,其中包含要挂载的元素的选择器和双向绑定的数据对象。在构造函数中,它调用observe函数进行数据劫持,然后调用nodeToFragment函数将DOM节点转换为虚拟DOM,并将其挂载到指定的元素上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Vue(options) {
this.data = options.data;
let data = this.data;
// 构造数据劫持
observe(data, this);

let id = options.el;
let dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将dom返回到app中进行挂载
document.getElementById(id).appendChild(dom);
}

/*
*@Time : 2024/05/03 11:09:06
*@Desc : 定义 Vue 对象,传递需要挂载的元素,双向绑定的数据对象
*/
let vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});

ES6的Proxy 数据劫持 Demo

在 Vue.js 3.0 开始,使用了ES6的Proxy来实现数据劫持。下面的 Demo 演示了如何使用Proxy来进行数据劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>双向数据绑定Demo</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
<br />
{{ text }}
</div>
<script>
/*
*@Time : 2024/05/03 10:59:55
*@Desc : 数据劫持 或者叫数据监听
*/
function observeProxy(obj) {
// 构造代理对象
debugger
let dep = new Dep();
const vm = new Proxy(obj, {
get(target, key) {
// 注册订阅者
if (Dep.target) {
dep.addSub(Dep.target);
}
return target[key];
},
set(target, key, value) {
if (value === target[key]) {
return true;
}
target[key] = value;
// 通知相关的订阅者进行更新
dep.notify();
return true;
}
});
console.log(1, "构造代理对象", vm)
return vm
}
/*
*@Time : 2024/05/03 10:48:55
*@Desc : 简化的虚拟DOM编译和更新的示例
*/
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;
console.log(3, "编译虚拟 Dom 节点")
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child);
}
// 返回虚拟 Dom
return flag;
}

/*
*@Time : 2024/05/03 10:52:42
*@Desc : 编译DOM节点
*/
function compile(node, vm) {
// 节点类型为元素
if (node.nodeType === 1) {
// 获取所有的属性
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取v-model绑定的属性名
// 添加 input 事件
node.addEventListener('input', function (e) {
// 给相应的 data 属性赋值,进而触发该属性的set方法
vm.vm[name] = e.target.value;
});
debugger
// 将data的值赋给该node,这里触发 getter 方法
node.value = vm.vm[name];
debugger
node.removeAttribute('v-model');
// 构造订阅者
new Watcher(vm, node, name, 'input');
}
}

}
let reg = /\{\{(.*)\}\}/;
// 节点类型为 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
// 构造订阅者
new Watcher(vm, node, name, 'text');
}
}
}
/*
*@Time : 2024/05/03 11:53:27
*@Desc : 订阅者
*/
function Watcher(vm, node, name, nodeType) {
// this为watcher函数

Dep.target = this;
console.log(this);
this.name = name;
this.node = node;
this.vm = vm.vm;
this.nodeType = nodeType;
this.update();
Dep.target = null;
}
Watcher.prototype = {
update () {
this.get();
// 如果是文本,直接更新 `nodeValue`
if (this.nodeType == 'text') {
debugger
this.node.nodeValue = this.value;
}
// 如果是输入标签,更新 value 的值
if (this.nodeType == 'input') {
debugger
this.node.value = this.value;
}
console.log(6.2, "通知 Dom", this.nodeType, "数据为", this.value)
},
// 获取 data 中的属性值
get () {
// 触发相应属性的 get
this.value = this.vm[this.name];
console.log(6.1, "获取", this.nodeType, "数据最新的值:", this.value)
}
}
/*
*@Time : 2024/05/03 11:48:38
*@Desc : 发布者
*/
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub (sub) {
this.subs.push(sub);
console.log(4, "suds:", this.subs.length, "注册订阅者:", sub)
},
notify () {
console.log(6, "通知订阅者:", this.subs)
this.subs.forEach((sub) => {
sub.update();
});
}
};

/*
*@Time : 2024/05/03 11:07:01
*@Desc :
*/

function Vue(options) {
this.data = options.data;
let data = this.data;
// 构造数据劫持
this.vm = observeProxy(data);

let id = options.el;
let dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将dom返回到app中进行挂载
document.getElementById(id).appendChild(dom);
}
/*
*@Time : 2024/05/03 11:09:06
*@Desc : 定义 Vue 对象,传递需要挂载的元素,双向绑定的数据对象
*/
let vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>

和最上面的 Demo 相比较,observeProxy 方法没有直接修改 VM 对象,Proxy 本身并没有提供一种方法来修改对象属性,所以这里返回一个代理对象Proxy 给了 VM 的 vm 属性,把 需要劫持的数据嵌套了一层放到了 VM 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Vue(options) {
this.data = options.data;
let data = this.data;
// 构造数据劫持
this.vm = observeProxy(data);
let id = options.el;
let dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将dom返回到app中进行挂载
document.getElementById(id).appendChild(dom);
}
function observeProxy(obj) {
// 构造代理对象
debugger
let dep = new Dep();
const vm = new Proxy(obj, {
get(target, key) {
// 注册订阅者
if (Dep.target) {
dep.addSub(Dep.target);
}
return target[key];
},
set(target, key, value) {
if (value === target[key]) {
return true;
}
target[key] = value;
// 通知相关的订阅者进行更新
dep.notify();
return true;
}
});
console.log(1, "构造代理对象", vm)
return vm
}

Object.defineProperty && Proxy API 介绍

Object.defineProperty

Object.definePropertyES5引入的一个特性,它允许我们将自定义的逻辑应用于对象的属性访问和修改。它可以定义一个新属性或修改现有属性,并定义属性的行为,例如读取(get)和写入(set)时的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {};

Object.defineProperty(obj, 'name', {
get() {
console.log('读取name属性');
return this._name;
},
set(value) {
console.log('设置name属性');
this._name = value;
}
});

obj.name = 'John'; // 输出:设置name属性
console.log(obj.name); // 输出:读取name属性和John

Proxy API

ProxyES6引入的另一个特性,它提供了对对象的拦截和自定义行为的能力。Proxy可以拦截对象上的各种操作,包括属性的读取、写入、函数调用等。通过Proxy,我们可以对对象的访问和修改进行自定义处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const obj = {
name: 'John'
};

const proxy = new Proxy(obj, {
get(target, key) {
console.log(`访问属性:${key}`);
return target[key];
},
set(target, key, value) {
console.log(`设置属性:${key} = ${value}`);
target[key] = value;
return true;
}
});

proxy.name = 'Jane'; // 输出:设置属性:name = Jane
console.log(proxy.name); // 输出:访问属性:name 和 Jane

简单比较

用法行为:

  • Object.defineProperty:需要逐个定义每个属性的行为,即显式地指定对象的某个属性需要进行拦截和处理。这需要修改现有的对象定义,使其符合拦截要求。这种操作是显式的,需要直接操作对象本身,并且需要事先知道要拦截的属性。后期的操作还是使用目标对象

  • Proxy:创建代理对象时需要提供一个处理器对象,该处理器对象定义了拦截器方法,用于拦截和处理各种操作。代理对象会完全地代理目标对象,并将所有操作转发给目标对象,因此无需修改目标对象本身。这种操作是隐式的,代理对象会在后台拦截和处理所有操作,而不需要直接操作目标对象。代理对象可以在外部对目标对象进行拦截和处理,而目标对象本身保持不变。后期的操作对象是代理对象,而不是目标对象.

拦截能力:

  • Object.defineProperty:主要用于拦截对象的属性读取和写入操作,也可以通过get和set定义一些自定义逻辑。它只能拦截属性级别的操作,无法拦截其他操作。
  • Proxy:具有更强大的拦截能力,可以拦截对象上的多种操作,包括属性的读取、写入、删除、函数调用等。可以通过代理对象的不同处理器方法来自定义拦截逻辑。

动态属性和删除属性:

  • Object.defineProperty:在对象创建后无法动态添加或删除拦截的属性。
  • Proxy可以动态添加和删除属性,并在拦截器中处理相应的操作。

兼容性:

  • Object.defineProperty:相对来说,较好地支持各种现代浏览器和旧版本浏览器,包括IE9+。
  • Proxy:较新的特性,不被所有旧版本浏览器支持,特别是在IE浏览器中不被支持。如果需要在不支持Proxy的环境中运行,需要使用其他解决方案或使用polyfill进行兼容处理。

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)


https://liruilong.blog.csdn.net/article/details/117675985


© 2018-至今 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

发布于

2023-09-09

更新于

2024-11-22

许可协议

评论
加载中,最新评论有1分钟缓存...
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×