注意:本文档适用于Knockout 3.4.0及更高版本。 对于先前版本,“延迟更新”插件提供类似的支持。
在复杂的应用程序中,由于具有多个交织的依赖关系,更新单个observable可能会触发计算的observable,手动订阅和UI绑定更新的级联。 如果将不必要的中间值推送到视图或产生额外的计算的可观察评估,则这些更新可能是昂贵的和低效的。 即使在简单的应用程序中,更新相关的可观察量或单个可观察的多次(例如填充可观察的数组)也可以具有类似的效果。
使用延迟更新可确保仅在计算的可观察量和绑定的依赖性稳定后才更新。 即使可观察者可能经历多个中间值,也只使用最新的值来更新其依赖性。 为了方便这一点,所有通知变为异步,使用Knockout微任务队列进行调度。 这听起来非常类似于速率限制,这也有助于防止额外的通知,但延迟更新可以在整个应用程序中提供这些好处,而不会增加延迟。 以下是标准,延迟和速率限制模式之间通知调度的不同之处:
- Standard (标准)– 通知立即并同步。 通常通知中间值的依赖关系。
- Deferred (延迟)– 通知异步发生,紧接在当前任务之后,通常在任何UI重绘之前发生。
- Rate-limited (速率限制)– 通知发生在指定的时间段之后(根据浏览器的不同,最小值为2-10 ms)。
启用延迟更新
延迟更新默认关闭,以提供与现有应用程序的兼容性。 要为应用程序使用延迟更新,必须在通过设置以下选项初始化视图模型之前启用它:
ko.options.deferUpdates = true;
当deferUpdates选项打开时,所有observable,computed observable和绑定都将设置为使用延迟更新和通知。 在开始创建基于Knockout的应用程序时启用此功能意味着您不需要担心解决中间值问题,因此可以促进更清洁,纯粹的反应性设计。 但是,当启用现有应用程序的延迟更新时,应该注意,因为它会破坏依赖于同步更新或中间值通知的代码(尽管您可能可以解决这些问题)。
示例:避免多个UI更新
以下是一个有用的示例来演示延迟更新消除中间值的UI更新的能力,以及如何提高性能。
UI怨骂:
<!--ko foreach: $root--> <div class="example"> <table> <tbody data-bind='foreach: data'> <tr> <td data-bind="text: name"></td> <td data-bind="text: position"></td> <td data-bind="text: location"></td> </tr> </tbody> </table> <button data-bind="click: flipData, text: 'Flip ' + type"></button> <div class="time" data-bind="text: (data(), timing() + ' ms')"></div> </div> <!--/ko—>
视图模型源码:
function AppViewModel(type) { this.type = type; this.data = ko.observableArray([ { name: 'Alfred', position: 'Butler', location: 'London' }, { name: 'Bruce', position: 'Chairman', location: 'New York' } ]); this.flipData = function () { this.starttime = new Date().getTime(); var data = this.data(); for (var i = 0; i < 999; i++) { this.data([]); this.data(data.reverse()); } } this.timing = function () { return this.starttime ? new Date().getTime() - this.starttime : 0; }; } ko.options.deferUpdates = true; var vmDeferred = new AppViewModel('deferred'); ko.options.deferUpdates = false; var vmStandard = new AppViewModel('standard'); ko.applyBindings([vmStandard, vmDeferred]);
对特定observable使用延迟更新
即使您不为整个应用程序启用延迟更新,您仍然可以通过专门使某些可观察项延迟,从中受益。 使用延迟扩展器:
this.data = ko.observableArray().extend({ deferred: true });
现在我们可以将一堆项目推入数据数组,而不必担心引起过多的UI或计算更新。 延迟扩展器可以应用于任何类型的可观察量,包括可观察数组和计算可观察量。
示例:避免多个Ajax请求
以下模型表示可以作为分页网格呈现的数据:
function GridViewModel() { this.pageSize = ko.observable(20); this.pageIndex = ko.observable(1); this.currentPageData = ko.observableArray(); // Query /Some/Json/Service whenever pageIndex or pageSize changes, // and use the results to update currentPageData ko.computed(function() { var params = { page: this.pageIndex(), size: this.pageSize() }; $.getJSON('/Some/Json/Service', params, this.currentPageData); }, this); }
因为计算的observable计算了pageIndex和pageSize,它变得依赖于它们。 所以,这个代码将使用jQuery的$ .getJSON函数重新加载currentPageData当一个GridViewModel首次实例化时,每当pageIndex或pageSize属性后来改变。
这是非常简单和优雅(和添加更多可观察的查询参数,也触发刷新自动每当他们改变)的,但有一个潜在的效率问题。 假设您向GridViewModel添加了以下函数,该函数同时更改pageIndex和pageSize:
this.setPageSize = function(newPageSize) { // Whenever you change the page size, we always reset the page index to 1 this.pageSize(newPageSize); this.pageIndex(1); }
问题是,这将导致两个Ajax请求:第一个将在您更新pageSize时启动,第二个将在您更新pageIndex时立即启动。 这是浪费带宽和服务器资源,以及不可预测的竞争条件的来源。
当应用于计算的可观察量时,延迟的延长器也将避免对计算函数的过度评价。 使用延迟更新可确保对当前任务中的依赖性的任何更改顺序只触发对计算的observable的一次重新评估。 例如:
ko.computed(function() { // This evaluation logic is exactly the same as before var params = { page: this.pageIndex(), size: this.pageSize() }; $.getJSON('/Some/Json/Service', params, this.currentPageData); }, this).extend({ deferred: true });
现在,您可以根据需要更改pageIndex和pageSize,并且Ajax调用只会在您将线程释放回JavaScript运行时后发生一次。
强制延迟通知
虽然延迟,异步通知通常更好,因为更少的UI更新,如果你需要立即更新UI可能是一个问题。 有时,为了正确的功能,您需要一个推送到UI的中间值。 您可以使用ko.tasks.runEarly方法完成此操作。 例如:
// remove an item from an array var items = myArray.splice(sourceIndex, 1); // force updates so the UI will see a delete/add rather than a move ko.tasks.runEarly(); // add the item in a new location myArray.splice(targetIndex, 0, items[0]);
强制延迟可观察者总是通知订阅者
当any observable的值是原始值(数字,字符串,布尔值或null)时,只有当observable的依赖项设置为实际上与之前不同的值时,才会通知它的依赖项。 因此,原始值延迟可观测量只有在当前任务结束时它们的值实际上不同时才通知。 换句话说,如果一个原始值的延迟的observable被改变为一个新的值,然后改回原来的值,则不会发生通知。
要确保始终通知订阅者更新,即使该值相同,也可以使用notify扩展器:
ko.options.deferUpdates = true; myViewModel.fullName = ko.pureComputed(function() { return myViewModel.firstName() + " " + myViewModel.lastName(); }).extend({ notify: 'always' });