对每个人而言,真正的职责只有一个:找到自我。然后在心中坚守其一生,全心全意,永不停息。所有其它的路都是不完整的,是人的逃避方式,是对大众理想的懦弱回归,是随波逐流,是对内心的恐惧 ——赫尔曼·黑塞《德米安》
写在前面
很早的一篇博客,整理了部分,蹭假期整理完
博文内容涉及:
双向数据绑定 实现方式简单介绍
基于发布订阅
、数据劫持
的双向数据绑定
两种不同实现(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”事件),而所有订阅了这个事件的视图都会收到通知,并更新自己以反映新的数据。
这种模式允许数据模型和视图之间实现松散的耦合,因为它们之间不需要直接通信;它们只需要知道如何发布和监听事件即可。此外,这种模式还具有良好的可扩展性,因为你可以轻松地添加新的发布者或订阅者,而无需修改现有的代码。
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 > function observe (obj, vm ) { Object .keys(obj).forEach((key ) => { console .log(1 , "劫持的数据对象为" , key, "值为" , obj[key]) defineReactive(vm, key, obj[key]); }); } function defineReactive (obj, key, val ) { let dep = new Dep(); console .log(2 , "创建发布者:" , dep) Object .defineProperty(obj, key, { enumerable : true , configurable : true , 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(); } }); } function nodeToFragment (node, vm ) { var flag = document .createDocumentFragment(); var child; console .log(3 , "编译虚拟 Dom 节点" ) while (child = node.firstChild) { compile(child, vm); flag.appendChild(child); } return flag; } 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; node.addEventListener('input' , function (e ) { vm[name] = e.target.value; debugger }); node.value = vm[name]; node.removeAttribute('v-model' ); new Watcher(vm, node, name, 'input' ); } } } let reg = /\{\{(.*)\}\}/ ; if (node.nodeType === 3 ) { if (reg.test(node.nodeValue)) { var name = RegExp .$1; name = name.trim(); new Watcher(vm, node, name, 'text' ); } } } function Watcher (vm, node, name, nodeType ) { Dep.target = this ; this .name = name; this .node = node; this .vm = vm; this .nodeType = nodeType; this .update(); Dep.target = null ; } Watcher.prototype = { update ( ) { this .get(); if (this .nodeType == 'text' ) { this .node.nodeValue = this .value; } if (this .nodeType == 'input' ) { this .node.value = this .value; } console .log(6.2 , "通知 Dom" , this .nodeType, "数据为" , this .value) }, get ( ) { this .value = this .vm[this .name]; console .log(6.1 , "获取" , this .nodeType, "数据最新的值:" , this .value) } } 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(); }); } }; 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 ); document .getElementById(id).appendChild(dom); } 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 , 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); } 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; node.addEventListener('input' , function (e ) { vm[name] = e.target.value; debugger }); node.value = vm[name]; node.removeAttribute('v-model' ); new Watcher(vm, node, name, 'input' ); } } } let reg = /\{\{(.*)\}\}/ ; 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 ) { Dep.target = this ; this .name = name; this .node = node; this .vm = vm; this .nodeType = nodeType; this .update(); Dep.target = null ; } Watcher.prototype = { update () { this .get(); if (this .nodeType == 'text' ) { this .node.nodeValue = this .value; } if (this .nodeType == 'input' ) { this .node.value = this .value; } console .log(6.2 ,"通知 Dom" ,this .nodeType, "数据为" ,this .value) }, 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 ); document .getElementById(id).appendChild(dom); } 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 > 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 } function nodeToFragment (node, vm ) { var flag = document .createDocumentFragment(); var child; console .log(3 , "编译虚拟 Dom 节点" ) while (child = node.firstChild) { compile(child, vm); flag.appendChild(child); } return flag; } 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; node.addEventListener('input' , function (e ) { vm.vm[name] = e.target.value; }); debugger node.value = vm.vm[name]; debugger node.removeAttribute('v-model' ); new Watcher(vm, node, name, 'input' ); } } } let reg = /\{\{(.*)\}\}/ ; if (node.nodeType === 3 ) { if (reg.test(node.nodeValue)) { var name = RegExp .$1; name = name.trim(); new Watcher(vm, node, name, 'text' ); } } } function Watcher (vm, node, name, nodeType ) { 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(); if (this .nodeType == 'text' ) { debugger this .node.nodeValue = this .value; } if (this .nodeType == 'input' ) { debugger this .node.value = this .value; } console .log(6.2 , "通知 Dom" , this .nodeType, "数据为" , this .value) }, get () { this .value = this .vm[this .name]; console .log(6.1 , "获取" , this .nodeType, "数据最新的值:" , this .value) } } 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(); }); } }; 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 ); document .getElementById(id).appendChild(dom); } 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 ); 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)时的操作。
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' ; console .log(obj.name);
Proxy API Proxy
是ES6
引入的另一个特性,它提供了对对象的拦截和自定义行为的能力。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' ; console .log(proxy.name);
简单比较 用法行为:
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)