我刚开始使用Knockout.js(一直想尝试一下,但现在我终于有了借口!) - 但是,当我将表绑定到一个相对的表时,我遇到了一些非常糟糕的性能问题一小组数据(约400行左右)。
在我的模型中,我有以下代码:
this.projects = ko.observableArray( [] ); //Bind to empty array at startup
this.loadData = function (data) //Called when AJAX method returns
{
for(var i = 0; i < data.length; i++)
{
this.projects.push(new ResultRow(data[i])); //<-- Bottleneck!
}
};
问题是上面的for
循环大约需要30秒左右,大约需要400行。但是,如果我将代码更改为:
this.loadData = function (data)
{
var testArray = []; //<-- Plain ol' Javascript array
for(var i = 0; i < data.length; i++)
{
testArray.push(new ResultRow(data[i]));
}
};
然后for
循环在眨眼间完成。换句话说,Knockout的push
对象的observableArray
方法非常慢。
这是我的模板:
<tbody data-bind="foreach: projects">
<tr>
<td data-bind="text: code"></td>
<td><a data-bind="projlink: key, text: projname"></td>
<td data-bind="text: request"></td>
<td data-bind="text: stage"></td>
<td data-bind="text: type"></td>
<td data-bind="text: launch"></td>
<td><a data-bind="mailto: ownerEmail, text: owner"></a></td>
</tr>
</tbody>
我的问题:
push
每次调用时都会进行一些重新计算,例如重建绑定的DOM对象。有没有办法延迟这个recalc,或者可能一次推入我的所有项目?如果需要,我可以添加更多代码,但我很确定这是相关的。在大多数情况下,我只是关注网站上的Knockout教程。
更新
根据以下建议,我已更新了我的代码:
this.loadData = function (data)
{
var mappedData = $.map(data, function (item) { return new ResultRow(item) });
this.projects(mappedData);
};
但是,对于400行,this.projects()
仍然需要大约10秒。我承认我不确定没有 Knockout会有多快(只是通过DOM添加行),但我觉得它会比10秒快得多。
更新2:
根据下面的其他建议,我给了 jQuery.tmpl 一个镜头(KnockOut原生支持),这个模板引擎将在3秒内绘制大约400行。这似乎是最好的方法,而不是在滚动时动态加载更多数据的解决方案。
答案 0 :(得分:50)
请参阅:Knockout.js Performance Gotcha #2 - Manipulating observableArrays
更好的模式是获取对我们的底层数组的引用,推送到它,然后调用.valueHasMutated()。现在,我们的订阅者只会收到一个通知,表明该阵列已更改。
答案 1 :(得分:16)
正如评论中所建议的那样。
Knockout拥有与(foreach,with)绑定相关联的自己的本机模板引擎。它还支持其他模板引擎,即jquery.tmpl。阅读here了解更多详情。我没有用不同的引擎做任何基准测试,所以不知道它是否会有所帮助。阅读您以前的评论,在IE7中,您可能很难获得所追求的表现。
顺便说一句,KO支持任何js模板引擎,如果有人为它编写了适配器。您可能想尝试其他人,因为jquery tmpl将被JsRender取代。
答案 2 :(得分:13)
使用分页和KO。
我使用1400个记录的大型数据集遇到了同样的问题,直到我使用了敲除分页。使用$.map
加载记录确实产生了巨大的差异,但DOM渲染时间仍然很可怕。然后我尝试使用分页,这使我的数据集照明快速 - 以及更加用户友好。页面大小为50使得数据集的压倒性更大,并且显着减少了DOM元素的数量。
用KO很容易做到:
答案 3 :(得分:11)
KnockoutJS有一些很棒的教程,特别是the one about loading and saving data
在他们的情况下,他们使用getJSON()
来提取数据非常快。从他们的例子来看:
function TaskListViewModel() {
// ... leave the existing code unchanged ...
// Load initial state from server, convert it to Task instances, then populate self.tasks
$.getJSON("/tasks", function(allData) {
var mappedTasks = $.map(allData, function(item) { return new Task(item) });
self.tasks(mappedTasks);
});
}
答案 4 :(得分:9)
给KoGrid看一看。它智能地管理你的行渲染,使其更具性能。
如果您尝试使用foreach
绑定将400行绑定到表中,那么您将很难通过KO将其推入DOM中。
KO使用foreach
绑定做了一些非常有趣的事情,其中大多数都是非常好的操作,但随着数组大小的增长,它们会开始打破perf。
我一直在试图将大型数据集绑定到表格/网格上的漫长黑暗道路,最终需要在本地拆分/分页数据。
KoGrid完成所有这一切。它被构建为仅渲染查看者可以在页面上看到的行,然后虚拟化其他行直到需要它们。我想你会发现400件物品的性能要比你遇到的要好得多。
答案 5 :(得分:5)
在渲染非常大的数组时避免锁定浏览器的解决方案是“限制”数组,以便一次只添加少量元素,并在其间进行休眠。这是一个可以做到这一点的功能:
function throttledArray(getData) {
var showingDataO = ko.observableArray(),
showingData = [],
sourceData = [];
ko.computed(function () {
var data = getData();
if ( Math.abs(sourceData.length - data.length) / sourceData.length > 0.5 ) {
showingData = [];
sourceData = data;
(function load() {
if ( data == sourceData && showingData.length != data.length ) {
showingData = showingData.concat( data.slice(showingData.length, showingData.length + 20) );
showingDataO(showingData);
setTimeout(load, 500);
}
})();
} else {
showingDataO(showingData = sourceData = data);
}
});
return showingDataO;
}
根据您的使用情况,这可能会导致大量的UX改进,因为用户可能只能在滚动之前看到第一批行。
答案 6 :(得分:5)
利用push()接受变量参数可以在我的情况下获得最佳性能。 装载1300行,持续5973ms(约6秒)。通过该优化,加载时间降至914ms(<1秒) 这提高了84.7%!
Pushing items to an observableArray
的更多信息this.projects = ko.observableArray( [] ); //Bind to empty array at startup
this.loadData = function (data) //Called when AJAX method returns
{
var arrMappedData = ko.utils.arrayMap(data, function (item) {
return new ResultRow(item);
});
//take advantage of push accepting variable arguments
this.projects.push.apply(this.projects, arrMappedData);
};
答案 7 :(得分:4)
我一直处理着大量的数据,valueHasMutated
就像魅力一样。
查看型号:
this.projects([]); //make observableArray empty --(1)
var mutatedArray = this.projects(); -- (2)
this.loadData = function (data) //Called when AJAX method returns
{
ko.utils.arrayForEach(data,function(item){
mutatedArray.push(new ResultRow(item)); -- (3) // push to the array(normal array)
});
};
this.projects.valueHasMutated(); -- (4)
调用(4)
数组后,数据将自动加载到this.projects
所需的observableArray中。
如果你有时间看看这个,只是在遇到任何麻烦让我知道
欺骗这里:通过这样做,如果在推送级别可以避免任何依赖(计算,订阅等),我们可以在调用{{1 }}
答案 8 :(得分:1)
结合使用jQuery.tmpl,可能的解决方法是使用setTimeout以异步方式将项目一次打开到可观察数组;
var self = this,
remaining = data.length;
add(); // Start adding items
function add() {
self.projects.push(data[data.length - remaining]);
remaining -= 1;
if (remaining > 0) {
setTimeout(add, 10); // Schedule adding any remaining items
}
}
这样,当你一次只添加一个项目时,浏览器/ knockout.js可以花时间相应地操作DOM,而不会在几秒钟内完全阻止浏览器,这样用户就可以滚动同时列出。
答案 9 :(得分:1)
我一直在尝试表演,并且有两个我希望可能有用的贡献。
我的实验主要关注DOM操作时间。因此,在进入此之前,在创建可观察数组等之前,绝对值得遵循以上关于推入JS数组的要点。
但是如果DOM操作时间仍然妨碍你,那么这可能会有所帮助:
1:用于在慢速渲染周围包裹加载微调器的模式,然后使用afterRender
隐藏它这并不能解决性能问题,但是如果你循环遍历数千个项目并且它使用的模式可以确保在长时间之前出现加载微调器,则表明延迟可能是不可避免的。 KO操作,然后隐藏它。所以它至少改善了用户体验。
确保您可以加载微调器:
// Show the spinner immediately...
$("#spinner").show();
// ... by using a timeout around the operation that causes the slow render.
window.setTimeout(function() {
ko.applyBindings(vm)
}, 1)
隐藏微调器:
<div data-bind="template: {afterRender: hide}">
触发:
hide = function() {
$("#spinner").hide()
}
2:将html绑定用作黑客
我记得当我使用Opera在机顶盒上工作时,使用DOM操作构建UI时的旧技术。它的速度令人震惊,因此解决方案是将大块HTML存储为字符串,并通过设置innerHTML属性来加载字符串。
通过使用html绑定和一个计算器可以实现类似的东西,该计算器将表格的HTML派生为一大块文本,然后一次性应用它。这确实解决了性能问题,但是它的巨大缺点是它严重限制了你在每个表行内部绑定所能做的事情。
这是一个显示这种方法的小提琴,以及一个可以从表格行内部调用的函数,以类似于KO的方式删除项目。显然这不如正确的KO好,但如果你真的需要超强(ish)性能,这是一种可能的解决方法。
答案 10 :(得分:0)
我还注意到Knockout js模板引擎在IE中工作得更慢,我用underscore.js替换它,工作方式更快。</ p>
答案 11 :(得分:0)
如果使用IE,请尝试关闭开发工具。
在IE中打开开发人员工具会显着减慢此操作的速度。我将〜1000个元素添加到数组中。打开开发工具时,这需要大约10秒钟,并且当IE发生时,IE会冻结。当我关闭开发工具时,操作是即时的,我看到IE中没有减速。