双向数据绑定
可算是前端领域经久不衰的热词,不管是前端开发还是面试都会有所涉及。而且不同的框架也想尽一切办法去实现这一特性,比如:
Knockout / Backbone --- 发布-订阅模式
Angular --- ‘脏检查’
Vue --- 'Object.defineProperty'
那么
双向数据绑定
到底是什么?没图说个卵,直接上图
简单的说就是在数据
和UI
之间建立双向的通信通道,当用户通过Function改变了数据,那么这个改变也会立即反应到UI上;或者说用户通过UI的操作,那么这些操作也会随之引起对应的数据变动。emmmmmm...没毛病!
既然本文标题是讨论Object.defineProperty
,那么笔者就把当前火热的到Boom的国产框架:Vue.js
请出来,然后在了解完她实现双向数据绑定的原理之后,我们着手实现一个抽象派的双向数据绑定。那么那位朋友就说了,什么叫 抽象派 ?我估计吧,可能就是马(Vue)和马骨架的区别吧,TAT...
在介绍Vue的双向数据绑定之前,笔者还想多叨叨几句,如果某一天有人问你:Vue是如何实现双向数据绑定的?
姑且先在这里停顿下,思考下这个问题的答案...................
或许有朋友会脱口而出“数据劫持”
,说的没错!的确就是“数据劫持”,但是还不够充分和不够精确。笔者在这里也谈下自己的一点点所见所闻所想:
- 不够精确:与其说是 数据劫持,更应该说是对数据对象的
Setter
和Getter
实现的劫持。- 不够充分:为什么说不够充分?是因为 Object.defineProperty 仅仅是实现了对数据的监控,后续实现对UI的重新渲染并不是它做的,所以这里还涉及到
发布-订阅模式
(有兴趣的朋友戳这里);过程是,当监控的数据对象被更改后,这个变更会被广播给所有订阅该数据的watcher
,然后由该 watcher实现对页面的重新渲染。
下面进入正题,一起了解下Vue实现双向数据绑定的原理,果断上图:
首先,Vue的Compile
模块会对Vue的 template 代码进行编译解析并生成一系列的watcher
,也可以称之为“更新函数”,它负责把变更后的相关数据重新渲染到指定的地方。举个栗子:
Compile会解析出 v-moel
这个指令并且生成 watcher 并连接数据中的 message 和当前这个Dom对象,一旦收到这个message被变更的通知,watcher就会根据变更对这个Dom进行重新渲染。
当然一个页面或者一个项目中肯定有很多watcher,因此Vue使用了Dep这个对象来存储每一个watcher,当数据发生变更,Observer会调用Dep的notify方法以通知所有订阅了该数据的watcher,让它们醒醒该干活了...
Vue的双向数据绑定也说得差不多了,下面就开始顺着这个思路着手写一个吧,毕竟说得多不如code来得好啊!!!具体的实现效果如下,Let‘s do it
不知道为什么GIF上传不了,所以只能将就用图片了,QAQ....
功能就用文字解释下:
第一个行的 title0
直接显示的是数据,以便观察;我们可以在输入框中输入任何int, 然后点击 “加”可以实现对数值的 +1 操作,同时输入框的数值和 title 也会随之变化;当然,通过输入数值,title也会跟着变化。
首先把Html代码呈上来:
Object.defineProperty实现双向绑定
然后开始一步一步在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
实现双向数据绑定。
咱们下期再见!!