复杂的应用通常包含着大量错综复杂的依赖,更新一个监控属性可能会级联触发很多计算监控属性、手动的订阅、UI绑定更新。这些更新可能是代价昂贵的,一些不必要的中间值被推送给视图,或者导致额外的计算监控属性评估,严重影响效率。即使在简单的应用里,更新关联的多个属性或一个属性更新多次(如填充监控数组)也会导致类似的问题。
为此,可以使用deferred延迟更新来确保计算监控属性和UI绑定只在它们的依赖稳定下来后才更新。即使监控属性会经历多次中间值,只有最后一次值被用于更新它的依赖。为了促进这一点,使用microtask来是所有通知成为异步的、调度的。听起来很像rate-limiting,另一种阻止额外通知的方法,但是延迟更新可以不用添加延迟在整个应用里提供该功能。
标准模式、deferred和rate-limit的通知调度的区别为:
默认deferred更新是关闭的。要使用它,必须在初始化视图模型前启用:
ko.options.deferUpdates = true;
当deferUpdates启用了,所有的监控属性、计算监控属性和绑定都被设置为延迟更新和通知。在创建基于ko的应用之前启用该特性可以不用担心中间值更新问题了。但是,要注意的是,在已有的应用中启用延迟更新可能会破坏原代码中依赖的同步更新或中间值通知。
下面的例子是刻意地为了演示deferred更新的功能和对性能的提高。
<!--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]);
不希望对整个应用使用deferred更新时,也可以为指定监控属性使用,通过使用deferred
扩展器(对监控属性、计算监控属性和监控数组都适用):
this.data = ko.observableArray().extend({ deferred: true });
下面的模型表示一个可以渲染成分页列表的数据:
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);
}
可以看到,计算监控属性同时依赖于pageIndex
和pageSize
,因此在模型第一次实例化和以后pageIndex
或pageSize
更新时,代码会使用getJSON来重新加载currentPageData
。
这时有个潜在的效率问题,例如同时改变pageIndex
和pageSize
的值时,会触发两次ajax请求:
this.setPageSize = function(newPageSize) {
// Whenever you change the page size, we always reset the page index to 1
this.pageSize(newPageSize);
this.pageIndex(1);
}
这会浪费带宽和服务器资源,并且导致不可预知的竞态条件。
对计算监控属性使用deferred更新可以保证当前任务中的依赖多次改变时只出发一次更新。
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 });
尽管可以延迟更新,更少的UI更新让异步通知看起来更好,但是如果想立刻更新UI怎么办?这种情况下可以使用ko.tasks.runEarly method
:
// 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]);
当监控属性的值是原始类型时(数字、字符串、bool值、null),监控属性只会在设置的值与之前的值不一样时才通知依赖者。因此,原始值得监控属性延迟更新时,只会在当前任务结束前,值的确不一样了才发出通知。也就是说如果原始类型监控属性延迟更新,当值变化了又变回原来的值,是不会通知更新的。
为了保证订阅者始终能收更新到通知,即使值一样,可以使用notify
扩展器:
ko.options.deferUpdates = true;
myViewModel.fullName = ko.pureComputed(function() {
return myViewModel.firstName() + " " + myViewModel.lastName();
}).extend({ notify: 'always' });
通常监控属性在变化时时实时通知订阅者的,以便依赖它的计算监控属性和绑定能实时同步更新。而rateLimit
扩展器是为了将变化通知阻止并延迟指定时间。延时监控属性时异步通知依赖者的。
rateLimit
扩展器可用于任何监控属性,包括计算监控属性和监控数组。通常使用它的原因是:
如果只是要合并更新而不需要延时,使用deferred监控效率更高。
rateLimit
有两种参数格式:
// Shorthand: Specify just a timeout in milliseconds
someObservableOrComputed.extend({ rateLimit: 500 });
// Longhand: Specify timeout and/or method
someObservableOrComputed.extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" } });
method
选项是在通知发起时触发,有两个参数:
notifyAtFixedRate
:没有指定method选项时的默认值。从监控属性第一次改变开始(初始化时或上一次更新后),在指定时间后开始通知。notifyWhenChangesStop
:监控属性在指定时间后没有变化时开始通知。监控属性每次变化时,定时器就会被更新。因此如果监控属性更新频率比指定时间还短时,就一直不会发出通知。var name = ko.observable('Bert');
var upperCaseName = ko.computed(function() {
return name().toUpperCase();
});
通常会这样改变name的值:
name('The New Bert');
这时upperCaseName会立即更新。而如果使用了rateLimit:
var name = ko.observable('Bert').extend({ rateLimit: 500 });
那么upperCaseName不会在name变化后立即重新计算,name会在值变化后等待500ms再将新的值通知给upperCaseName。不管在这500ms内name的值变化了多少次,upperCaseName只会收到一次最新值得通知。
在本例中,监控属性instantaneousValue
是用户按下按键后的实时输入值,计算监控属性delayedValue
配置为在instantaneousValue
停止变化400ms后被通知。
<p>Type stuff here: <input data-bind='textInput: instantaneousValue' /></p>
<p>Current delayed value: <b data-bind='text: delayedValue'> </b></p>
<div data-bind="visible: loggedValues().length > 0">
<h3>Stuff you have typed:</h3>
<ul data-bind="foreach: loggedValues">
<li data-bind="text: $data"></li>
</ul>
</div>
function AppViewModel() {
this.instantaneousValue = ko.observable();
this.delayedValue = ko.pureComputed(this.instantaneousValue)
.extend({ rateLimit: { method: "notifyWhenChangesStop", timeout: 400 } });
// Keep a log of the throttled values
this.loggedValues = ko.observableArray([]);
this.delayedValue.subscribe(function (val) {
if (val !== '')
this.loggedValues.push(val);
}, this);
}
ko.applyBindings(new AppViewModel());
对于计算监控属性,rateLimit定时器是在它依赖的监控属性其中一个变化时开始触发,而不是在计算监控属性的值变化时。计算监控属性只会在需要它的值时才重新评估——延时已到应该发出通知了,或者当它的值被直接访问。如果需要计算监控属性的最新值,可以使用peek
方法。
当监控属性的值是原始类型时(数字、字符串、bool值、null),监控属性只会在设置的值与之前的值不一样时才通知依赖者。因此,原始值得监控属性延时更新时,只会在延时timeout时,值的确不一样了才发出通知。也就是说如果原始类型监控属性延时更新,在延时timeout之前值变化了又变回原来的值,是不会通知更新的。
为了保证订阅者始终能收更新到通知,即使值一样,可以使用notify扩展器:
myViewModel.fullName = ko.computed(function() {
return myViewModel.firstName() + " " + myViewModel.lastName();
}).extend({ notify: 'always', rateLimit: 500 });
deferred与rateLimit相似,deferred是通过异步通知和更新的来达到延迟目的的。但defferred用的不是延时的方式,而是在当前任务结束后立刻更新。在使用ko v3.4.0版本之后可以用deferred来代替rateLimit:
ko.computed(function() {
// ....
}).extend({ deferred: true });
参考资料:
KnockoutJS Documentation