MVVM基础之双向绑定原理

双向绑定的实现原理

到处找了下数据双向绑定的资料。这里跑回来总结一下双向绑定实现的方式:

  1. 依赖ES5的Object.defineProperty方法构成的观察者模式(订阅发布模式|Pub/Sub)
  2. 类似AngularJS的脏值检查模式

观察者模式

有关观察者模式这里基本引用来自汤姆大叔的博客,需要了解更多可以前往观摩

观察者模式又叫发布订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

观察者模式的优点(更多详情可以看:

  • 支持简单的广播通信,自动通知所有已经订阅过的对象。
  • 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
  • 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。

在javascript开发过程中,因为javascript的特殊性,在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式。

在MVVM这里,这个订阅发布路径基本是这样的:

  1. Data to View(UpdateView订阅了Model的change事件)
    a. 首先设置一个UpdateView函数,传入Data就更新View
    b. Object.defineProperty拦截对象的get和set参数,当发生属性变化时候额外触发UpdateView更新View

  2. View to Data(UpdateModel订阅了元素的press、change之类事件)
    a. 绑定事件 触发时候修改Model,设这个动作叫做UpdateModel
    b. 触发后进行数据更改

换个通俗的说法就是:Data在改变触发回调时候更新View,而View改动后触发回调更新Data。这里使用观察者模式有两个目的:

  1. 解耦:目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用
  2. 1对多:一个数据可能在一个View里面多处使用。

观察者模式其实就这么点了。View2Data随便干过点js开发的人都会理解这个事件触发机制。整个MVVM双向绑定思路里面相对麻烦的其实是Data2View。

当Dom修改时候会有click,change,pressdown等事件可以响应、捕捉。

但是Data数据修改时候却没有什么时机可以用来捕捉以触发一个回调。

这个时候Object.defineProperty就成了唯一的救命稻草——我们在通过这个方法劫持get和set,对它进行了『覆写』,并在这些个覆写的函数里面手动调用相应的回调——事实上它的机制就是那么笨拙,只是把它写到set、get里面自动调用不需要你手动调用了而已,并且模拟了一个change事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 拦截 object 的 prop 属性的 get 和 set 方法
Object.defineProperty(object, prop, {
get: function() {
return this.getValue(object, prop);
},
set: function(newValue) {
var oldValue = this.getValue(object, prop);
if (newValue !== oldValue) {
this.setValue(object, newValue, prop);
// 触发变化回调
this.triggerChange(prop, newValue, oldValue);
}
}
});

脏值检查模式

脏检查即一种不关心你如何以及何时改变的数据,只关心在特定的检查阶段数据是否改变的数据监听技术。

首先说下脏检查模式和观察者模式最大的区别:
就像之前已经说过了,如果不使用Object.defineProperty那么就无法在值发生改变时候触发动作。观察者模式使用Object.defineProperty对get和set进行了覆写,但是脏检查模式没有这样做。这意味它的操作不是被动调用,而是“主动”进入。

这个被动与主动,就是观察者模式,和脏检查模式最根源的区别。

脏检查如何进行观察对象的对比和更新,这是一个处理细节,在原理阶段可以不深究。

更加关心的是,这个主动进入,是遵循怎样的方式。这里细节相对多,以angularJS为例:

  1. DOM事件,譬如用户输入文本,点击按钮等。(ng-click)
  2. XHR响应事件 ($http)
  3. 浏览器Location变更事件 ($location)
  4. Timer事件($timeout, $interval)
  5. 执行$digest()或$apply()

这里有写地方需要重点突出:

  1. 脏检测不是心跳检测。它不会轮询检查数据
  2. 数据变化时候默认不会有回调被调用——如果你用过$apply()就知道。它是基于上诉时机触发的。
  3. 数据数组的splice等操作会触发Data2View,这是因为数组的方法被重写了。这点同之前一样。

当然,脏检测同观察者模式相比也是有优点的:

1.脏检查完全不关心你改变数据的方式,而常规的set, get的方式则会强加许多限制
2.脏检查可以实现批处理完数据之后,再去统一更新view.
3.脏检查其实比 GET/SET 更容易实现。脏检查是个单向的检查流程(请不要和双向绑定发生混淆),可以实现_任意复杂度的表达式支持。而get/set的方式则需要处理复杂依赖链,基本上表达式支持都是阉割的(使用时就像踩雷).
——摘自<<脏检查: 数据绑定的秘密>>

总结

暂时收尾一下,MVVM必然是个大话题,但是这里首先主要总结原理,指明下一步研究方向。到此数据绑定的原理基本梳理了一遍。此文暂且收结。

参考文章:

深入理解JavaScript系列(32):设计模式之观察者模式
利用 JavaScript 数据绑定实现一个简单的 MVVM 库
脏检查: 数据绑定的秘密