写在前面
- 很早的一篇博客,整理了部分,蹭假期整理完
- 博文内容涉及:
- 双向数据绑定 实现方式简单介绍
- 基于
发布订阅
、数据劫持
的双向数据绑定
两种不同实现(ES5/ES6
) Demo,以及代码简单分析 Object.defineProperty
&&Proxy API
介绍以及特性对比- 理解不足小伙伴帮忙指正 :),生活加油
对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》
双向数据绑定介绍
在前端框架中,特别是响应式框架(如Vue.js
, Angular
等)中,双向数据绑定(Two-way data binding
)是一个核心特性,它允许开发者在UI和数据
之间建立直接的、双向的联系(MVVM
)。下面是一些实现双向数据绑定的常见做法:
脏值检查(Dirty Checking)
脏值检查是一种简单的双向数据绑定策略。它周期性地检查数据模型(Model)
是否发生了变化,如果发生了变化,则更新视图(View)
。脏值检查通常涉及一个“检查周期
”或“轮询间隔
”,在这个间隔内,框架会遍历所有绑定,并检查是否有任何变化。
然而,脏值检查并不高效,因为它可能需要对整个数据模型进行不必要的遍历,即使数据实际上并没有改变。此外,它也不能立即反映变化,因为它依赖于轮询间隔。
数据劫持(Data Interception)
数据劫持(也称为数据代理或对象劫持)是一种更高效的双向数据绑定策略。它依赖于JavaScript
的 Object.defineProperty()
方法(在ES5中引入),该方法允许你定义或修改对象的属性,包括getter和setter方法。
在 Vue.js
的早期版本中,当一个对象被用作数据模型时,Vue 会遍历它的所有属性,并使用 Object.defineProperty()
将它们转化为getter/setter,以便在数据变化时能够立即感知到。当视图需要读取数据模型时,getter方法会被调用;当视图需要更新数据模型时,setter方法会被调用,并且可以在这里触发视图的更新。
从 Vue.js 3.0
开始,引入了更高效的响应式系统,称为Proxy-based reactive system
。Vue.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
库(它类似于数据劫持,但工作方式略有不同)来实现类似的功能。
MVVM
Object.defineProperty 数据劫持 Demo
下面的 Demo 简化了 Vue.js
实现,通过数据劫持、订阅者和发布者的机制
,实现了将数据和DOM节点进行绑定,并在数据变化时自动更新相关的DOM节点,从而实现了简单的双向数据绑定功能。
<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
,从而实现对属性的劫持和监视。
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中更新属性的值并通知相关的订阅者进行更新。
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
函数来解析和编译
节点。
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()
方法,实现对所有订阅的通知
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属性。在第一次获取值的时候会进行订阅者注册
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
方法中通知所有订阅者进行更新。
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
,并将其挂载到指定的元素上。
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来进行数据劫持
:
<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 对象。
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.defineProperty
是ES5
引入的一个特性,它允许我们将自定义的逻辑应用于对象的属性访问和修改。它可以定义一个新属性或修改现有属性,并定义属性的行为,例如读取(get)和写入(set)时的操作。
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
Proxy
是ES6
引入的另一个特性,它提供了对对象的拦截和自定义行为的能力。Proxy
可以拦截对象上的各种操作,包括属性的读取、写入、函数调用等。通过Proxy
,我们可以对对象的访问和修改进行自定义处理
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