双向数据绑定
可算是前端领域经久不衰的热词,不论是前端开发仍是面试都会有所涉及。并且不一样的框架也想尽一切办法去实现这一特性,好比:
Knockout / Backbone --- 发布-订阅模式
Angular --- ‘脏检查’
Vue --- 'Object.defineProperty'
html
那么
双向数据绑定
究竟是什么?没图说个卵,直接上图
简单的说就是在数据
和UI
之间创建双向的通讯通道,当用户经过Function改变了数据,那么这个改变也会当即反应到UI上;或者说用户经过UI的操做,那么这些操做也会随之引发对应的数据变更。emmmmmm...没毛病!前端
既然本文标题是讨论Object.defineProperty
,那么笔者就把当前火热的到Boom的国产框架:Vue.js
请出来,而后在了解完她实现双向数据绑定的原理以后,咱们着手实现一个抽象派的双向数据绑定。那么那位朋友就说了,什么叫 抽象派 ?我估计吧,可能就是马(Vue)和马骨架的区别吧,TAT...git
在介绍Vue的双向数据绑定以前,笔者还想多叨叨几句,若是某一天有人问你:Vue是如何实现双向数据绑定的?
姑且先在这里停顿下,思考下这个问题的答案...................
或许有朋友会脱口而出“数据劫持”
,说的没错!的确就是“数据劫持”,可是还不够充分和不够精确。笔者在这里也谈下本身的一点点所见所闻所想:面试
- 不够精确:与其说是 数据劫持,更应该说是对数据对象的
Setter
和Getter
实现的劫持。- 不够充分:为何说不够充分?是由于 Object.defineProperty 仅仅是实现了对数据的监控,后续实现对UI的从新渲染并非它作的,因此这里还涉及到
发布-订阅模式
(有兴趣的朋友戳这里);过程是,当监控的数据对象被更改后,这个变动会被广播给全部订阅该数据的watcher
,而后由该 watcher实现对页面的从新渲染。
下面进入正题,一块儿了解下Vue实现双向数据绑定的原理,果断上图:segmentfault
首先,Vue的Compile
模块会对Vue的 template 代码进行编译解析并生成一系列的watcher
,也能够称之为“更新函数”,它负责把变动后的相关数据从新渲染到指定的地方。举个栗子:数组
<input v-model="message">
Compile会解析出 v-moel
这个指令而且生成 watcher 并链接数据中的 message 和当前这个Dom对象,一旦收到这个message被变动的通知,watcher就会根据变动对这个Dom进行从新渲染。框架
固然一个页面或者一个项目中确定有不少watcher,所以Vue使用了Dep这个对象来存储每个watcher,当数据发生变动,Observer会调用Dep的notify方法以通知全部订阅了该数据的watcher,让它们醒醒该干活了...函数
Vue的双向数据绑定也说得差很少了,下面就开始顺着这个思路着手写一个吧,毕竟说得多不如code来得好啊!!!具体的实现效果以下,Let‘s do itui
不知道为何GIF上传不了,因此只能将就用图片了,QAQ....this
功能就用文字解释下:
第一个行的 title0
直接显示的是数据,以便观察;咱们能够在输入框中输入任何int, 而后点击 “加”能够实现对数值的 +1 操做,同时输入框的数值和 title 也会随之变化;固然,经过输入数值,title也会跟着变化。
首先把Html代码呈上来:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Object.defineProperty实现双向绑定</title> </head> <body> <h1 id='h1'></h1> <input type="text" id="inp" onkeyup="inputChange(event)"> <input type="button" value="加" onclick="btnAdd()" /> </body> <script src="./index.js"></script> </html>
而后开始一步一步在index.js
里写代码吧1)
首先咱们先定义一个数据源
//数据源 let vm = { value: 0 }
2)
而后定义一个Dep
,用于存储watcher
//用于管理watcher的Dep对象 let Dep = function () { this.list = []; this.add = function(watcher){ this.list.push(watcher) }, this.notify = function(newValue){ this.list.forEach(function (fn) { fn(newValue) }) } };
3)
模拟Compile出来的watchers,该demo涉及到两个地方的从新render,一个是title,另外一个是输入框。因此写两个watcher,而后存入Dep
// 模拟compile,经过对Html的解析生成一系列订阅者(watcher) function renderInput(newValue) { let el = document.getElementById('inp'); if (el) { el.value = newValue } } function renderTitle(newValue) { let el = document.getElementById('h1'); if (el) { el.innerHTML = newValue } } //将解析出来的watcher存入Dep中待用 let dep = new Dep(); dep.add(renderInput); dep.add(renderTitle)
4)
使用 Object.defineProperty 定义一个Observer
function observer(vm, key, value) { Object.defineProperty(vm, key, { enumerable: true, configurable: true, get: function () { console.log('Get'); return value }, set: function (newValue) { if (value !== newValue) { value = newValue console.log('Update') //将变更通知给相关的订阅者 dep.notify(newValue) } } }) }
5) 再将页面使用的两个方法写出来。(Vue使用的是指令对事件进行绑定,可是本文不涉及指令,因此用最原始的方法绑定事件)
//页面引用的方法 function inputChange(ev) { let value = Number.parseInt(ev.target.value); vm.value = (Number.isNaN(value)) ? 0 : value; } function btnAdd() { vm.value = vm.value + 1; }
主要的代码都写好后,下面第一件事就是初始化
:
//数据初始化方法 function initMVVM(vm) { Object.keys(vm).forEach(function (key) { observer(vm, key, vm[key]) }) } //初始化数据源 initMVVM(vm) //初始化页面,将数据源渲染到UI dep.notify(vm.value);
这样一个简单的基于 Object.defineProperty
的双向数据绑定就完成了。看完的朋友有没有对双向数据绑定有了更多的理解了呢?若是没有理解的话,能够将代码复制到本地,而后循着代码再运行下,或许能容易理解。固然这里的代码并不高深,只是从浅层去谈论了双向数据绑定,因此有不足或者表达错误的地方,烦请各位朋友多多指正。
这里是源码,因为放不了动图,因此有兴趣的小伙伴能够拿下来
最后仍是补充一句,Object.defineProperty
虽然好用,但并非无懈可击的,它对数组数据
的处理并无想象中的好甚至表现不好,所以Vue团队专门为Vue中的数组类型编写了额外的方法以实现对数组的正确监控
。所以,ES6中的Proxy自告奋勇,拯救了ES5 中 Object.defineProperty
对数组数据处理的不足。有兴趣的朋友请期待笔者的下一篇博客,讨论下用Proxy
实现双向数据绑定。
我们下期再见!!