Object.observe()带来的数据绑定变革
一场变革即将到来。一项Javascript中的新特性将会改变你对于数据绑定的所有认识。它也将改变你所使用的MVC库观察模型中发生的修改以及更新的实现方式。你会看到,那些所有在意属性观察的应用性能将会得到巨大的提升。
我们很高兴的看到,Object.observe()已经正式加入到了Chrome 36 beta版本中。
Object.observe()是未来ECMAScript标准之一,它是一个可以异步观察Javascript中对象变化的方法,而无需你去使用一个其他的JS库。它允许一个观察者接收一个按照时间排序的变化记录序列,这个序列描述的是一列被观察的对象所发生的变化。
var model = {}; // 然后我们对他进行观察 Object.observe(model, function(changes){ // 这个异步毁掉函数将会运行 changes.forEach(function(change) { // 让我们获知变化 console.log(change.type, change.name, change.oldValue); }); });
通过使用Object.observe(),你可以不需要使用任何框架就能实现双向数据绑定。
但这并不意味着你就不应该使用一个框架。对于一些有着复杂业务逻辑的项目,经过精心设计的框架的重要性不言而喻,你应该继续使用它们。这些框架 减轻了开发新手的工作量,而且只需要编写很少的代码就能够维护和实现一些模式并达到我们想要的目的。如果你不需要一个框架,你可以使用一个体积更小,针对 性更强的库,比如Polymer(它已经开始使用Object.observe()了)。
即便你已经重度依赖于一个MV*框架,Object.observe()任然能为你的应用带来一些性嫩各方面的提升,它能够更快更简单的实现一 些功能并维持同样的API。例如,在Angular的一个Benchmark测试中,对于一个Model中发生的变化,脏值检查对每次更新会花费 40ms,而Object.observe()只会花费1-2ms(相当于20-40倍的性能提升)。
介绍Object.observe()
我们真正想要的可能是两个世界中最好的东西 — 一种支持对原生数据对象(普通JavaScript对象)进行观察的方法,同时不需要每次都对所有东西进行脏值检查。它需要有良好的算法表现。它还需要能 够很好的整合到各个平台中。这些都是Object.observe()能够带给我们的东西。
它允许我们对一个对象或者变异属性进行观察,并且在变化发生时得到及时通知。但是我们在这里不想看什么理论,让我们来看看代码!
Object.observe()和Object.unobserve()
让我们假设我们现在有一个简单的JavaScript对象,它代表一个模型:
var todoModel = { label: 'Default', completed: false };
我们可以制定一个比回调函数,用来处理对象上的变化:
function observer(changes){ changes.forEach(function(change, i){ console.log('what property changed? ' + change.name); console.log('how did it change? ' + change.type); console.log('whats the current value? ' + change.object[change.name]); console.log(change); // 所有的变化 }); }
注意:当观察者回调函数被调用时,被观察的对象可能已经发生了多次改变,因此对于每一次变化,新的值(即每次变化以后的值)和当前值(最终的值)并不一定是相同的。
我们可以使用Object.observe()来观察这些变化,只要将对象作为第一个参数,而将回调函数作为第二个参数:
Object.observe(todoModel, observer);
在任何观察系统中,总是存在一个方法来停止观察。在这里,我们有Object.unobserve()方法,它的用法和Object.observe()一样但是可以像下面一样被调用:
Object.unobserve(todoModel, observer);
指定感兴趣的变化
现在我们已经了解到了我们如何去获取一个被观察对象的变化列表。但是如果我们仅仅只对一个对象中的某些属性感兴趣该怎么办?人人都需要一个垃圾 邮件过滤器。Observer可以通过一个列表指定一些我们想要看到的变化。我们需要通过Object.observe()的第三个参数来指定:
Object.observe(obj, callback, opt_acceptList)
现在我们来看一个如何使用的例子:
var todoModel = { label: 'Default', completed: false }; // 指定一个回调函数 function observer(changes){ changes.forEach(function(change, i){ console.log(change); }) } // 指定一个我们想要观察的变化类型 Object.observe(todoModel, observer, ['delete']); todoModel.label = 'Buy some milk'; // 注意到变化没有被报告
如果你不指定一个列表,它默认将会报告“固有的”对象变化类型 (“add”, “update”, “delete”, “reconfigure”, “preventExtensions” (丢与那些不可扩展的对象是不可观察的))。
通知
Object.observe()也带有一些通知。它们并不像是你在手机上看到了通知,而是更加有用。通知和变异观察者比较类似。它们发生在微任务的结尾。在浏览器的上下文,它几乎总是位于当前事件处理器的结尾。
这个时间点非常的重要因为基本上来说此时一个工作单元已经结束了,现在观察者已经开始它们的共走了。这是一个非常好的回合处理模型。
使用一个通知器的工作流程如下所示:
现在我们通过一个例子来如何通过自定义一个通知器来处理一个对象的属性被设置或者被获取的情况。注意看代码中的注释:
// 定义一个简单的模型 var model = { a: {} }; // 定义一个单独的变量,我们即将使用它来作为我们的模型中的getter var _b = 2; // 在'a'下面定义一个新的属性'b',并自定义一个getter和setter Object.defineProperty(model.a, 'b', { get: function () { return _b; }, set: function (b) { // 当'b'在模型中被设置时,注意一个特定类型的变化将会发生 // 这将给你许多关于通知的控制器 Object.getNotifier(this).notify({ type: 'update', name: 'b', oldValue: _b }); // 在值发生变化时将会输出信息 console.log('set', b); _b = b; } }); // 设置我们的观察者 function observer(changes) { changes.forEach(function (change, i) { console.log(change); }) } // 开始观察model.a Object.observe(model.a, observer);
现在当数据属性发生变化时(‘update’)我们将会得到报告。以及任何对象的实现也将会被报告(notifier.notifyChange())。
多年的web平台开发经验告诉我们整合方法是你应该最先尝试的事情,因为它最容易去实现。但是它存在的问题是以它会创造一个从根本上来看就很未下的 处理模型。如果你正在编写代码并且更新了一个对象的属性,你实际上并不想陷入这样一种困境:更新模型中的属性会最终导致任意一段代码去做任意一件事情。当 你的函数正好运行到一半时,假设失效并不是什么理想的状况。
如果你是一个观察者,你并不想当某人正在做某事的时候被调用。你并不像在不连续的状态下被调用。因为这最终往往会导致更多的错误检查。你应该试着去容忍更多的情形,并且基本上来说它是一个很难去合作的模型。异步是一件更难处理的事情但是最终它会产生更好的模型。
上述问题的解决办法是变化合成记录(synthetic change records)。
变化合成记录
基本上来说,如果你想要存取器或者计算属性的话,你应该复杂在这些值发生改变时发出通知。这会导致一些额外的工作,但是它是这种机制第一类的特征,并且这些通知会连同来自余下的底层数据对象的通知一起被发布出来。
观察存取器或者计算属性的问题可以通过使用notifier.notify来解决 — 它也是Object.observe()的另外一部分。大多数的观察系统想要某些形式的观察导出值。有很多方法可以实现它。 Object.observe()并没有用“正确的”方式进行判断。计算属性应该是存取器,当内部的(私有的)状态发生改变时它应该发出通知。
再一次声明,在web中应该有一些库来帮助我们进行通知并且帮助我们更好的实现计算属性(以及减少模板的使用)。
我们在这里会假设一个例子,这个例子中有一个circle类。在这里,我们有一个citcle,它有一个radius属性。在这里的情形 中,radius是一个存取器,并且当它的值发生变化时它实际上会去通知自己值已经发生变化了。这些通知将会连同其他变化被传递到这个对象或者其他对象。 本质上来说,如果你正在实现一个对象,你一定会想要拥有整合或者计算属性的对象,或者你想要想出一个策略如何让它运行。一旦你做了这件事,它将会适应你的 整个系统。
看看下面的代码在开发者工具中是如何运行的:
function Circle(r) { var radius = r; var notifier = Object.getNotifier(this); function notifyAreaAndRadius(radius) { notifier.notify({ type: 'update', name: 'radius', oldValue: radius }) } Object.defineProperty(this, 'radius', { get: function() { return radius; }, set: function(r) { if (radius === r) return; notifyAreaAndRadius(radius); radius = r; } }); Object.defineProperty(this, 'area', { get: function() { return Math.pow(radius, 2) * Math.PI; }, set: function(a) { r = Math.sqrt(a)/Math.PI; notifyAreaAndRadius(radius); radius = r; } }); } function observer(changes){ changes.forEach(function(change, i){ console.log(change); }) }
存取器属性
在这里我们对于存取器属性有一个简短的提示。在前面我们提到了对于数据属性来说只有值得变化是能够被观察到的。而存取器属性和计算属性则无法被观察到。这是因为JavaScript中的存取器并没有真正的值的变化。一个存取器仅仅是一个函数集合。
如果你为一个存取器属性赋值,你仅仅只是调用了这个函数,并且在它看来值并没有发生变化。它仅仅只是让一些代码运行起来。
这里的问题在于我们在上面的例子中将存取器属性赋值为5.我们应该能够知道这里究竟发生了什么。这实际上是一个未解决的问题。这个例子说明了原 因。对任何系统来说知道这究竟意味着什么是不可能的,因为在这里可以运行任意代码。每当存取器属性被访问时,它的值都会发生改变,因此询问它什么时候会发 生变化并没有多大的意义。
使用一个回调函数观察多个对象
Object.observe()上的另一个模式是使用单个回调观察者。这允许我们使用同一个回调函数堆多个不同的对象进行观察。这个回调函数在“微任务”的结尾将会把所有的变化都传递给它所观察的对象。
大规模的变化
也许你正在编写一个非常大的应用,并且经常需要处理大规模的变化。此时我们希望用一种更加紧凑的方式来描述影响很多属性的语义变化。
Object.observe()使用两个特定的函数来解决这个问题:notifier.performChange()以及notifier.notify(),我们在上面已经介绍过这两个函数了。
我们可以从下面的例子中看到我们如何来描述大规模变化,在这个例子中定义了一个叫做Thingy的对象,其中包含几个数计算功能 (multiply, increment, incrementAndMultiply)。只要其中一个功能被使用,它就会告诉系统一些包含特定变化的事情发生了。
例如:
notifier.performChange('foo', performFooChangeFn)
function Thingy(a, b, c) { this.a = a; this.b = b; } Thingy.MULTIPLY = 'multiply'; Thingy.INCREMENT = 'increment'; Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply'; Thingy.prototype = { increment: function(amount) { var notifier = Object.getNotifier(this); // 告诉系统一系列事情包含一个特定的变化。例如: // notifier.performChange('foo', performFooChangeFn); // notifier.notify('foo', 'fooChangeRecord'); notifier.performChange(Thingy.INCREMENT, function() { this.a += amount; this.b += amount; }, this); notifier.notify({ object: this, type: Thingy.INCREMENT, incremented: amount }); }, multiply: function(amount) { var notifier = Object.getNotifier(this); notifier.performChange(Thingy.MULTIPLY, function() { this.a *= amount; this.b *= amount; }, this); notifier.notify({ object: this, type: Thingy.MULTIPLY, multiplied: amount }); }, incrementAndMultiply: function(incAmount, multAmount) { var notifier = Object.getNotifier(this); notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() { this.increment(incAmount); this.multiply(multAmount); }, this); notifier.notify({ object: this, type: Thingy.INCREMENT_AND_MULTIPLY, incremented: incAmount, multiplied: multAmount }); } }
我们可以为我们的对象定义两个观察者: 一个用来捕获所有的变化,另一个将只会汇报我们定义的特定类型的变化 (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY)。
var observer, observer2 = { records: undefined, callbackCount: 0, reset: function() { this.records = undefined; this.callbackCount = 0; } }; observer.callback = function(r) { console.log(r); observer.records = r; observer.callbackCount++; }; observer2.callback = function(r){ console.log('Observer 2', r); }; Thingy.observe = function(thingy, callback) { // Object.observe(obj, callback, opt_acceptList) Object.observe(thingy, callback, [Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY, 'update']); }; Thingy.unobserve = function(thingy, callback) { Object.unobserve(thingy); }
我们现在可以开始玩弄一下代码了。我们先定义一个新的Thingy:
var thingy = new Thingy(2,4);
对它进行观察并进行一些变化。有趣的事情发生了!
// 观察thingy Object.observe(thingy, observer.callback); Thingy.observe(thingy, observer2.callback); // 把玩一下thing暴露的方法 thingy.increment(3); // { a: 5, b: 7 } thingy.b++;// { a: 5, b: 8 } thingy.multiply(2);// { a: 10, b: 16 } thingy.a++;// { a: 11, b: 16 } thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
位于这个“perform function”中的一切东西都可以被看作是“大型变化”进行的工作。接受“大型变化”的观察者仅仅只会接受“大型变化”的记录。那些不会接受底层变化的观察者都来源于“perform function”所做的事。
观察数组
我们已经讨论了如何观察一个对象,但是应该如何观察数组呢?
Array.observe()是一个针对自身大型变化的方法 — 例如 — splice,unshift或者任何能够隐式影响数组长度的东西。在内部它使用了Internally it uses notifier.performChange(“splice”,…)。
下面是一个我们如何观察一个模型“数组”的例子,当底层数据发生一些变化时,我们将能够得到一个变化的列表。
var model = ['Buy some milk', 'Learn to code', 'Wear some plaid']; var count = 0; Array.observe(model, function(changeRecords) { count++; console.log('Array observe', changeRecords, count); }); model[0] = 'Teach Paul Lewis to code'; model[1] = 'Channel your inner Paul Irish';
性能
考虑Object.observe()性能的方式是将它想成读缓存。基本上来说,在以下几种情形中,一个缓存是最佳选择(按照重要性排序):
- 读的频率决定着写的频率
- 你可以创造一个缓存,它可以在读数据期间将涉及到写数据的操作进行算法上的优化
- 写数据减慢的时间常数是可以接受的
Object.observe()是为上述第一种情形设计的。
脏值检查需要保留一个你所要观察数据的副本。这意味着在脏值检查中你需要一个额外的结构内存开销。脏值检查,一个作为权宜之计的解决方案,同时根本上也是一个脆弱的抽象,它可能会导致应用中一些不必要的复杂性。
脏值检查在任何数据可能发生变化的时候都必须要运行。这很明显并不是一个非常鲁棒的方法,并且任何实现脏值检查的途径都是有缺陷的(例如,在轮 询中进行检查可能会造成视觉上的假象以及涉及到代码的紊乱情况)。脏值检查也需要注册一个全局的观察者,这很可能会造成内存泄漏,而 Object.observe()会避免这一点。
为Object.observe()提供垫片
Object.observe()现在已经可以在Chrome 36 beta中使用,但是如果我们想要在其他浏览器中使用它该怎么办?Polymer中的Observe-JS是一个针对于那些没有原生实现 Object.observe()浏览器的一个垫片,但是它不仅仅是作为垫片,同时也包含了许多有用的语法糖。它提供了一种整合的视角,它能够将所有变化 总结起来并且提交一份关于变化的报告。它的好处主要体现在两点:
- 你可以观察路径。这意味着你可以说,我想要从一个给定的对象中观察’foo.bar.baz’,只要这个路径的值发生了改变,你会得到通知。如果路径是错误的,将会返回undefined。
下面是一个例子:
var obj = { foo: { bar: 'baz' } }; var observer = new PathObserver(obj, 'foo.bar'); observer.open(function(newValue, oldValue) { // 针对于 obj.foo.bar 已经改变的值进行响应 });
- 它能够告诉你数组的拼接。数组拼接基本上来说是你为了将旧版本数组转换为新版本数组是需要进行了最基本的拼接操作。这是一种转换的类型或者是这个数组的不同视图。它是你想要将数组从旧状态变为新状态时需要进行的最基本的工作。
下面是一个例子:
var arr = [0, 1, 2, 4]; var observer = new ArrayObserver(arr); observer.open(function(splices) { // 响应arr元素的变化 splices.forEach(function(splice) { splice.index; // 变化发生的位置 splice.removed; // 一个代表被移除的元素的序列值数组 splice.addedCount; // 被插入元素的个数 }); });
框架和Object.observe()
正如上面所提到的,使用Object.observe()能够给予框架和库中关于数据绑定的性能巨大的提升。
来自Ember的Yehuda Katz和Erik Bryn已经确定将会在Ember最近的修改版本中添加对Object.observe()的支持。来自Angular的Misko Hervy写了一份关于Angular 2.0的设计文档,其中的内容关于改善变化探测(change detection)。在将来,当Object.observe()在Chrome稳定版中出现时,Angular会使用 Object.observe()来实现变化探测的功能,在此之前它们会选择使用Watchtower.js — Angular自己的变化探测的实现方式。实在是太令人激动了。
总结
Object.observe()是一个添加到web平台上非常强大的特性,你现在就可以开始使用它。
我们希望这项特征能够及时的登陆到更多的浏览器中,它能够允许JavaScript框架从本地对象观察的能力中获得更多性能上的提升。Chrome 36 beta及其以上的版本都能使用这项特性,在未来Opera发布的版本中这项特性也会得到支持。
现在就和JavaScript框架作者谈谈Object.observe()如何能够提高他们框架中数据绑定的性能。未来还有更多让人激动的时刻。
关键词: javascript,object,observe 编辑时间: 2015-07-26 11:44:29
10
高兴9
支持9
搞笑10
不解9
谎言9
枪稿9
震惊9
无奈9
无聊9
反对9
愤怒
- 暂无评论
网友评论