Knockout.js невероятно медленно работает с полубольшими наборами данных - PullRequest
86 голосов
/ 15 марта 2012

Я только начинаю работать с 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 завершится в мгновение ока.Другими словами, метод push объекта Knockout 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>

Мои вопросы:

  1. Является ли это правильным способом связать мои данные (получаемые из метода AJAX) с наблюдаемой коллекцией?
  2. Я ожидаю, что push каждый раз выполняет тяжелый пересчетвызвать его, например, перестроить связанные объекты DOM.Есть ли способ отсрочить этот повторный вызов, или, возможно, вставить все мои элементы сразу?

Я могу добавить больше кода, если это необходимо, но я уверен, что это то, что имеет значение.По большей части я просто следовал инструкциям по нокауту с сайта.

ОБНОВЛЕНИЕ:

Согласно приведенному ниже совету, я обновил свой код:

this.loadData = function (data)
{
   var mappedData = $.map(data, function (item) { return new ResultRow(item) });
   this.projects(mappedData);
};

Тем не менее, this.projects() все еще занимает около 10 секунд для 400 строк.Я признаю, что не уверен, насколько быстро это будет без Knockout (просто добавление строк через DOM), но я чувствую, что это будет намного быстрее, чем 10 секунд.

ОБНОВЛЕНИЕ 2:

По другим советам ниже я дал jQuery.tmpl выстрел (который изначально поддерживается KnockOut), и этот движок шаблонов будет рисовать около 400строки в чуть более 3 секунд.Это кажется лучшим подходом, если не считать решения, которое бы динамически загружало больше данных при прокрутке.

Ответы [ 12 ]

50 голосов
/ 12 октября 2012

Пожалуйста, смотрите: Производительность Knockout.js # 2 - манипулирование observableArrays

Лучшим шаблоном является получение ссылки на наш базовый массив, толчок к нему, затем вызов.valueHasMutated ().Теперь наши подписчики получат только одно уведомление о том, что массив изменился.

16 голосов
/ 15 марта 2012

Как предлагается в комментариях.

Knockout имеет свой собственный механизм шаблонов, связанный с привязками (foreach, with).Он также поддерживает другие движки шаблонов, а именно jquery.tmpl.Читайте здесь для более подробной информации.Я не делал никаких тестов с разными двигателями, поэтому не знаю, поможет ли это.Читая ваш предыдущий комментарий, в IE7 вы можете изо всех сил пытаться получить желаемую производительность.

Кроме того, KO поддерживает любой шаблонизатор js, если кто-то написал адаптер для него.Вы можете попробовать другие, так как jquery tmpl должен быть заменен на JsRender .

13 голосов
/ 27 августа 2012

Используйте нумерацию страниц с KO в дополнение к использованию $ .map.

У меня была такая же проблема с большими наборами данных из 1400 записей, пока я не использовал подкачку с нокаутом.Использование $.map для загрузки записей имело огромное значение, но время рендеринга DOM было по-прежнему отвратительным.Затем я попытался использовать нумерацию страниц, и это сделало мой набор данных быстрым, а также более удобным для пользователя.Размер страницы 50 сделал набор данных намного менее громоздким и значительно сократил количество элементов DOM.

Это очень легко сделать с помощью KO:

http://jsfiddle.net/rniemeyer/5Xr2X/

11 голосов
/ 15 марта 2012

KnockoutJS предлагает несколько отличных учебных пособий, в частности по загрузке и сохранению данных

В их случае они извлекают данные, используя 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);
    });    
}
9 голосов
/ 15 марта 2012

Дайте KoGrid взгляд.Он интеллектуально управляет рендерингом строк, что делает его более производительным.

Если вы пытаетесь связать 400 строк с таблицей, используя привязку foreach, у вас возникнут проблемы с проталкиванием этого через KOв DOM.

KO делает некоторые очень интересные вещи, используя привязку foreach, большинство из которых являются очень хорошими операциями, но они начинают разрушаться при улучшении размера вашего массива.

Я прошел долгий темный путь, пытаясь связать большие наборы данных с таблицами / сетками, и вам в конечном итоге понадобилось разбивать / разбивать на страницы данные локально.

KoGrid делает все это.Она была построена так, чтобы отображать только те строки, которые зритель может видеть на странице, а затем виртуализировать другие строки, пока они не понадобятся.Я думаю, вы найдете его превосходство в 400 предметах намного лучше, чем вы испытываете.

5 голосов
/ 25 апреля 2014

Использование push () для принятия переменных аргументов дало лучшую производительность в моем случае.1300 строк были загружены в течение 5973 мс (~ 6 сек).Благодаря этой оптимизации время загрузки сократилось до 914 мс (<1 сек.) <br>Это улучшение на 84,7%!

Дополнительная информация на Загрузка элементов в массив 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);
};
5 голосов
/ 25 октября 2013

Решение, позволяющее избежать блокировки браузера при рендеринге очень большого массива, состоит в том, чтобы «задушить» массив таким образом, чтобы за один раз добавлялось только несколько элементов с промежуточным сном.Вот функция, которая будет делать именно это:

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, так как пользователь может видеть только первый пакет строк перед прокруткой.

4 голосов
/ 02 декабря 2014

Я имел дело с такими огромными объемами данных, которые поступают для меня 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) данные массива будут загружены в требуемый массив observableArray, который автоматически this.projects.

если у вас есть время, посмотрите на это и на всякий случай возникнут проблемы, дайте мне знать

Трюк здесь: Делая так, если в случае каких-либо зависимостей (вычисляемых, подписок и т. Д.) Можно избежать на уровне push, и мы можем заставить их выполняться сразу после вызова (4).

1 голос
/ 23 июля 2014

Я экспериментировал с производительностью и имею два вклада, которые, я надеюсь, могут быть полезны.

Мои эксперименты сосредоточены на времени манипуляции DOM. Поэтому, прежде чем углубляться в это, определенно стоит следовать приведенным выше пунктам о вставке в массив JS, прежде чем создавать наблюдаемый массив и т. Д.

Но если время манипуляции с DOM все еще мешает, то это может помочь:


1: шаблон, который оборачивает загрузочный счетчик вокруг медленного рендера, а затем скрывает его с помощью afterRender

http://jsfiddle.net/HBYyL/1/

Это на самом деле не является решением проблемы производительности, но показывает, что задержка, вероятно, неизбежна, если вы перебираете тысячи элементов и используете шаблон, в котором вы можете убедиться, что у вас есть загрузочный счетчик перед длительной операцией KO , а затем скрыть это. Так что это улучшает UX, по крайней мере.

Убедитесь, что вы можете загрузить счетчик:

// 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. Это было ужасно медленно, поэтому решение состояло в том, чтобы хранить большие куски HTML в виде строк и загружать строки, устанавливая свойство innerHTML.

Нечто похожее можно достичь с помощью привязки html и вычисления, которое выводит HTML-код для таблицы как большой кусок текста, а затем применяет его за один раз. Это решает проблему производительности, но существенным недостатком является то, что это сильно ограничивает то, что вы можете делать с привязкой внутри каждой строки таблицы.

Вот скрипка, демонстрирующая этот подход, вместе с функцией, которую можно вызывать из строк таблицы, чтобы удалить элемент неопределенно-но-подобным способом. Очевидно, что это не так хорошо, как правильный KO, но если вам действительно нужна высокая производительность, это возможный обходной путь.

http://jsfiddle.net/9ZF3g/5/

1 голос
/ 09 октября 2012

Возможный обходной путь, в сочетании с использованием 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 без полной блокировки браузера на несколько секунд, чтобы пользователь мог прокрутить список одновременно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...