Dopiero zaczynam pracę z Knockout.js (zawsze chciałem go wypróbować, ale teraz w końcu mam wymówkę!) - Jednak napotykam naprawdę złe problemy z wydajnością, gdy wiążę tabelę ze stosunkowo małym zestawem dane (około 400 wierszy).
W moim modelu mam następujący kod:
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!
}
};
Problem polega na tym, że for
powyższa pętla zajmuje około 30 sekund z około 400 rzędami. Jeśli jednak zmienię kod na:
this.loadData = function (data)
{
var testArray = []; //<-- Plain ol' Javascript array
for(var i = 0; i < data.length; i++)
{
testArray.push(new ResultRow(data[i]));
}
};
Następnie for
pętla kończy się w mgnieniu oka. Innymi słowy, push
metoda obiektu Knockout observableArray
jest niesamowicie powolna.
Oto mój szablon:
<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>
Moje pytania:
- Czy to właściwy sposób na powiązanie moich danych (pochodzących z metody AJAX) z obserwowalną kolekcją?
- Spodziewam się, że za
push
każdym razem, gdy to nazywam, wykonuję ciężkie ponowne obliczenia, na przykład może odbudować powiązane obiekty DOM. Czy istnieje sposób, aby opóźnić to ponowne obliczenie lub wrzucić wszystkie moje elementy jednocześnie?
W razie potrzeby mogę dodać więcej kodu, ale jestem prawie pewien, że jest to istotne. Przez większość czasu po prostu śledziłem samouczki Knockout z witryny.
AKTUALIZACJA:
Zgodnie z poniższą radą zaktualizowałem swój kod:
this.loadData = function (data)
{
var mappedData = $.map(data, function (item) { return new ResultRow(item) });
this.projects(mappedData);
};
Jednak this.projects()
nadal trwa około 10 sekund dla 400 wierszy. Przyznaję, że nie jestem pewien, jak szybko byłoby to bez Knockout (po prostu dodając wiersze przez DOM), ale mam wrażenie, że potrwa to znacznie szybciej niż 10 sekund.
AKTUALIZACJA 2:
Zgodnie z innymi wskazówkami poniżej, dałem ujęcie jQuery.tmpl (który jest natywnie obsługiwany przez KnockOut), a ten silnik szablonów narysuje około 400 wierszy w nieco ponad 3 sekundy. Wydaje się, że jest to najlepsze podejście, bez rozwiązania, które dynamicznie ładowałoby więcej danych podczas przewijania.
źródło
valueHasMutated
robi to. sprawdź odpowiedź, jeśli masz czas.Odpowiedzi:
Jak zasugerowano w komentarzach.
Knockout ma własny natywny silnik szablonów powiązany z powiązaniami (foreach, with). Obsługuje również inne silniki szablonów, a mianowicie jquery.tmpl. Więcej informacji znajdziesz tutaj . Nie wykonałem żadnego testu porównawczego z różnymi silnikami, więc nie wiem, czy to pomoże. Czytając swój poprzedni komentarz, w IE7 możesz mieć trudności z uzyskaniem żądanej wydajności.
Na marginesie, KO obsługuje dowolny silnik szablonu js, jeśli ktoś napisał do niego adapter. Możesz wypróbować inne, ponieważ jquery tmpl ma zostać zastąpione przez JsRender .
źródło
jquery.tmpl
więc użyję tego. Mogę zbadać inne silniki, a także napisać własne, jeśli będę miał trochę więcej czasu. Dzięki!data-bind
instrukcji w swoim szablonie jQuery, czy też używasz składni $ {code}?${code}
składni i jest znacznie szybsza. Próbowałem również uruchomić Underscore.js, ale nie miałem jeszcze szczęścia (<% .. %>
składnia koliduje z ASP.NET) i wydaje się, że nie ma jeszcze wsparcia dla JsRender.ResultRow
, nie zaktualizuje to interfejsu użytkownika (będziesz musiał zaktualizowaćprojects
obserwowalną tablicę, co wymusi ponowne renderowanie tabeli). $ {} może być zdecydowanie korzystne, jeśli twoje dane są prawie tylko do odczytuZobacz: Knockout.js Performance Gotcha # 2 - Manipulowanie obserwowalnymi tablicami
źródło
Użyj paginacji z KO oprócz używania $ .map.
Miałem ten sam problem z dużymi zbiorami danych obejmującymi 1400 rekordów, dopóki nie użyłem stronicowania z nokautem. Używanie
$.map
do ładowania rekordów robiło ogromną różnicę, ale czas renderowania DOM był nadal ohydny. Potem spróbowałem użyć paginacji, co sprawiło, że mój zbiór danych świecił szybko i był bardziej przyjazny dla użytkownika. Rozmiar strony wynoszący 50 sprawił, że zestaw danych był znacznie mniej przytłaczający i radykalnie zmniejszył liczbę elementów DOM.Bardzo łatwo to zrobić z KO:
http://jsfiddle.net/rniemeyer/5Xr2X/
źródło
KnockoutJS ma kilka świetnych samouczków, szczególnie ten dotyczący ładowania i zapisywania danych
W ich przypadku pobierają dane,
getJSON()
które są niezwykle szybkie. Z ich przykładu: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); }); }
źródło
self.tasks(mappedTasks)
zajmuje około 10 sekund (przy 400 rzędach). Uważam, że jest to nadal nie do zaakceptowania.+1
aby zarówno uprościć mój kod, jak i radykalnie zwiększyć szybkość. Być może ktoś ma bardziej szczegółowe wyjaśnienie, czym jest wąskie gardło.Daj KoGrid wygląd. Inteligentnie zarządza renderowaniem wierszy, dzięki czemu jest bardziej wydajny.
Jeśli próbujesz powiązać 400 wierszy z tabelą za pomocą
foreach
wiązania, będziesz miał problem z przepchnięciem tego przez KO do DOM.KO robi kilka bardzo interesujących rzeczy przy użyciu
foreach
wiązania, z których większość to bardzo dobre operacje, ale zaczynają się rozpadać na perf, gdy rozmiar twojej tablicy rośnie.Przeszedłem długą, ciemną drogę, próbując powiązać duże zestawy danych z tabelami / siatkami, a ty w końcu musisz rozdzielić / stronicować dane lokalnie.
KoGrid robi to wszystko. Został zbudowany, aby renderować tylko te wiersze, które widz może zobaczyć na stronie, a następnie wirtualizować inne wiersze, dopóki nie będą potrzebne. Myślę, że przekonasz się, że jego wydajność w 400 przedmiotach jest znacznie lepsza niż doświadczasz.
źródło
Rozwiązaniem pozwalającym uniknąć blokowania przeglądarki podczas renderowania bardzo dużej tablicy jest „dławienie” tablicy w taki sposób, że tylko kilka elementów jest dodawanych naraz, z zaśnięciem pomiędzy. Oto funkcja, która to zrobi:
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; }
W zależności od przypadku użycia może to spowodować ogromną poprawę UX, ponieważ użytkownik może zobaczyć tylko pierwszą partię wierszy, zanim będzie musiał przewijać.
źródło
Wykorzystanie metody push () akceptującej zmienne argumenty dało najlepsze wyniki w moim przypadku. 1300 wierszy ładowano przez 5973 ms (~ 6 sek.). Dzięki tej optymalizacji czas ładowania spadł do 914 ms (<1 s).
To 84,7% poprawy!
Więcej informacji na temat przesyłania elementów do obserowalnej tablicy
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); };
źródło
Miałem do czynienia z tak ogromnymi ilościami danych, które napływały do mnie
valueHasMutated
jak marzenie.Zobacz model:
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)
Po wywołaniu
(4)
tablicy dane zostaną załadowane do wymaganej tablicy obserwaable, która jestthis.projects
automatycznie.jeśli masz czas, spójrz na to i na wszelki wypadek daj mi znać
Sztuczka: robiąc to w ten sposób, jeśli w przypadku jakichkolwiek zależności (obliczonych, subskrybowanych itp.) Można uniknąć na poziomie wypychania i możemy sprawić, by były wykonywane za jednym razem po wywołaniu
(4)
.źródło
push
, problem polega na tym, że nawet pojedyncze wywołanie push spowoduje długie czasy renderowania. Jeśli tablica ma 1000 elementów powiązanych z aforeach
, wypchnięcie pojedynczego elementu powoduje ponowne renderowanie całego elementu foreach, co wiąże się z dużymi kosztami czasu renderowania.Możliwym obejściem tego problemu w połączeniu z użyciem jQuery.tmpl jest wypychanie elementów naraz do obserwowalnej tablicy w sposób asynchroniczny, przy użyciu metody 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 } }
W ten sposób, gdy dodajesz tylko jeden element na raz, browser / knockout.js może zająć trochę czasu, aby odpowiednio zmodyfikować DOM, bez całkowitego blokowania przeglądarki przez kilka sekund, tak aby użytkownik mógł jednocześnie przewijać listę.
źródło
Eksperymentowałem z wydajnością i mam dwa wkłady, które, mam nadzieję, mogą być przydatne.
Moje eksperymenty skupiają się na czasie manipulacji DOM. Dlatego przed przystąpieniem do tego zdecydowanie warto postępować zgodnie z powyższymi punktami dotyczącymi wypychania do tablicy JS przed utworzeniem obserwowalnej tablicy itp.
Ale jeśli czas manipulacji DOM wciąż przeszkadza, może to pomóc:
1: wzór do zawijania pokrętła ładowania wokół powolnego renderowania, a następnie ukrycia go za pomocą funkcji afterRender
http://jsfiddle.net/HBYyL/1/
To nie jest naprawa problemu z wydajnością, ale pokazuje, że opóźnienie jest prawdopodobnie nieuniknione, jeśli zapętlasz tysiące elementów i używa wzorca, w którym możesz upewnić się, że pojawi się spinner ładowania przed długą operacją KO, a następnie ukryj to później. Więc przynajmniej poprawia UX.
Upewnij się, że możesz załadować spinner:
// 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)
Ukryj spinner:
<div data-bind="template: {afterRender: hide}">
który wyzwala:
hide = function() { $("#spinner").hide() }
2: Używanie powiązania HTML jako sztuczki
Przypomniałem sobie starą technikę z czasów, gdy pracowałem nad dekoderem z Operą, budując interfejs użytkownika za pomocą manipulacji DOM. To było przerażająco wolne, więc rozwiązaniem było przechowywanie dużych fragmentów kodu HTML jako ciągów i ładowanie ciągów przez ustawienie właściwości innerHTML.
Coś podobnego można osiągnąć, używając powiązania html i obliczenia, które wyprowadza kod HTML dla tabeli jako duży fragment tekstu, a następnie stosuje go za jednym razem. To rozwiązuje problem z wydajnością, ale ogromną wadą jest to, że poważnie ogranicza to, co można zrobić z wiązaniem w każdym wierszu tabeli.
Oto skrzypce, które pokazują to podejście, wraz z funkcją, którą można wywołać z wnętrza wierszy tabeli, aby usunąć element w niejasny sposób. Oczywiście nie jest to tak dobre, jak właściwe KO, ale jeśli naprawdę potrzebujesz niesamowitej (ish) wydajności, jest to możliwe obejście.
http://jsfiddle.net/9ZF3g/5/
źródło
Jeśli używasz IE, spróbuj zamknąć narzędzia deweloperskie.
Otwarcie narzędzi programistycznych w IE znacznie spowalnia tę operację. Dodaję ~ 1000 elementów do tablicy. Otwarcie narzędzi deweloperskich trwa około 10 sekund, a przeglądarka IE zawiesza się, gdy to się dzieje. Kiedy zamykam narzędzia programistyczne, operacja jest natychmiastowa i nie widzę spowolnienia w IE.
źródło
Zauważyłem też, że silnik szablonów Knockout js działa wolniej w IE, zastąpiłem go podkreśleniem.js, działa znacznie szybciej.
źródło