Optimieren des Renderings für ListView-Elemente

Für viele Windows Store-Apps in JavaScript, die Collections verwenden, ist es entscheidend, dass sie mit dem WinJS ListView-Steuerelement gut zusammenarbeiten, um eine gute App-Leistung zu erreichen. Dies ist nicht verwunderlich: Wenn potenziell tausende von Elementen verwaltet und angezeigt werden müssen, fällt jede noch so kleine Optimierung für diese Vorgänge ins Gewicht. Am wichtigsten ist dabei, wie jedes dieser Elemente gerendert wird, d. h. wie und wann jedes Element im ListView-Steuerelement im DOM erstellt und in der App angezeigt wird. Insbesondere das Wann wird zum entscheidenden Faktor, wenn ein Benutzer schnell durch eine Liste schwenkt und erwartet, dass dies verzögerungsfrei geschieht.

Elementrendering wird in einer ListView entweder durch eine in HTML definierte deklarative Vorlage oder durch eine benutzerdefinierte JavaScript-Renderingfunktion ausgeführt, die für jedes Element in der Liste aufgerufen wird. Auch wenn die deklarative Vorlage am einfachsten ist, bietet sie nicht die Flexibilität für eine spezifische Steuerung des Prozesses. Mit einer Renderingfunktion können Sie andererseits das Rendering für jedes einzelne Element anpassen und eine Reihe von Optimierungen umsetzen, die unter HTML ListView optimizing performance sample dargestellt werden. Die Optimierungen sind Folgende:

  • Ermöglichen einer asynchronen Bereitstellung der Elementdaten und eines gerenderten Elements, wie es von einfachen Renderingfunktionen unterstützt wird.
  • Trennen der Elementformerstellung für das allgemeine ListView-Layout von den internen Elementen. Dies wird durch einen Platzhalterrenderer unterstützt.
  • Wiederverwenden eines zuvor erstellten Elements (und dessen untergeordneten Elementen) durch Ersetzen der Daten mit einem Recycling-Platzhalterrenderer. Dadurch erübrigen sich die meisten Elementerstellungsschritte.
  • Verzögern von anspruchsvollen Anzeigevorgängen wie Laden von Bildern oder Animationen, bis das Element tatsächlich angezeigt wird und die ListView nicht mehr schnell geschwenkt wird. Dies wird mit einem mehrstufigen Renderer erreicht.
  • Zusammenfassen ähnlicher visueller Vorgänge durch einen mehrstufigen Batchverarbeitungsrenderer, um das erneute Rendern des DOM zu minimieren.

In diesem Beitrag werden all diese Schritte und deren Interaktion mit dem Elementrenderingprozess der ListView erläutert. Wie Sie sich vorstellen können, sind bei Optimierungen des Wann des Elementrenderings viele asynchrone Vorgänge involviert, d. h. viele Zusagen (Promises). Daher wird auch das Konzept der Zusagen verständlicher, das auf dem Beitrag All about promises in diesem Blog aufbaut.

Im Allgemeinen gilt für alle Renderer, dass die Kernzeit (ohne zurückgestellte Prozesse) zum Rendern von Elementen auf ein Minimum reduziert sein sollte. Da die effektive Leistung der ListView stark davon abhängt, wie gut die Aktualisierungsintervalle mit den Bildschirmaktualisierungsintervallen abgestimmt sind, können bei einem Elementrenderer ein paar Millisekunden zu viel die Gesamtrenderingzeit der ListView über das nächste Aktualisierungsintervall verzögern, was zu verlorenen Frames und stotternder Anzeige führt. Klarer ausgedrückt, bei Elementrenderern ist es wirklich entscheidend, den JavaScript-Code zu optimieren.

Einfache Renderer

Beginnen wir mit einem kurzen Überblick, wie eine Elementrenderingfunktion, hier als Renderer bezeichnet, aussieht. Ein Renderer ist eine Funktion, die der itemTemplate-Eigenschaft der ListView anstelle eines Vorlagennamens zugewiesen wird. Diese Funktion wird bei Bedarf für Elemente aufgerufen, die die ListView in den DOM aufnehmen soll. (Eine grundlegende Dokumentation für Renderer ist auf der Seite itemTemplate verfügbar, aber erst im Beispiel werden die Optimierungen ersichtlich.)

Vielleicht erwarten Sie, dass eine Elementrenderingfunktion einfach ein Element aus der Datenquelle der ListView erhält. Sie sollte dann die HTML-Elemente erstellen, die für ein bestimmtes Element erforderlich sind, und das Stammelement zurückgeben, das die ListView dem DOM hinzufügen kann. Dies geschieht tatsächlich, allerdings gibt es zwei zusätzliche Überlegungen. Erstens können die Elementdaten selbst asynchron geladen werden, daher ist es sinnvoll, die Elementerstellung an die Verfügbarkeit der Daten zu knüpfen. Zweitens kann der Prozess des Elementrenderings selbst andere asynchrone Vorgänge beinhalten, wie z. B. das Laden von Bildern von Remote-URIs oder das Lesen von Daten aus anderen Dateien, die in den Elementdaten angegeben sind. Die verschiedenen Stufen der hier gezeigten Optimierung ermöglichen genau diese asynchronen Prozesse zwischen der Abfrage der Elemente und der tatsächlichen Bereitstellung dieser Elemente.

Daher spielen Zusagen wieder eine Rolle. Die ListView übergibt dem Renderer die Elementdaten nämlich nicht einfach direkt, sie übergibt vielmehr eine Zusage für diese Daten. Und die Funktion gibt nur einer Zusage für das Stammelement des Elements zurück, nicht das Element selbst. Dadurch kann die ListView viele Elementrenderingzusagen zusammenfassen und (asynchron) warten, bis eine ganze Seite von Elementen gerendert wurde. Dies wird zur intelligenten Erstellung verschiedener Seiten auch durchgeführt: zuerst wird die Seite mit den angezeigten Elementen erstellt, dann die zwei nicht sichtbaren Seiten davor und dahinter, zu denen der Benutzer wahrscheinlich schwenken wird. Außerdem kann die ListView dank dieser zusammengefassten Zusagen das Rendern unfertiger Elemente einfach abbrechen, wenn der Benutzer wegschwenkt. Dadurch wird eine unnötige Elementerstellung vermieden.

So werden dieses Zusagen in der simpleRenderer-Funktion des Beispiels verwendet:

 

 function simpleRenderer(itemPromise) {
    return itemPromise.then(function (item) {
        var element = document.createElement("div");
        element.className = "itemTempl";
        element.innerHTML = "<img src='" + item.data.thumbnail +
            "' alt='Databound image' /><div class='content'>" + item.data.title + "</div>";
        return element;
    });
}

In diesem Code wird zuerst ein Completed-Handler an itemPromise angehängt. Der Handler wird aufgerufen, wenn die Elementdaten verfügbar sind, und er erstellt daraufhin die tatsächlichen Elemente. Beachten Sie jedoch wieder, dass die Elemente nicht direkt zurückgegeben werden, es wird eine Zusage zurückgegeben, die für dieses Element erfüllt wird. Das heißt, der Rückgabewert von itemPromise.then() ist eine Zusage, die mit element erfüllt wird, wenn die ListView es benötigt.

Durch die Rückgabe einer Zusage können bei Bedarf andere asynchrone Prozesse abgearbeitet werden. In diesem Fall kann der Renderer diese vorläufigen Zusagen einfach aneinanderreihen und die Zusage vom letzten then in der Kette zurückgeben. Zum Beispiel:

 function someRenderer(itemPromise) {
    return itemPromise.then(function (item) {
        return doSomeWorkAsync(item.data);
    }).then(function (results) {
        return doMoreWorkAsync(results1);
    }).then(function (results2) {
        var element = document.createElement("div");
        // use results2 to configure the element
        return element;
    });
}

Beachten Sie, dass am Ende der Kette nicht done verwendet wird, da die Zusage des letzten then-Aufrufs zurückgegeben wird. Die ListView behandelt alle eventuell auftretenden Fehler.

Platzhalterrenderer

In der nächsten Stufe der ListView-Optimierung wird ein Platzhalterrenderer verwendet, der die Elementerstellung in zwei Ebenen aufteilt. Dadurch kann die ListView nur die Elementteile abfragen, die zur Definition des allgemeinen Listenlayouts erforderlich sind, ohne gleichzeitig alle Elemente in jedem Element erstellen zu müssen. Im Ergebnis kann die ListView den Layoutdurchgang schnell ausführen und auf weitere Eingaben schnell reagieren. Die übrigen Bestandteile des Elements können dann später abgefragt werden.

Ein Platzhalterrenderer gibt anstelle einer Zusage ein Objekt mit zwei Eigenschaften zurück:

  • element Das Element der höchsten Ebene in der Elementstruktur, mit dem die Größe und Form definiert werden kann und das nicht von den Elementdaten abhängig ist.
  • renderComplete Eine Zusage, die erfüllt wird, wenn die restlichen Elementinhalte erstellt werden. Es wird die Zusage der Kette zurückgegeben, die mit itemPromise.then beginnt, wie zuvor beschrieben.

Die ListView kann ermitteln, ob der Renderer eine Zusage (einfacher Fall wie bisher) oder ein Objekt mit den Eigenschaften element und renderComplete (komplexere Fälle) zurückgibt. Der entsprechende Platzhalterrenderer (im Beispiel) für den vorherigen simpleRenderer sieht folgendermaßen aus:

 function placeholderRenderer(itemPromise) {
    // create a basic template for the item that doesn't depend on the data
    var element = document.createElement("div");
    element.className = "itemTempl";
    element.innerHTML = "<div class='content'>...</div>";

    // return the element as the placeholder, and a callback to update it when data is available
    return {
        element: element,

        // specifies a promise that will be completed when rendering is complete
        // itemPromise will complete when the data is available
        renderComplete: itemPromise.then(function (item) {
            // mutate the element to include the data
            element.querySelector(".content").innerText = item.data.title;
            element.insertAdjacentHTML("afterBegin", "<img src='" +
                item.data.thumbnail + "' alt='Databound image' />");
        })
    };
}

Die element.innerHTML-Zuweisung könnte in renderComplete verschoben werden, da die itemTempl-Klasse in der Datei „css/scenario1.css“ des Beispiels die Breite und Höhe des Elements direkt angibt. Der Grund, warum sie in der element-Eigenschaft steht, ist, dass sie den Standardtext „…“ im Platzhalter enthält. Sie könnten genauso gut ein img-Element verwenden, das sich auf eine kleine Paketressource bezieht, die mit allen Elementen geteilt wird (und deshalb schnell gerendert wird).

Recycling-Platzhalterrenderer

Die nächste Optimierung, der Recycling-Platzhalterrenderer, fügt Zusagen nichts Neues hinzu. Er macht den Renderer vielmehr auf einen zweiten Parameter mit dem Namen recycled aufmerksam, der das Stammelement eines zuvor gerenderten aber nicht mehr angezeigten Elements darstellt. Das bedeutet, dass für das recycelte Element bereits untergeordnete Elemente zur Verfügung stehen. Sie können dann einfach die Daten ersetzen und evtl. Änderungen an diesen Elementen vornehmen. Dadurch werden leistungsintensive Aufrufe zur Elementerstellung vermieden, die für neue Elemente erforderlich wären, und es wird für den Renderingprozess ziemlich viel Zeit gespart.

Die ListView kann eine recyceltes Element bereitstellen, wenn ihr loadingBehavior auf „randomaccess“ festgelegt wird. Wenn recycled angegeben ist, können Sie die Daten einfach aus dem Element (und dessen untergeordneten Elementen) löschen, das Element als Platzhalter zurückgeben und dann Daten einfügen und ggf. weitere zusätzliche untergeordnete Elemente in renderComplete erstellen. Wenn ein Recycled-Element nicht vorhanden ist (z. B. bei der ersten Erstellung der ListView oder wenn das loadingBehavior „inkrementell“ ist), müssen Sie das Element neu erstellen. Hier ist der Code aus dem Beispiel für diese Variante:

 function recyclingPlaceholderRenderer(itemPromise, recycled) {
    var element, img, label;
    if (!recycled) {
        // create a basic template for the item that doesn't depend on the data
        element = document.createElement("div");
        element.className = "itemTempl";
        element.innerHTML = "<img alt='Databound image' style='visibility:hidden;'/>" +
            "<div class='content'>...</div>";
    }
    else {
        // clean up the recycled element so that we can reuse it 
        element = recycled;
        label = element.querySelector(".content");
        label.innerHTML = "...";
        img = element.querySelector("img");
        img.style.visibility = "hidden";
    }
    return {
        element: element,
        renderComplete: itemPromise.then(function (item) {
            // mutate the element to include the data
            if (!label) {
                label = element.querySelector(".content");
                img = element.querySelector("img");
            }
            label.innerText = item.data.title;
            img.src = item.data.thumbnail;
            img.style.visibility = "visible";
        })
    };
}

Überprüfen Sie renderComplete auf das Vorhandensein von Elementen, die Sie nicht für einen neuen Platzhalter erstellen, wie z. B. label, und erstellen Sie diese ggf. hier.

Wenn Sie recycelte Elemente allgemeiner leeren möchten, können Sie der resetItem-Eigenschaft der ListView eine Funktion bereitstellen. Diese Funktion enthält einen ähnlichen Code wie den oben gezeigten. Dasselbe gilt für die resetGroupHeader-Eigenschaft, da die Vorlagenfunktionen für Gruppenkopfzeilen genauso verwenden können wie für Elemente. Gruppenkopfzeilen haben wir bisher nicht thematisiert, da sie viel seltener sind und nicht die gleichen Leistungsanforderungen haben. Nichtsdestotrotz haben sie diese Funktion.

Mehrstufige Renderer

Die vorletzte Optimierung ist der mehrstufige Renderer. Er erweitert den Recycling-Platzhalterrenderer insoweit, dass Bilder und andere Medien so lange verzögert werden, bis der Rest des Elements vollständig im DOM repräsentiert ist. Er verzögert auch Effekte wie Animationen so lange, bis das Element tatsächlich auf dem Bildschirm angezeigt wird. Dies berücksichtigt, dass Benutzer oft ziemlich schnell in einer ListView hin- und herschwenken. Und daher ist es sinnvoll, die leistungsintensiven Prozesse asynchron zu verzögern, bis die ListView in eine ruhige Position kommt.

Die ListView bietet die erforderlichen Hooks als Mitglied des item-Ergebnisses aus itemPromise: eine Eigenschaft namens ready (eine Zusage) sowie die zwei Methoden loadImage und isOnScreen, die beide weitere Zusagen zurückgeben. Das heißt:

 renderComplete: itemPromise.then(function (item) {
    // item.ready, item.loadImage, and item.isOnScreen available
})

So werden sie verwendet

  • ready gibt diese Zusage aus dem ersten Completed-Handler in Ihre Kette zurück. Diese Zusage wird erfüllt, wenn die vollständige Struktur des Elements gerendert wurde und angezeigt wird. Das bedeutet, dass Sie ein weiteres then mit einem Completed-Handler anhängen können, in dem Sie nach der Anzeige andere Prozesse, wie das Laden von Bildern, verarbeiten können.
  • loadImage lädt ein Bild von einem URI herunter und zeigt es im vorhandenen img-Element an, wobei eine Zusage zurückgegeben wird, die mit demselben Element erfüllt wird. Sie können einen Completed-Handler an diese Zusage anhängen, die selbst wiederum die Zusage von isOnScreen zurückgibt. Beachten Sie, dass loadImage ein img-Element erstellt, wenn keines vorhanden ist, und dieses dem Completed-Handler bereitstellt.
  • isOnScreen gibt eine Zusage zurück, deren Erfüllungswert ein boolescher Wert ist und angibt, ob das Element sichtbar ist. Bei aktuellen Implementierungen ist dieser Wert bekannt, sodass die Zusage synchron erfüllt wird. Durch Einschließen in eine Zusage kann er jedoch in einer längeren Kette verwendet werden.

All dies wird in der multistageRenderer-Funktion des Beispiels veranschaulicht, in der der Abschluss des Ladens eines Bilds zum Starten einer Einblendanimation verwendet wird. Hier wird gezeigt, was von der renderComplete-Zusage zurückgegeben wird:

 renderComplete: itemPromise.then(function (item) {
    // mutate the element to update only the title
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;

    // use the item.ready promise to delay the more expensive work
    return item.ready;
    // use the ability to chain promises, to enable work to be cancelled
}).then(function (item) {
    // use the image loader to queue the loading of the image
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
        // once loaded check if the item is visible
        return item.isOnScreen();
    });
}).then(function (onscreen) {
    if (!onscreen) {
        // if the item is not visible, don't animate its opacity
        img.style.opacity = 1;
    } else {
        // if the item is visible, animate the opacity of the image
        WinJS.UI.Animation.fadeIn(img);
    }
})

Auch wenn sich hier viel abspielt, ist dies lediglich eine einfache Zusagekette. Der erste asynchrone Vorgang im Renderer aktualisiert einfache Teile der Elementstruktur, wie z. B. Text. Dann gibt er die Zusage in item.ready zurück. Wenn diese Zusage erfüllt ist, wird die asynchrone loadImage-Methode des Elements verwendet, um einen Bilddownload zu starten, und die item.isOnScreen-Zusage von diesem Completed-Handler wird zurückgegeben. Das heißt, dass das onscreen-Sichtbarkeitsflag an den letzten Completed-Handler in der Kette übergeben wird. Wenn diese isOnScreen-Zusage erfüllt ist (das Element wird angezeigt), können wichtige Vorgänge wie Animationen ausgeführt werden.

Das Wenn ist hier entscheidend, da es wiederum wahrscheinlich ist, dass der Benutzer in der ListView herumschwenkt, während all dies geschieht. Wenn all diese Zusagen aneinandergekettet sind, kann die ListView wie gesagt die asynchronen Vorgänge abbrechen, sobald dieses Elemente aus der Ansicht und/oder gepufferten Seiten weggeschwenkt werden. Es genügt der Hinweis, dass das ListView-Steuerelement vielen Leistungstests unterzogen wurde!

Sie sollten auch daran denken, dass das then in all diesen Ketten verwendet wird, da immer noch eine Zusage aus der Renderingfunktion in der renderComplete-Eigenschaft zurückgegeben wird. In diesen Renderern gibt es kein Ende der Kette, daher wird am Ende niemals ein done verwendet.

Miniaturansicht-Batchverarbeitung

Die letzte Optimierung ist wahrhaft das Husarenstück für das ListView-Steuerelement. In der Funktion namens batchRenderer befindet sich diese Struktur für renderComplete (der meiste Code wurde weggelassen):

 renderComplete: itemPromise.then(function (item) {
    // mutate the element to update only the title
    if (!label) { label = element.querySelector(".content"); }
    label.innerText = item.data.title;

    // use the item.ready promise to delay the more expensive work
    return item.ready;
    // use the ability to chain promises, to enable work to be cancelled
}).then(function (item) {
    // use the image loader to queue the loading of the image
    if (!img) { img = element.querySelector("img"); }
    return item.loadImage(item.data.thumbnail, img).then(function () {
        // once loaded check if the item is visible
        return item.isOnScreen();
    });
}).then(function (onscreen) {
    if (!onscreen) {
        // if the item is not visible, don't animate its opacity
        img.style.opacity = 1;
    } else {
        // if the item is visible, animate the opacity of the image
        WinJS.UI.Animation.fadeIn(img);
    }
})

Es ist fast dasselbe wie der multistageRenderer, nur dass dieser mysteriöse Aufruf der Funktion thumbnailBatch zwischen dem item.loadImage-Aufruf und der item.isOnScreen-Überprüfung eingefügt wurde. Das Einfügen von thumbnailBatch in der Kette gibt an, dass ihr Rückgabewert ein Completed-Handler sein muss, der wiederum eine andere Zusage zurückgibt.

Sind Sie verwirrt? Eine Erklärung folgt sogleich, zunächst aber müssen wir die Hintergründe dafür klären, was eigentlich erreicht werden soll.

Wenn nur eine ListView mit einem einzigen Element vorhanden wäre, wären die verschiedenen Ladeoptimierungen nicht bemerkbar. In ListViews befinden sich jedoch normalerweise viele Elemente, und die Renderingfunktion wird für jedes dieser Elemente aufgerufen. Im multistageRenderer des vorherigen Abschnitts startet das Rendern jedes Elements einen asynchronen item.loadImage-Vorgang, um die entsprechende Miniaturansicht von einem beliebigen URI herunterzuladen, und jeder Vorgang kann eine beliebige Zeit lang dauern. Für die gesamte Liste gibt es also eine Reihe von gleichzeitigen loadImage-Aufrufen, und das Rendern jedes Elements wartet auf die Vervollständigung der entsprechenden Miniaturansicht. So weit, so gut.

Eine wichtige Eigenschaft, die im multistageRenderer überhaupt nicht sichtbar ist, ist jedoch, dass sich das img-Element für die Miniaturansicht bereits im DOM befindet, und die loadImage-Funktion legt das src-Attribut des Bilds fest, sobald der Download abgeschlossen ist. Dies wiederum löst eine Aktualisierung im Renderingmodul aus, sobald wir von der übrigen Zusagekette zurückkehren, die danach im Wesentlichen synchron abläuft.

Es ist dann möglich, dass eine Reihe von Miniaturansichten in kurzer Zeit in den UI-Thread zurückkommen. Dies wird dann eine übermäßige Beanspruchung im Renderingmodul und somit eine schlechte visuelle Leistung verursachen. Um diese Überlastung zu vermeiden, sollten diese img-Elemente vollständig erstellt werden, bevor sie sich im DOM befinden, und dann sollten sie Batches hinzugefügt werden, damit sie in einem einzigen Renderingdurchlauf verarbeitet werden können.

Im Beispiel wird dies durch einen raffinierten Zusagecodeabschnitt, nämlich mit der Funktion createBatch erreicht. createBatch wird für die gesamte App nur einmal aufgerufen, und das Ergebnis, eine andere Funktion, wird in der Variable thumbnailBatch gespeichert:

 var thumbnailBatch;
thumbnailBatch = createBatch();

Ein Aufruf dieser thumbnailBatch-Funktion wird in die Zusagekette des Renderers eingefügt. Der Zweck dieser Einfügung ist, wie es der Batchverarbeitungscode im Folgenden zeigen wird, einen Satz von geladenen img-Elementen zu gruppieren und diese Gruppen dann in geeigneten Intervallen zur weiteren Verarbeitung freizugeben. In der Zusagekette des Renderers muss ein Aufruf von thumbnailBatch() eine Completed-Handler-Funktion zurückgeben, die wiederum eine Zusage zurückgibt, und der Erfüllungswert dieser Zusage (im nächsten Schritt der Kette) muss ein img-Element sein, das dann dem DOM hinzugefügt werden kann. Durch das Hinzufügen der Bilder zum DOM nach der Batchverarbeitung wird die gesamte Gruppe im selben Renderingdurchlauf zusammengefasst.

Dies ist ein wichtiger Unterschied zwischen dem batchRenderer und dem multistageRenderer des vorherigen Abschnitts: Bei letzterem ist das img-Element der Miniaturansicht bereits im DOM vorhanden, und es wird als zweiter Parameter an loadImage übergeben. Wenn also loadImage das src-Attribut des Bilds festlegt, wird eine Renderingaktualisierung ausgelöst. Im batchRenderer wird jedoch dieses img-Element separat in loadImage (wo auch src festgelegt wird) erstellt, das img ist jedoch noch nicht im DOM. Es wird erst nach dem Abschluss des thumbnailBatch-Schritts dem DOM hinzugefügt und so zum Mitglied einer Gruppe eines einzigen Layoutdurchlaufs.

Wie funktioniert also die Batchverarbeitung? Im Folgenden ist die vollständige createBatch-Funktion dargestellt:

 function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];

    function completeBatch() {
        var callbacks = batchedItems;
        batchedItems = [];
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }

    return function () {
        batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

        var delayedPromise = new WinJS.Promise(function (c) {
            batchedItems.push(c);
        });

        return function (v) {
            return delayedPromise.then(function () {
                return v;
            });
        };
    };
}

createBatch wird wie gesagt nur einmal aufgerufen, und dessen thumbnailBatch-Ergebnis wird für jedes gerenderte Elementin der Liste aufgerufen. Der Completed-Handler, der von thumbnailBatch erzeugt wird, wird immer dann aufgerufen, wenn ein loadImage-Vorgang abgeschlossen wird.

Dieser Completed-Handler könnte genauso gut direkt in die Renderingfunktion eingefügt werden, es soll jedoch hier versucht werden, die Aktionen über mehrere Elemente hinweg und nicht nur für ein einzelnes Element zu koordinieren. Diese Koordination wird mithilfe der zwei Variablen erreicht, die zu Beginn von createBatch erstellt werden: batchedTimeout wird als leere Zusage initialisiert, und batchedItems wird als Array von Funktionen initialisiert, der zunächst leer ist. createBatch deklariert die Funktion completeBatch, die einfach batchedItems leert, indem sie jede Funktion im Array aufruft:

 function createBatch(waitPeriod) {
    var batchTimeout = WinJS.Promise.as();
    var batchedItems = [];

    function completeBatch() {
        var callbacks = batchedItems;
        batchedItems = [];
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i]();
        }
    }

    return function () {
        batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

        var delayedPromise = new WinJS.Promise(function (c) {
            batchedItems.push(c);
        });

        return function (v) {
            return delayedPromise.then(function () {
                return v;
            });
        };
    };
}

Folgendes passiert nun in thumbnailBatch (die von createBatch zurückgegebene Funktion), die wieder für jedes zu renderndes Element aufgerufen wird. Zuerst werden alle vorhandenen batchedTimeout abgebrochen und gleich wieder neu erstellt:

 batchTimeout.cancel();
        batchTimeout = WinJS.Promise.timeout(waitPeriod || 64).then(completeBatch);

In der zweiten Zeile steht das zukünftige Bereitstellungs-/Erfüllungsmuster, das im Beitrag „All About Promises“ <TODO: link>beschrieben ist: completeBatch muss nach einer Verzögerung von waitPeriod Millisekunden (Standardwert 64 ms) aufgerufen werden. Das heißt, dass so lange wie thumbnailBatch innerhalb der waitPeriod eines vorhergehenden Aufrufs wieder aufgerufen wird, batchTimeout auf eine andere waitPeriod zurückgesetzt wird. Und da thumbnailBatch nur nach dem Abschluss eines item.loadImage-Aufrufs aufgerufen wird, bedeutet dies im Ergebnis, dass alle loadImage-Vorgänge, die innerhalb der waitPeriod des vorhergehenden Aufrufs abgeschlossen werden, in denselben Batchvorgang aufgenommen werden. Wenn eine Pause länger als waitPeriod eintritt, wird die Batchverarbeitung gestartet, d. h. die Bilder werden dem DOM hinzugefügt, und der nächste Batchvorgang beginnt.

Nach Verarbeitung dieses Timeoutcodes erstellt thumbnailBatch eine neue Zusage, die einfach die Abschlussverteilerfunktion in den batchedItems-Array übergibt:

 var delayedPromise = new WinJS.Promise(function (c) {
         batchedItems.push(c);
     });

Sie erinnern sich an den Beitrag „All About Promises“<TODO: link>, in dem erläutert wird, dass eine Zusage lediglich ein Codekonstrukt ist, und so ist es hier auch. Die neu erstellte Zusage zeigt keinerlei asynchrones Verhalten: Es wird lediglich die Abschlussverteilerfunktion c zu batchedItems hinzugefügt. Da aber der Verteiler nichts macht, bis batchedTimeout asynchron abgeschlossen ist, besteht de facto eine asynchrone Beziehung. Wenn das Timeout eintritt und der Batch (in completeBatch) geleert wird, werden alle anderen vorhandenen Completed-Handler für delayedPromise.then aufgerufen.

Die letzten Zeilen des Codes in createBatch stellen die Funktion dar, die von thumbnailBatch zurückgegeben wird. Diese Funktion ist genau der Completed-Handler, der in die gesamte Zusagekette des Renderers eingefügt wird:

 return function (v) {
           return delayedPromise.then(function () {
               return v;
           });
       };

Wenn dieser Codeabschnitt direkt in die Zusagekette eingefügt wird, sehen Sie die daraus resultierenden Beziehungen:

 return item.loadImage(item.data.thumbnail);
          }).then(function (v) {
              return delayedPromise.then(function () {
                  return v;
              });
          ).then(function (newimg) {

Jetzt sehen Sie, dass das Argument v das Ergebnis von item.loadImage ist, das wiederum das erstellte img-Element ist. Wenn auf die Batchverarbeitung verzichtet wird, könnte einfach return WinJS.Promise.as(v) angegeben werden, und die gesamte Kette würde trotzdem funktionieren: v würde dann synchron übergeben und im nächsten Schritt als newimg erscheinen.

Wir übergeben jedoch eine Zusage von delayedPromise.then, die mit v so lange nicht erfüllt wird, bis das aktuelle batchedTimeout erfüllt ist. Wenn dann wieder eine Pause von waitPeriod zwischen loadImage-Vorgängen eintritt, werden diese img-Elemente dem nächsten Schritt in der Kette übergeben, in dem sie dem DOM hinzugefügt werden.

Das war's!

Zusammenfassung

Die fünf verschiedenen Renderingfunktionen in HTML ListView optimizing performance sample haben alle eines gemeinsam: Sie zeigen, wie die asynchrone Beziehung zwischen der ListView und dem Renderer, die durch Zusagen ausgedrückt wird, dem Renderer eine immense Flexibilität dafür verleiht, wie und wann Elemente für Listenelemente erstellt werden. Beim Schreiben Ihrer eigenen Apps hängt die Strategie für ListView-Optimierungen stark davon ab, wie groß die Datenquelle, wie komplex die Elemente selbst und wie groß die Menge der asynchron abgerufenen Daten für diese Elemente ist (z. B. Herunterladen von remoten Bildern). Natürlich werden Sie Elementrenderer so einfach wie möglich konstruieren, um Ihre Leistungsziele zu erreichen. Aber jetzt haben Sie alle erforderlichen Tools an der Hand, um der ListView und Ihrer App zur Höchstleistung zu verhelfen.

Kraig Brockschmidt

Programmmanager, Windows Ecosystem Team

Autor, Programming Windows 8 Apps in HTML, CSS, and JavaScript