Quantcast
Channel: MSDN Blogs
Viewing all 29128 articles
Browse latest View live

[Sample Of Jun 24th] Keep AutoComplete List Open in ASP.NET

$
0
0

 

Homepageimage
RSS Feed

Sample Download : http://code.msdn.microsoft.com/Keep-AutoComplete-List-Open-7cef304f

This code sample demonstrates how to keep AutoComplete List Open, and select multiple options. This feature can be used in many scenarios. Such as when user wants to select multiple products based on fuzzy search


image

You can find more code samples that demonstrate the most typical programming scenarios by using Microsoft All-In-One Code Framework Sample Browser or Sample Browser Visual Studio extension. They give you the flexibility to search samples, download samples on demand, manage the downloaded samples in a centralized place, and automatically be notified about sample updates. If it is the first time that you hear about Microsoft All-In-One Code Framework, please watch the introduction video on Microsoft Showcase, or read the introduction on our homepage http://1code.codeplex.com/.


Top 10 Microsoft Developer Links for Monday, June 24th

$
0
0

Unable to Configure “File Access” Feature on Forefront Unified Access Gateway Server

$
0
0

 

 

Introduction

I am sure most of us will be aware of the “File Access” Functionality which can be configured through UAG. I find it a very cool feature as you can provide access of specific Servers/Shares to the Users connecting through Forefront UAG.

Scenario

The issue that I am going to discuss here is a typical one. We were trying to configure the “File Access” Option on UAG as shown below:

image

 

 

And after clicking on the “File Access” option shown above we were asked to provide Credentials. We provided Domain Credentials and then we got an Error as shown in the snippet below:

image

 

 

So, we checked the Basic stuff on UAG to make sure we have all the Pre-requisites are in place which are required to make “File Access” work, as per the Article below:

 

http://technet.microsoft.com/en-us/library/dd897168.aspx

 

Following points are most Important and can not be missed:

 

  1. Set a local security policy for a mixed-mode domain. For instructions, see To set a local security policy for a mixed-mode domain.

  2. On the Forefront UAG server, set the startup type for the following Windows services to automatic:

    • Computer Browser (optional, for performance enhancement).
    • Distributed Transaction Coordinator.
    • Workstation.
  3. Install Client for Microsoft Networks. For instructions, see To install Client for Microsoft Networks. You might be required to provide the operating system installation disk while completing this task.

  4. Join the domain. For instructions, see How to join your computer to a domain.

 

 

But even after following all the steps mentioned in the above Article, we were still getting the same Error and we were not able to Browse neither the Domain, nor any Machines there.

 

That's where I started looking into it from a different angle. We went to the Domain Controller and looked at the Event Viewer there. And to our Surprise we saw the following Events there:

image

 

 

 

image

Now after looking at the above Events we got some Direction. The First Event(8021) showed a Machine as the Master Browser and when we checked about that machine we came to know that it was some MAC Client. Now that was strange. How can a MAC client be acting as a Master Browser in the Domain.

So, we followed the following Action Plan then:

1) Stop and disable the “Computer browser” on all the machines  and except for the UAG  . (if possible please stop and disable the computer browser service on as many machines in the domain if possible all the computers.

2) please make the following registry changes on the UAG

3) \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services \Browser\Parameters

4) Unless the computer is configured as the preferred master browser, the value of the IsDomainMaster entry is always set to False or No . Make the value TRUE

5) HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services \Browser\Parameters >>>  MaintainServerList set to Yes or Auto

6) Reboot the UAG server

And after following the above steps we could see the machines getting enumerated in the “File Access” Option on the UAG console.

 

Blog Written By

NITIN SINGH

SUPPORT ESCALATION ENGINEER, FOREFRONT EDGE SECURITY, MICROSOFT

Uso de tecnologías y ahorro

$
0
0

Cada día es más evidente que el uso de tecnologías de la información y comunicación (TIC) es más que apropiado en los hospitales. Hoy no se cuestiona más si es o no necesario la implementación de diversas soluciones, sino que tan rápido se pueden implementar.

Hace unos días estuve charlando con el Director General de un Hospital público en Lima, Perú el cual mencionaba de la necesidad de actualizar no solo el tema de infraestructura tecnológica pero más importante era de como acelerar el proceso de adopción de diversas soluciones con el objetivo de poder brindar atención de salud a más pacientes, en major forma y sobre todo con posibilidad de ahorro en general, poder diagnosticar más rápido, implementar el proceso de citas y muchas otras iniciativas relacionadas con la mejor eficiencia y más eficacia del personal del hospital, aprovechando lo que ya es práctica común en otras partes del mundo. Y como esta historia, hay muchas más.

Recientemente se publicó el artículo “Las TICs en salud ayudan a reducir el coste del cuidado de los mayores en un 25%” en donde se hacen varios comentarios que aluden a estudios realizados en donde se destaca que las TIC en salud permiten tratar al doble de pacientes en zonas alejadas, en temas de información de pacientes, ayudan a reducir la mortalidad infantil y ayudan a reducir los costos asociados con personas adultas.

Como verán, el ahorro se traduce en ahorro de tiempo de médicos y pacientes, ahorro salvando vidas, tiempo, recursos, en fin, más allá del simple ahorro económico del cual todos estamos pendientes.

Y con esto les dejo un par de preguntas: ¿cuáles han sido sus experiencias en la implementación del uso de las TIC en hospitales? ¿Qué harían diferente hoy día?

Hasta la próxima!

Flavio

World of Warcraft API Starter Kit for Windows Phone

$
0
0

 

Via DaveDev.net

 

AllRealms[1]SingleRealmAndMenu[1]settings[1]

Overview

In my previous post I gave you an overview of my teams Starter Kits for Windows Store Apps.  I recently took the World of Warcraft Starter Kit I did for Windows, which was written in WinJS (Windows Library for JavaScript), and ported it to Windows Phone.

Since there is currently no WinJS available for Windows Phone I chose to write a XAML/C#  native Windows Phone app and then make my network calls using the Web Browser Control.  This is the same common approach that is used in the Windows Phone HTML5 App template and other popular tools such as Intel’s XDK.

In case you missed the previous post my reasons for choosing Blizzard’s World of WarCraft APIs was to create an understandable sample app to display Realm Status (these are the game servers for World of Warcraft).  For those not in the know World of Warcraft is a very successful online game from Blizzard Entertainment® and offers an excellent open api to get game statistics.  The full API documentation can be found at http://blizzard.github.io/api-wow-docs/.

You can grab the Phone version of the Starter Kit here on Github and I have also added it to my Starter Kits section on this blog.

Currently I have implemented the WOW API into two core pieces of functionality in the Windows Phone version.

  • Realm Status (individual)
  • Realm Status (all)

Just like the Windows Store App I wanted the Windows Phone version to serve as a workable app template for helping your own apps pass certification.  Taken what I’ve learned from my own apps I’ve implemented several pieces of functionality I’ve seen developers fail certification on or get tripped up with during development.  This sample app includes workable functionality for all of the following:

    • Application Bar
    • Settings Page
    • About Page
    • Web Browser Control
    • Web Tasks
    • Calling HTML5/JS from C#
    • Small and Medium Tiles
Requirements
  • Windows 8
  • Visual Studio 2012 Express for Windows Phone or higher
Setup

Customization

Step 1. All of the Blizzard World of Warcraft API's do not require a developer key to use. However, if you plan on creating an application with heavy api usage Blizzard requests you contact them at api-support@blizzard.com to register your application.

Step 2. Currently only Realm Status is implemented. Adding additional functionality such as character or pvp info is as easy as calling the appropriate wow api (found in http://blizzard.github.io/api-wow-docs/) and then wrapping it in a function the same way realm status was done in /html/js/wowapi.js and /html/js/getRealmStatus.

Wowapi.js file contains the function call to get all of the realm statuses and then generated a bunch of div tags inside of a div in /html/index.html called divStatus.  In the Windows Store version we used WinJS data convertors to change the value of the colors in the listview columns.  For this version we are using simple CSS to color the background of each div.

 1:"use strict";
 2:  
 3:function getRealmStatusAll() {
 4:  
 5:     $.getJSON("http://us.battle.net/api/wow/realm/status?jsonp=?", function (data) {
 6:         $.each(data.realms, function (i, item) {
 7:  
 8://Add some padding between the server name cells
 9:var bgPadding = 'padding-top: 30px;padding-bottom: 30px;';
 10:  
 11://Check status of realm and convert background to green for online and red for offline
 12:var bgStyle = item.status === true ? 'background-color: green;' : 'background-color: red;';
 13:  
 14://Set up div style and show server text
 15:var divText = "<div style='' + bgPadding + bgStyle + "'><p>" + item.name + "<p></div><p>";
 16:  
 17://Add new div to main divStatus div
 18:             $(divText).appendTo("#divStatus");
 19:         });
 20:     });
 21: }
 22:  
 23:function onLoad() {
 24://tmp until buttons included
 25:     getRealmStatusAll();
 26: }

GetRealmStatus.js follows similar convention to wowapi.js but with the exception that we will now accept an argument for a single server.  This argument will actually be passed to the JavaScript function from the C# code the Web Browser Control is hosted in.

 1:"use strict";
 2:  
 3:function getRealmStatus(realm) {
 4:      $.getJSON("http://us.battle.net/api/wow/realm/status?realm=" + realm + "&jsonp=?", function (data) {
 5:         $.each(data.realms, function (i, item) {
 6:  
 7://Add some padding between the server name cells
 8:var bgPadding = 'padding-top: 30px;padding-bottom: 30px;';
 9:  
 10://Check status of realm and convert background to green for online and red for offline
 11:var bgStyle = item.status === true ? 'background-color: green;' : 'background-color: red;';
 12:  
 13://Set up div style and show server text
 14:var divText = "<div style='' + bgPadding + bgStyle + "'><p>" + item.name + "<p></div><p>";
 15:  
 16://Add new div to main divStatus div
 17:             $(divText).appendTo("#divStatus");
 18:         });
 19:     }); 
 20:  
 21://alert(realm); //To Test args
 22: }
 23:  
 24:  

Step 3. Now that we have the networking calls setup (with minimal change from our WinJS Windows Store version) we need some way to display the HTML that we are generating.

This is where we will take the Web Browser Control and tell it to load the appropriate content.  We are also using a flag here for what type of data we need to display (All or Single realms).  These flags are set in the Application scope and available from all the app’s pages.  So when a user sets a new server to track we will see those changes reflected immediately.

 1:// Url of Home page
 2:privatestring uriRealmsAll = "/Html/index.html";
 3:privatestring uriRealmsSingle = "/Html/realmStatus.html";
 4:
 5:// Constructor
 6:public MainPage()
 7:         {
 8:             InitializeComponent();
 9:             InitSettings();
 10:  
 11:             Browser.IsScriptEnabled = true;
 12:         }
 13:  
 14://Get server settings
 15:privatevoid InitSettings() 
 16:         {
 17:if (App.appSettings.Contains("realm"))
 18:             {
 19:                 App.userRealm = (string)App.appSettings["realm"];
 20:             }
 21:else
 22:             {
 23:                 App.userRealm = App.defaultRealm;
 24:                 App.appSettings.Add("realm", App.defaultRealm);
 25:             }
 26:         }
 27:  
 28:privatevoid Browser_Loaded(object sender, RoutedEventArgs e)
 29:         {
 30:if (App.allRealms) 
 31:             {
 32:                 Browser.Navigate(new Uri(uriRealmsAll, UriKind.Relative));
 33:             }
 34:else
 35:             {
 36:                 Browser.Navigate(new Uri(uriRealmsSingle, UriKind.Relative));
 37:             }
 38:  
 39:         }
 40:  
 41:
 42:privatevoid btnAbout_Click(object sender, System.EventArgs e)
 43:         {
 44:             NavigationService.Navigate(new Uri("/About.xaml", UriKind.Relative));
 45:         }
 46:  
 47:privatevoid btnSettings_Click(object sender, System.EventArgs e)
 48:         {
 49:             NavigationService.Navigate(new Uri("/RealmSettings.xaml", UriKind.Relative));
 50:         }
 51:  
 52:privatevoid btnRealm_Click(object sender, System.EventArgs e)
 53:         {
 54:             App.allRealms = false;
 55:             Browser.Navigate(new Uri(uriRealmsSingle, UriKind.Relative));
 56:           }
 57:  
 58:privatevoid btnAllRealms_Click(object sender, System.EventArgs e)
 59:         {
 60:             App.allRealms = true;
 61:             Browser.Navigate(new Uri(uriRealmsAll, UriKind.Relative));
 62:         }
 63:  
 64:privatevoid Browser_LoadCompleted(object sender, NavigationEventArgs e)
 65:         {
 66:if (!App.allRealms)
 67:             {
 68:                 Browser.IsScriptEnabled = true;
 69:                 String[] realm = new String[1];
 70:                 realm[0] = HttpUtility.UrlEncode(App.userRealm);
 71:                 Browser.InvokeScript("getRealmStatus", realm[0]);
 72:  
 73:             }
 74:         }

You will notice that we are passing in the server name flag to the Web Browser Control itself through the InvokeScript method.  This allows us to generate user interaction in our native XAML based UI and then pass arguments to the JavaScript functions we load into the Web Browser Control.

Two things to point out here the first is that you must have scripting enabled on the control through the IsScriptEnabled flag and the second is that you’ll need to wait to call the JavaScript function once all the content has been loaded into the DOM.  I chose to do this through the control’s LoadCompleted Event.

Step 4. As you add more functionality to your own app you will want to include an About and Settings page.  Both of these are set up using native XAML controls.

The About page will hand of processing to the WebBrowserTask if a user decides to get more information.

XAML:

 1:<Gridx:Name="LayoutRoot"Background="Transparent">
 2:<Grid.RowDefinitions>
 3:<RowDefinitionHeight="Auto"/>
 4:<RowDefinitionHeight="*"/>
 5:</Grid.RowDefinitions>
 6:  
 7:<!--TitlePanel contains the name of the application and page title-->
 8:<StackPanelGrid.Row="0"Margin="12,17,0,28">
 9:<TextBlockText="{StaticResource app_title}"Style="{StaticResource PhoneTextNormalStyle}"/>
 10:<TextBlockText="about"Margin="9,-7,0,0"Style="{StaticResource PhoneTextTitle1Style}"/>
 11:</StackPanel>
 12:<StackPanelx:Name="ContentPanel"Grid.Row="1"Margin="12,0,12,0">
 13:<TextBlockx:Name="txtAbout"HorizontalAlignment="Center"TextWrapping="Wrap"Text="The World of Warcraft Windows Phone 8 API Starter Kit is a completely free app for educational and entertainment purposes only. The code for this app is open source and available on Github. "VerticalAlignment="Top"Height="108"Width="446"/>
 14:<Buttonx:Name="btnGithub"Content="Download Source Code"Margin="0,40"Click="btnGithub_Click"/>
 15:<TextBlockx:Name="txtAbout2"HorizontalAlignment="Center"TextWrapping="Wrap"Text="For more information aout my other free apps, developer starter kits, and online training courses check out my blog DaveDev.net."VerticalAlignment="Top"Height="108"Width="446"/>
 16:<Buttonx:Name="btnBlog"Content="Visit DaveDev.net"Margin="0,0,0,40"Click="btnBlog_Click"/>
 17:  
 18:</StackPanel>

C#:

 1:privatevoid btnGithub_Click(object sender, RoutedEventArgs e)
 2:        {
 3:            WebBrowserTask webBrowserTask = new WebBrowserTask();
 4:            webBrowserTask.Uri = new Uri("http://github.com/apimash", UriKind.Absolute);
 5:            webBrowserTask.Show();
 6:        }
 7:  
 8:privatevoid btnBlog_Click(object sender, RoutedEventArgs e)
 9:        {
 10:            WebBrowserTask webBrowserTask = new WebBrowserTask();
 11:            webBrowserTask.Uri = new Uri("http://davedev.net", UriKind.Absolute);
 12:            webBrowserTask.Show();
 13:        }

The Settings page will store any server name that a user enters and then use that for 

XAML:

 1:<Gridx:Name="LayoutRoot"Background="Transparent">
 2:<Grid.RowDefinitions>
 3:<RowDefinitionHeight="Auto"/>
 4:<RowDefinitionHeight="*"/>
 5:</Grid.RowDefinitions>
 6:  
 7:<!--TitlePanel contains the name of the application and page title-->
 8:<StackPanelGrid.Row="0"Margin="12,17,0,28">
 9:<TextBlockText="{StaticResource app_title}"Style="{StaticResource PhoneTextNormalStyle}"/>
 10:<TextBlockText="settings"Margin="9,-7,0,0"Style="{StaticResource PhoneTextTitle1Style}"/>
 11:</StackPanel>
 12:  
 13:<!--ContentPanel - place additional content here-->
 14:<Gridx:Name="ContentPanel"Grid.Row="1"Margin="12,0,12,0">
 15:<StackPanelMargin="10,58,-10,339"Orientation="Vertical">
 16:<TextBlockHorizontalAlignment="Left"TextWrapping="Wrap"Text="My Realm"Margin="10,0,0,0"FontSize="24"/>
 17:<TextBoxx:Name="txtRealm"HorizontalAlignment="Left"Height="72"TextWrapping="Wrap"Width="406"/>
 18:  
 19:<Buttonx:Name="btnSave"Content="Save"HorizontalAlignment="Left"Width="152"Height="91"Click="btnSave_Click"/>
 20:</StackPanel>
 21:</Grid>
 22:</Grid>

C#:

 1:publicpartialclass RealmSettings : PhoneApplicationPage
 2:     {
 3:public RealmSettings()
 4:         {
 5:             InitializeComponent();
 6:             InitUserSettings();
 7:         }
 8:  
 9:privatevoid InitUserSettings()
 10:         {
 11:if (App.appSettings.Contains("realm"))
 12:             {
 13:                 txtRealm.Text = (string)App.appSettings["realm"];
 14:             }
 15:         }
 16:  
 17:privatevoid btnSave_Click(object sender, System.Windows.RoutedEventArgs e)
 18:         {
 19:if (App.appSettings.Contains("realm"))
 20:             {
 21:                 App.appSettings["realm"] = txtRealm.Text;
 22:             }
 23:else
 24:             {
 25:                 App.appSettings.Add("realm", txtRealm.Text);
 26:             }
 27:  
 28:             var result = MessageBox.Show("Your Realm has been updated to '" + txtRealm.Text + "'.", "Changes Saved", MessageBoxButton.OK);
 29:  
 30:             App.allRealms = false;
 31:             NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative));
 32:         }
 33:     }

Conclusion

Hopefully this Starter Kits will help aid you in your own Windows Phone app development. Be sure to check out the rest of my Starter Kits as well as the full APIMASH project here.

-Dave

SQL Server Data-Tier Application Framework (June 2013) Available

$
0
0

What’s new?

This feature release of the Data-Tier Application Framework (DACFx) brings with it a significant payload of both new functionality as well as 100+ bug fixes since the last release. The features included in this update center around a new set of extensibility scenarios and an extended API surface area on top of DACFx. These extensibility scenarios include:

  • New Schema Model API
    • We have introduced a new set of APIs in the DAC namespace that can be used to open, enumerate, and navigate the schema model contained in DAC Packages (.dacpacs/.bacpacs).
    • This API, centered around the TSqlModel class, supports scenarios including hydrating a schema model from a DAC Package, querying an opened model for objects of a certain type, enumerating the properties on a specific object, and traversing relationships between objects.
    • This API allows you to implement custom solutions on top of DACFx and DAC package artifacts like performing additional validation of your database schema and writing custom schema navigation and browsing tools.
  • Deployment Contributor Extensibility
    • This release introduces two extensibility points that allow users to customize the DACFx deployment pipeline to enable custom validation or manipulation of the deployment plan. Supported deployment contributor types include:
      • Deployment Plan Modifier - Extension that runs after the deployment plan has been generated allowing read/write access to the deployment plan, and read-only access to the model comparison results, and source/target schema models
      • Deployment Plan Executor - Extension that runs during deployment allowing read-only access to the deployment plan       
    • Deployment contributors can be executed when deploying via the DacServices API, SqlPackage.exe, or SSDT.
  •  Build Contributor Extensibility
    • Like with the deployment pipeline, this release adds an extensibility point to the DACFx build pipeline.
    • Users can create extensions that run when building a SQL Server database project in Visual Studio or using MSBuild, enabling read-only access to the schema model and all properties/arguments of the build task.

These updated APIs and extensibility mechanisms live in the Microsoft.SqlServer.Dac and Microsoft.SqlServer.Dac.Extensions assemblies that get installed when you install this latest release.

How can I get it?

You can obtain this new version of DACFx and its dependencies from our Microsoft Download Center page here. This version of DACFx supersedes all previously available versions. This new version of DACFx supports in-place upgrade from previous versions, so it is not required to remove previous DACFx installations before upgrading to this new release.

If you are on an x64 machine, you will need to install both the x64 and x86 versions of the MSIs from the DACFx June download page linked above. 

The version of this update is 11.1.2861.0. This supersedes the previously available May release (11.1.2825.1). You can determine which version of the Data-Tier Application Framework you currently have installed via Windows Add/Remove Programs.

Compatibility

This release of DACFxis fully compatible with the newly released June 2013 release of SQL Server Data Tools, available here. This release is also compatible with SSMS of SQL Server 2012 and SQL Server 2012 SP1.

Only SQL Server Management Studio that ships with SQL Server 2014 CTP1 should be used with SQL Server 2014 CTP1 (http://www.microsoft.com/en-us/sqlserver/sql-server-2014.aspx). No version of Visual Studio, SQL Server Data Tools, or Data-Tier Application Framework provides support for SQL Server 2014 CTP1. We will continue to invest in SQL Server Data Tools (SSDT) and Data-Tier Application Framework (DACFx) and the tooling will be updated to support future SQL Server and Windows Azure SQL Database releases.  

Note – Packages created with this version of DACFx that contain required deployment contributors are only consumable by this latest release of DACFx.

If you experience any issues or have any feedback, please let us know via the forum or Connect.

May 2013 Optional Updates are on MyOEM for Industry 8

$
0
0

Posted By Windows Embedded Team

The May 2013 Optional Updates are on MyOEM for Microsoft® Windows® Embedded 8 Industry.

The list below applies to Windows Embedded 8 Industry:

  • KB2832450 - This KB resolves an issue with Unified Write Filter (UWF) where:
    • File commit in Unified Write Filter fails when the file owner does not match the file caller.
    • File commit in Unified Write Filter fails when HORM is disabled after being enabled on the system.
    • Unified Write Filter does not work with portable devices.

If you have questions on accessing MyOEM, please email the OEM Customer Communications Care Team at OEM@microsoft.com.

Read More...

CommentsProduct Updates

...(read more)

IMPORTANT: regarding tomorrow's HERO session

$
0
0

Hey all, we do apologize that tomorrow’s HERO session
will be cancelled due to the current critical circumstances that we are facing.
Please take care of yourself, and let us pray for Egypt.


June 2013 Security Updates are on MyOEM for XPe SP3 and Standard 2009

$
0
0

Posted By Windows Embedded Team

The June 2013 Security Updates are now available on MyOEM for Microsoft® Windows® XP Embedded with Service Pack 3 and Windows® Embedded Standard 2009.

The list below applies to Windows Embedded Standard 2009:

  • KB 2804576 - Vulnerabilities in .NET Framework Could Allow Spoofing

The list below applies to both Windows XPe SP3 and Windows Embedded Standard 2009:

  • KB 2804577 - Vulnerabilities in .NET Framework Could Allow Spoofing
  • KB 2829361 - Vulnerabilities in Kernel-Mode Drivers Could Allow Elevation Of Privilege
  • KB 2820197 - Update Rollup for ActiveX Kill Bits
  • KB 2838727 - Cumulative Security Update for Internet Explorer
  • KB 2839229 - Vulnerability in Windows Kernel Could Allow Information Disclosure

This download includes cumulative database updates for the Windows XP Embedded and Windows Embedded Standard 2009 product development databases. The database updates incorporate all security updates from prior months; therefore you do not need to install previous security database updates. The new updates included in this download can be applied directly to runtime images. The componentized versions of these update for updating the database will be included in next month’s security update release.

If you have questions on accessing MyOEM, please email the OEM Customer Communications Care Team at OEM@microsoft.com.

Read More...

CommentsProduct Updates

...(read more)

Cross-Post: Partners in the enterprise cloud

$
0
0

Editor's Note: This post comes from Satya Nadella, President of Microsoft's Server & Tools Business

As longtime competitors, partners and industry leaders, Microsoft and Oracle have worked with enterprise customers to address business and technology needs for over 20 years. Many customers rely on Microsoft infrastructure to run mission-critical Oracle software and have for over a decade. Today, we are together extending our work to cover private cloud and public cloud through a new strategic partnership between Microsoft and Oracle. This partnership will help customers embrace cloud computing by improving flexibility and choice while also preserving the first-class support that these workloads demand.

For all of the details on the newly announced partnership, please read my complete post over on The Official Microsoft Blog.

Satya

优化 ListView 条目呈现

$
0
0

对于许多使用 JavaScript 编写的 Windows 应用商店应用(可以处理数据集合),与 WinJS ListView 控件很好地协同工作是获得出色应用性能的基础。这并不奇怪:当处理数以千计的条目的管理和显示时,您对这些条目进行的每个数位的优化可能都发挥着重要的作用。最重要的是每个条目的呈现方式,也就是说,如何呈现以及何时呈现,ListView 控件中的每个条目都会构建于 DOM 中,并成为应用的可见部分。实际上,当用户在列表内快速平移时并且希望列表能够跟进平移的速度时,何时呈现就成为一个重要的要素。

通过 HTML 中定义的声明性模板或通过一个自定义的 JavaScript 呈现函数(将会为列表中的每个条目调用该函数),就可以在 ListView 中呈现条目。尽管使用声明性模板是最简单的,但是它不能对整个过程提供太多具体的控制。另一方面,呈现函数允许逐条目自定义呈现,并实现一定量的优化,HTML ListView 优化性能示例中演示了其中的一些示例。优化包括:

  • 允许异步传输条目数据和已呈现的元素(基本呈现函数支持该功能)。
  • 将条目形状的创建与内部元素的创建分开,条目形状对于 ListView 的整体数据布局是必不可少的要素。这是通过一个占位符呈现器提供对该功能的支持。
  • 重用以前创建的条目元素(及其子集),方法是替换数据、避免大部分元素创建步骤,此功能是通过一个循环占位符呈现器提供支持。
  • 延迟耗费资源的可视性操作(如图片加载和动画显示)直到条目可见,并且 ListView 没有快速平移,此功能是通过一个多级呈现器来完成。
  • 将这些相同的可视性操作同时进行批处理操作,以便利用多级批处理呈现器将重新呈现 DOM 的工作量最小化。

在本篇博文中,我们将逐一介绍所有这些阶段,并了解它们是如何与 ListView 的条目呈现过程相互配合。您可以想象得到,围绕着何时呈现条目所进行的优化涉及大量的异步操作,因此会有大量的承诺。因此在这个过程中,基于先前发表的关于承诺博文,我们还会对承诺本身有更深入的理解。

作为一条适用于所有呈现器的一般原则,令核心条目呈现时间(不包括延迟的操作)尽量最短非常重要。因为 ListView 的最终性能表现很大程度上取决于它的更新与屏幕刷新间隔的吻合程度,在条目呈现器上花费的额外的几个毫秒可能会使 ListView 的整体呈现时间推迟到下一个刷新间隔,导致丢失帧和视觉效果波动。也就是说,条目呈现器就是一个用于优化您的 JavaScript 代码的位置。

基本呈现器

我们先快速了解一下条目呈现函数(我简单地称为呈现器)是什么样的。呈现器是您分配给 ListView 的 itemTemplate属性而不是模板名称的一个函数,将会在必要时为 ListView 希望包括在 DOM 中的条目调用该函数。(另外,您可以在 itemTemplate页查找到呈现器的基础说明文档,不过它是切实显示优化的示例。)

您可能希望条目呈现函数仅仅获得一个来自 ListView 数据源的条目。然后它将会创建该特定条目所需的 HTML 元素,然后返回根路径元素,ListView 可以将该根路径元素添加到 DOM。这是确凿出现的情况,但是还有两个额外的注意事项。首先,条目数据本身可以异步加载,因此,将元素创建与该数据的可用性相关联起来就很有必要。另外,呈现条目本身的过程可能还涉及其他异步操作,例如从远程 URI 加载图片,或者在条目数据中标识的其他文件中读取数据。事实上,我们将看到不同的优化级别,在条目元素请求和这些元素的实际传输之间允许任意数量的异步操作。

因此,重申一下,您可以看到呈现器会涉及到承诺。举例来说,ListView 并不直接为呈现器提供条目数据,它为该数据提供承诺。并非函数直接返回该条目的根路径元素,而是返回该元素的承诺。这允许 ListView 同时加入许多条目呈现承诺,并且等待(异步)直到整个页面得以呈现。事实上,它执行此操作以智能地管理构建不同页面的方式,首先构建可视条目的页面,然后构建用户最有可能下次向前平移和向后平移到的两个屏幕外页面。此外,具有所有��些承诺意味着,如果用户平移走,则 ListView 可以轻松地取消未完成条目的呈现,这样可以避免不必要的元素创建。

我们可以查看在示例的 simpleRenderer函数中是如何使用这些承诺的:

 

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;
});
}

此代码首先会将已完成处理程序附加到 itemPromise。当条目数据可用并且有效地创建元素时,将调用该处理程序。不过,再次强调,我们实际上并没有直接返回元素 — 而是返回了利用该元素履行的承诺。也就是说,从 itemPromise.then()的返回值是利用该元素履行的承诺(ListView 是否需要以及何时需要)。

返回承诺允许我们在必要时执行其他异步操作。在这种情况下,呈现器可以仅将这些中间承诺链接起来,从链中的最后一个 then中返回承诺。例如:

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;
});
}

请注意,这是一种我们在链的末尾不使用 done的情况,因为我们从最后一个 then调用中返回了承诺。ListView 负责处理这个过程中可能引发的任何错误。

占位符呈现器

ListView 优化的下一个阶段使用占位符呈现器,可以将元素构建分到两个不同阶段进行。这样将允许 ListView 只要求对于定义列表的整体布局必要的元素,而无需在每个条目内构建所有元素。因此,ListView 可以快速完成其布局过程,并且对后续的输入继续保持高度及时响应的状态。然后它可以在以后要求提供元素的其余部分。

占位符呈现器会返回一个具有两个属性的对象,而不是承诺:

  • element条目结构中的顶级元素,足以定义条目的大小和形状,它不依赖于条目数据。
  • renderComplete当构建其余的元素内容时要履行的承诺,即,依旧会从 itemPromise.then开始从您拥有的任意链处返回承诺。

ListView 足够智能,可以检查您的呈现器是返回一个承诺(如上基本情况),还是返回具有 element和 renderComplete属性的对象(更为高级的情况)。因此先前 simpleRenderer的同等占位符呈现器(在示例中)如下所示:

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' />");
})
};
}

请注意,element.innerHTML分配可以移到 renderComplete内,因为示例的 css/scenario1.css 文件中的itemTempl类会直接指定条目的宽度和高度。在 element属性中包括该类的原因是它在占位符中提供默认的 “…” 文本。您可以就像轻松地使用一个引用了在所有条目间共享的小型包内资源的 img元素那样(因此可以快速呈现)。

循环占位符呈现器

下一个优化是循环占位符呈现器,不会在涉及承诺的位置添加任何新的内容。它会将第二个参数的感知度添加到名为 recycled的呈现器,它是以前呈现过但现在不再可见的条目的根路径元素。即,循环的元素已经有自己的子元素,因为您可以简单地替换该数据,可能需要对一些元素进行微调。对于全新条目所需的调用,可以避免大部分成本高昂的其他方式的元素创建调用,这样在呈现的过程中可以节省不少时间。

当它的 loadingBehavior设置为“randomaccess”时,ListView 可能会提供一个循环的元素。如果指定循环,则您可以将数据清除出元素(及其子元素),将其返回为占位符,然后在 renderComplete内填充数据并创建所有其他子元素(如果需要)。如果未提供循环的元素(当 ListView 首次创建或者当 loadingBehavior为“incremental”时),您将再次创建该元素。下面是针对此种变化情况的示例代码:

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";
})
};
}

在 renderComplete中,请确保检查存在着您不是为新占位符创建的元素,例如 label,如果需要请在此处创建他们。

如果您希望以更常规地方式清除循环的条目,请注意,您可以为 ListView 的 resetItem属性提供一个函数。此函数将包含与上面所示类似的代码。对于 resetGroupHeader属性也是同样,因为您可以对组标头以及条目使用模板功能。我们尚未涉及太多此话题,是因为组标头非常少,并且通常没有相同的性能影响。不过,此功能仍然强大。

多级呈现器

现在我们来看倒数第二级的优化,多级呈现器。这会令循环占位符呈现器延迟加载图片和其他媒体,直到条目的其他部分已经在 DOM 中完全呈现。它还会延迟诸如动画显示之类的效果,直到条目确实完全显示在屏幕上。这是意识到用户会经常在 ListView 内快速平移,所以异步延迟很多耗费资源的操作直到 ListView 达到一种稳定的状态和位置,这是非常有意义的。

ListView 提供必要的挂钩作为来自 itemPromiseitem结果的成员:名为 ready的属性(承诺)和两个方法 loadImageisOnScreen,都会返回(更多!)承诺。即:

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

下面是它们的使用方式:

  • ready从链内第一个已完成处理程序中返回此承诺。当元素的整体结构已经被呈现并且可��时,就履行了此承诺。这意味着您可以利用一个已完成处理程序来链接另一个 then,在该处理程序中您可以执行其他可视化后操作,诸如加载图片。
  • loadImage从 URI 中下载图片,并在指定的 img元素中显示该图片,然后返回一个利用同一元素履行的承诺。您可以将已完成处理程序附加到此承诺,它本身会从 isOnScreen返回承诺。请注意,loadImage将创建一个 img元素(如果未提供该元素的话),并且将其提供给您的已完成处理程序。
  • isOnScreen返回一个承诺,其履行值为一个布尔值,指示该条目是否可见。在当前的实施中,这是一个已知值,因此将同步履行承诺。尽管是打包在一个承诺中,但是该值可以用在一个较长的链中。

我们会在示例的 multistageRenderer函数中查看到所有情形,其中图片加载的完成用于启动一个淡出动画。下面的内容是从 renderComplete承诺中返回的结果:

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);
}
})

尽管有很多内容,我们仍然还有一个基本承诺链在这里。呈现器中的第一个异步操作更新了条目元素结构的简单部分,比如文本。然后它会在 item.ready中返回承诺。当履行该承诺时,或者更准确地说,如果该承诺被履行后,我们使用条目的异步 loadImage方法开始图片的加载,从已完成处理程序中返回 item.isOnScreen承诺。这意味着 onscreen可见性标志会在链中传递到最终的已完成处理程序。是否履行以及何时履行了 isOnScreen承诺,意味着该条目确实可见,我们可以执行类似动画等相关操作。

我强调是否履行了承诺,是因为当所有这一切正发生的时候,很可能又是用户在 ListView 内平移。将所有这些承诺链在一起,令 ListView 可能在这些条目平移出视图和/或任何缓存页面时取消异步操作。只要说 ListView 控件已经经历了很多性能测试,这就足矣!

另外一点也很重要,就是要再次提醒我们自己在所有这些链中使用 then,因为我们仍会在 renderComplete属性内的呈现函数中返回了承诺。我们永远不会是这些呈现器中链的末尾,因此我们永远不会在末尾使用 done

缩略图批处理

最后的优化确实是 ListView 控件的杀手锏。在名为 batchRenderer的函数中,我们发现了 renderComplete的此结构(已省略了大部分代码):

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);
}
})

这与 multistageRenderer基本相同,除了在 item.loadImage调用和 item.isOnScreen检查之间对名为 thumbnailBatch函数的神秘调用以外。在链中放置 thumbnailBatch指示其返回值必须是已完成处理程序,其本身会返回另一个承诺。

有点糊涂了?好,我们现在就来探个究竟!不过首先需要一些有关我们将要完成内容的更多背景内容。

如果我们的 ListView 只具有一个条目,则各种不同的加载优化的效果可能不会那么显著。但 ListViews 通常具有很多条目,并且将会为每个条目调用呈现函数。在前面的 multistageRenderer部分中,每个条目的呈现都会启动异步 item.loadImage操作,从任意 URI 下载其缩略图,每个操作所需花费的时间都不固定。因此对于整个表,我们可能会同时调用一批 loadImage,等待其特定缩略图完成时会呈现每个条目。到目前为止一切顺利。

multistageRenderer中并不是全部可见的一个重要特征是缩略图的 img元素已经在 DOM 中,下载一经完成,loadImage函数会设置该图片的 src属性。当我们一经从承诺链的其余部分返回时,这反过来也会触发呈现引擎的更新,会在该点后同步。

然后,在可能的情况下,一批缩略图也会在很短的时间内返回到 UI 线程。这会导致呈现引擎的过度繁忙,以及低劣的视觉效果。为了避免这种混乱,我们希望在这些 img元素位于 DOM 之前完全创建它们,然后成批添加它们,以便可以在单一的呈现过程中处理这些元素。

该示例利用名为 createBatch的函数通过一些承诺魔力代码来完成上述操作。仅会为整个应用调用一次 createBatch,其结果(另一个函数)会存储在名为 thumbnailBatch的变量中:

var thumbnailBatch;
thumbnailBatch = createBatch();

对此 thumbnailBatch函数的调用(就像我在这里引用它一样)会再次插入到呈现器的承诺链中。考虑到我们马上要看到的批处理代码的性质,此插入的目的就是将一组已加载的 img元素分组到一起,然后在适合的间隔释放它们以便进一步处理。只需要在呈现器中查看承诺链,对 thumbnailBatch()的调用就必须返回已完成处理程序函数,该函数会返回一个承诺,该承诺的履行值(查看链中的下一步)必须是 img元素,可以添加到 DOM。在进行批处理后将这些图片添加到 DOM,我们会将整个组添合并到同一呈现过程。

这是 batchRenderer和上述 multistageRenderer之间的重要区别:后来,缩略图的 img元素已经存在于 DOM 中,会作为第二个参数传递给 loadImage。因此当 loadImage设置了图片的 src属性,将会触发呈现更新。不过,在 batchRenderer内,该 img元素是在 loadImage内单独创建的(其中 src也得到了设置),但是 img尚未在 DOM 中设置。它只是在 thumbnailBatch步骤完成后添加到 DOM,令其成为单一布局过程内的一个组。

因此现在让我们来看一下批处理是如何工作的。下面是整个 createBatch函数:

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]();
}
}

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

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

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

再强调一次,createBatch仅调用一次,将会为列表中的每个已呈现条目调用一次其 thumbnailBatch结果。然后将会调用 thumbnailBatch生成的已完成处理程序,不论何时 loadImage操作完成。

就好像已完成处理程序已经直接轻松地插入呈现函数一样,但是我们在这里要执行的是跨多个条目之间的协调活动,而不是按条目执行。通过在 createBatch开始处创建和初始化的两个变量来实现协调:batchedTimeout,(初始化为一个空承诺)和 batchedItems(初始化一个最初为空的函数数组)。createBatch还会声明一个函数,completeBatch只是清空 batchedItems在数组中调用每个函数:

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]();
}
}

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

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

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

现在让我们看 thumbnailBatch(从 createBatch返回的函数)内发生的情况,将会为每个要呈现的条目再次调用该函数。首先,我们取消所有现有 batchedTimeout并立即重新创建它:

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

第二行显示在“关于承诺”博文 <TODO:link>中介绍的未来的传输/履行模式:它会在延迟 waitPeriod毫秒后(默认值为 64ms)调用 completeBatch。这意味着只要在前一次调用的 waitPeriod内再次调用 thumbnailBatchbatchTimeout将被重设为另一个 waitPeriod。因为仅在 item.loadImage调用完成之后才会调用 thumbnailBatch,我们可以有把握地说,在上次调用的 waitPeriod内完成的任何 loadImage操作都将包括在同一批处理中。当时间超过 waitPeriod时,就会处理该批处理 — 图片会添加到 DOM — 下一批处理开始。

在处理此超时业务后,thumbnailBatch创建了一个新的承诺,只是将完成的调度程序函数推送到 batchedItems数组:

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

请注意,在“关于承诺”<TODO:link>中承诺只是一个代码构造,如此而已。新创建的承诺内及其自身没有异步行为:我们只是将完成的调度程序函数 c添加到 batchedItems。不过,当然,我们在 batchedTimeout异步完成前不会利用该调度程序执行任何操作,因此事实上这里存在着异步关系。当发生超时时,我们清除了批处理(在 completeBatch内),我们会将在别处指定的已完成处理程序调用到 delayedPromise.then

这将会为我们带来 createBatch中的最后一行代码,是 thumbnailBatch实际返回的函数。此函数正是已完成处理程序,将会插入到呈现器的整个承诺链:

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

事实上,让我们把这段代码直接放到承诺链中,因为我们可以看到产生的关系:

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

现在,我们可以看到参数 vitem.loadImage的结果,是 img元素为我们创建的。如果我们不想执行批处理,我们可以就说 return WinJS.Promise.as(v),整个链将仍可以工作:然后将同步传递 v,并且在下一步骤中显示为 newimg

尽管如此,我们将会从 delayedPromise.then返回承诺,将不会利用 v 履行该承诺,直到履行了当前的 batchedTimeout。这时 — 当 loadImage完成之间存在 waitPeriod的间隙时 — 那些 img元素将会传输到链中的下一步,在那里它们会被添加到 DOM。

就先介绍这么多内容!

结语

HTML ListView 优化性能示例中演示的五个不同的呈现函数都有一个共同的通性:他们显示 ListView 和呈现器之间的异步关系 — 通过承诺表示 — 为呈现器在如何以及何时为列表中的条目生成元素方面提供了极大的灵活性。在编写自己的应用时,您为 ListView 优化使用的策略很大程度上取决于您数据源的大小、条目本身的复杂性,以及您为这些条目异步获取的数据量(例如下载远程图片)。很明显,您希望在达到性能目标的前提下将条目呈现器尽量保持简单。但是,无论如何,您现在具有帮助 ListView — 和您的应用 — 达到性能最佳所需的所有工具。

Kraig Brockschmidt
Windows 生态系统团队项目经理
使用 HTML、CSS 和 JavaScript 编程 Windows 8 应用作者

Optimieren des Renderings für ListView-Elemente

$
0
0

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 nichtdone 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]();
}
}

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

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

returnfunction (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]();
}
}

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

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

returnfunction (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 batchedTimeoutabgebrochen 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:

returnfunction (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

Optimisation du rendu des éléments du contrôle ListView

$
0
0

Pour de nombreuses applications du Windows Store écrites en JavaScript qui utilisent les collections, une coopération appropriée avec le contrôle ListView WinJS est essentielle pour optimiser leurs performances. Ceci n'est pas surprenant : lorsque vous devez gérer et afficher des milliers d'éléments potentiels, toutes les optimisations que vous réalisez sur ces éléments font la différence. Ce qui compte le plus, c'est la manière dont chacun de ces éléments est rendu, c'est-à-dire comment (et quand) chaque élément du contrôle ListView est élaboré dans le DOM et apparaît dans l'application. En effet, la composante quand de cette équation devient un facteur critique lorsqu'un utilisateur se déplace rapidement dans une liste et s'attend à ce que cette liste soutienne le rythme qu'il lui impose.

Le rendu des éléments dans un contrôle ListView se produit via un modèle déclaratif défini en HTML ou via une fonction de rendu JavaScript personnalisée qui est appelée pour chaque élément de la liste. Même si l'utilisation d'un modèle déclaratif est la méthode la plus simple, elle ne confère pas une latitude très importante pour contrôler précisément le processus. Une fonction de rendu, d'un autre côté, vous permet de personnaliser le rendu élément par élément et de mettre en œuvre différentes optimisations qui sont illustrées dans l'exemple d'optimisation des performances de contrôle HTML ListView. Ces optimisations sont les suivantes :

  • Autoriser la distribution asynchrone des données des éléments et d'un élément rendu. Ceci est pris en charge par les fonctions de rendu élémentaires.
  • Séparer la création de la forme d'un élément, qui est nécessaire à la disposition globale du contrôle ListView, de ses éléments internes. Ceci est pris en charge via un convertisseur d'espace réservé.
  • Réutiliser un élément créé au préalable (et ses enfants) en remplaçant ses données, ce qui évite la plupart des étapes de création d'un élément. Ceci est possible via un convertisseur d'espace réservé de recyclage.
  • Reporter les opérations visuelles intensives, telles que le chargement d'images et les animations, jusqu'à ce qu'un élément soit visible et tant que le contrôle ListView n'est pas déplacé rapidement. Ceci s'effectue via un convertisseur à plusieurs phases.
  • Traiter par lot les opérations visuelles identiques afin de limiter la répétition des rendus du DOM avec un convertisseur par lot à plusieurs phases.

Dans ce billet, nous allons examiner toutes ces étapes et voir comment elles coopèrent avec le processus de rendu des éléments du contrôle ListView. Comme vous pouvez l'imaginer, les optimisations qui entourent le moment où les éléments sont rendus entraînent un grand nombre d'opérations asynchrones et véhiculent par conséquent beaucoup de promesses. Au cours du processus, nous allons également acquérir une connaissance plus approfondie des promesses, en nous appuyant sur le billet précédent Tout sur les promesses de ce blog.

De manière générale pour tous les convertisseurs, il est important de toujours ramener au minimum le temps de rendu des principaux éléments (sans compter les opérations reportées). Comme les performances finales du contrôle ListView dépendent de beaucoup du bon alignement de ses mises à jour sur les intervalles d'actualisation de l'écran, quelques millisecondes supplémentaires dans un convertisseur d'élément peut faire dépasser le temps de rendu global du contrôle ListView sur le prochain intervalle d'actualisation, entraînant la suppression d'images et des irrégularités visuelles. En d'autres termes, il est réellement très important d'optimiser votre code JavaScript dans les convertisseurs d'éléments.

Convertisseurs élémentaires

Commençons par passer rapidement en revue la fonction de rendu des éléments (que je vais simplement appeler convertisseur. Un convertisseur est une fonction que vous attribuez à la propriété itemTemplate du contrôle ListView à la place d'un nom de modèle. Cette fonction est appelée, lorsque cela est nécessaire, pour les éléments que le contrôle ListView souhaite inclure dans le DOM. (Vous trouverez une documentation élémentaire sur les convertisseurs à la page itemTemplate, mais l'exemple illustre mieux les optimisations.)

Vous pouvez penser qu'une fonction de rendu des éléments reçoit simplement un élément de la source de données du contrôle ListView. Qu'elle crée ensuite les composants HTML nécessaires à cet élément en particulier et renvoie le composant racine que le contrôle ListView peut ajouter au DOM. C'est essentiellement ce qui se passe, mais vous devez prendre en considération deux points complémentaires. D'abord, comme les données de l'élément même peuvent être chargées de manière asynchrone, il est logique de lier la création des composants à la disponibilité de ces données. Ensuite, le processus de rendu de l'élément même peut entraîner d'autres tâches asynchrones, telles que le chargement d'images à partir d'URI à distance ou la lecture de données dans d'autres fichiers identifiés dans les données de l'élément. Les différents niveaux d'optimisation que nous allons voir, en fait, peuvent donner lieu à un nombre arbitraire de tâches asynchrones entre la demande des composants de l'élément et la distribution effective de ces composants.

Ici encore, vous pouvez donc vous attendre à voir apparaître des promesses ! D'abord, le contrôle ListView ne communique pas simplement les données de l'élément au convertisseur directement ; il fournit une promesse concernant ces données. Et la fonction ne renvoie pas directement le composant racine de l'élément ; elle renvoie une promesse concernant ce composant. Ceci permet au contrôle ListView d'associer plusieurs promesses de rendu d'éléments et d'attendre (en mode asynchrone) qu'une page entière d'éléments soit rendue. En fait, il procède ainsi pour gérer intelligemment l'élaboration de différentes pages, en créant d'abord la page des éléments visibles, puis deux pages hors écran avant et après (les deux pages que les utilisateurs ont le plus de chances de consulter ensuite). En outre, la mise en place de toutes ces promesses signifie que le contrôle ListView peut facilement annuler le rendu des éléments non terminés si l'utilisateur s'éloigne, ce qui évite de créer inutilement des composants.

Nous pouvons voir comment ces promesses sont utilisées dans la fonction simpleRenderer de l'exemple :

 

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;
});
}

Ce code associe d'abord un gestionnaire Terminé à itemPromise. Le gestionnaire est appelé lorsque les données de l'élément sont disponibles et répond en créant les composants. Mais remarquez à nouveau que nous ne renvoyons pas l'élément directement : nous renvoyons une promesse qui est réalisée avec ce composant. En d'autres termes, la valeur renvoyée par itemPromise.then() est une promesse qui est réalisée avec element si et quand le contrôle ListView en a besoin.

Le renvoi d'une promesse nous permet d'effectuer d'autres tâches de synchronisation si nécessaire. Dans ce cas, le convertisseur peut simplement relier ces promesses intermédiaires et renvoyer la promesse issue du dernier appel de la méthode then dans la chaîne. Par exemple :

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;
});
}

Notez que dans ce cas, nous n'utilisons pasdoneà la fin de la chaîne, car nous renvoyons la promesse issue du dernier appel de la méthode then. Le contrôle ListView est chargé de la gestion des erreurs susceptibles de survenir.

Convertisseurs d'espaces réservés

La prochaine étape d'optimisation ListView utilise un convertisseur d'espace réservé qui décompose la création du composant en deux phases. Cela permet au contrôle ListView de demander uniquement les parties d'un composant qui sont nécessaires pour définir la disposition globale de la liste, sans créer tous les composants à l'intérieur de chaque élément. Le contrôle ListView peut ainsi s'acquitter de sa passe de disposition rapidement tout en demeurant hautement réactif aux saisies. Il peut ensuite demander le reste du composant plus tard.

Un convertisseur d'espace réservé renvoie un objet avec deux propriétés au lieu d'une simple promesse :

  • element Composant de niveau supérieur dans la structure de l'élément qui permet de définir sa taille et sa forme et ne dépend pas des données de l'élément.
  • renderComplete Promesse qui est réalisée lorsque le reste du contenu du composant est créé. En d'autres termes, cette propriété renvoie la promesse à partir de la chaîne que vous avez démarrée avec itemPromise.then comme dans l'exemple précédent.

Le contrôle ListView est suffisamment intelligent pour savoir si votre convertisseur renvoie une promesse (cas élémentaire de l'exemple précédent) ou un objet avec les propriétés element et renderComplete (cas plus avancés). Ainsi, le convertisseur d'espace réservé équivalent (dans l'exemple) pour la fonction simpleRenderer précédente ressemble à ceci :

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' />");
})
};
}

Notez que l'affectation element.innerHTML peut être déplacée à l'intérieur de renderComplete, car la classe itemTempl dans le fichier css/scenario1.css de l'exemple stipule directement la largeur et la hauteur de l'élément. L'affectation est incluse dans la propriété element, car elle fournit le texte « … » par défaut dans l'espace réservé. Il serait également possible d'utiliser un composant img se rapportant à une petite ressource dans le package qui est partagée dans tous les éléments (et dont le rendu est par conséquent rapide).

Convertisseurs d'espaces réservés de recyclage

La prochaine optimisation, le convertisseur d'espace réservé de recyclage, n'apporte aucune nouveauté sur le front des promesses. Elle met en revanche l'accent sur un deuxième paramètre du convertisseur, nommé recycled. Il s'agit du composant racine d'un élément qui a déjà été rendu mais qui n'est plus visible. Ainsi, les composants enfants du composant « recycled » (recyclé) sont déjà en place et vous pouvez simplement remplacer les données et même transformer certains de ces composants. Ceci évite la plupart des coûteux appels de création de composants qui sont nécessaires pour créer un élément entièrement nouveau et permet d'accélérer le processus de rendu.

Le contrôle ListView peut fournir un composant « recycled » lorsque sa propriété loadingBehavior est définie sur « randomaccess ». Si le composant recycled est stipulé, vous pouvez simplement supprimer les données du composant (et ses enfants), le renvoyer comme espace réservé, puis indiquer d'autres données et créer des enfants supplémentaires (le cas échéant) dans renderComplete. Si aucun composant « recycled » n'est fourni (par exemple si le contrôle ListView est créé en premier ou si loadingBehavior est « incrémentiel »), vous devez créer entièrement le composant. Voici le code de l'exemple correspondant à cette 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";
})
};
}

Dans renderComplete, veillez à vérifier l'existence des composants que vous ne créez pas pour un nouvel espace réservé (par exemple label) et créez-les si cela est nécessaire.

Si vous souhaitez supprimer les éléments recyclés de manière plus générale, notez que vous pouvez attribuer une fonction à la propriété resetItem du contrôle ListView. Le code de cette fonction serait alors similaire à celui présenté ci-dessus. Il en va de même pour la propriété resetGroupHeader, car vous pouvez utiliser les fonctions de modèles pour les en-têtes de groupes comme pour les éléments. Nous en avons peu parlé, car les en-têtes de groupes sont beaucoup moins nombreux et les implications en termes de performances ne sont généralement pas les mêmes. La fonctionnalité existe néanmoins.

Convertisseurs à plusieurs phases

Tout ceci nous amène à l'avant-dernière optimisation, le convertisseur à plusieurs phases. Il prolonge le convertisseur d'espace réservé de recyclage aux images et autres données multimédias à chargement différé tant que le reste de l'élément n'est pas entièrement présent dans le DOM. Il retarde également des effets, tels que les animations, tant que l'élément n'est pas réellement à l'écran. Les utilisateurs se déplaçant souvent très rapidement au sein du contrôle ListView, il est logique de reporter de manière asynchrone les opérations les plus exigeantes jusqu'à ce que le contrôle parvienne à une position stable.

Le contrôle ListView offre les points de raccordement nécessaires lorsque les membres de item sont issus de itemPromise : une propriété nommée ready (une promesse) et deux méthodes, loadImage et isOnScreen, toutes les deux renvoyant plus de promesses. C'est-à-dire :

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

Voici comment les utiliser :

  • ready Renvoie cette promesse à partir du premier gestionnaire Terminé de votre chaîne. Cette promesse est réalisée lorsque l'intégralité de la structure du composant a été rendue et est visible. Cela signifie que vous pouvez associer une autre méthode thenà un gestionnaire Terminé dans lequel vous effectuez d'autres tâches postérieures à la visibilité, par exemple le chargement d'images.
  • loadImage Télécharge une image à partir d'un URI et l'affiche dans le composant img donné, ce qui renvoie une promesse qui est réalisée avec ce même composant. Vous joignez un gestionnaire Terminé à cette promesse, qui renvoie à son tour la promesse depuis isOnScreen. Notez que loadImage crée un composant img si aucun n'est fourni et le distribue à votre gestionnaire Terminé.
  • isOnScreen Renvoie une promesse dont la valeur de réalisation est une valeur booléenne qui indique si le composant est visible ou non. Dans les implémentations actuelles, il s'agit d'une valeur connue et la promesse est donc réalisée de manière synchrone. En l'incluant dans une promesse, cependant, elle peut être utilisée dans une chaîne plus longue.

Tout cela est illustré dans la fonction multistageRenderer de l'exemple, où la fin du chargement de l'image déclenche l'apparition en fondu d'une animation. Voici simplement ce qui est renvoyé par la promesse renderComplete :

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);
}
})

Même si un grand nombre d'opérations se déroulent, cette chaîne de promesses demeure élémentaire. La première opération asynchrone du convertisseur actualise les parties simples de la structure des composants de l'élément, par exemple le texte. Elle renvoie ensuite la promesse dans item.ready. Une fois cette promesse réalisée (ou, plus précisément, si cette promesse est réalisée), nous utilisons la méthode asynchrone loadImage de l'élément pour déclencher le téléchargement d'une image, ce qui renvoie la promesse item.isOnScreen depuis ce gestionnaire Terminé. Cela signifie que l'indicateur de visibilité onscreen est transmis au gestionnaire Terminé final de la chaîne. Lorsque (et si) cette promesse isOnScreen est réalisée (ce qui signifie que l'élément est réellement visible), nous pouvons effectuer les opérations appropriées, telles que des animations.

Je tiens à souligner le caractère hypothétique (avec le terme « si »), car il est à nouveau probable que l'utilisateur se déplace au sein du contrôle ListView pendant le déroulement de toutes ces opérations. L'enchaînement de toutes ces promesses permet au contrôle ListView d'annuler les opérations asynchrones lorsque ces éléments sont déplacés hors de l'écran et/ou hors des pages mises en mémoire tampon. Qu'il suffise de dire que le contrôle ListView a subi à un grand nombre de tests de performances !

Il est également important de se rappeler que nous utilisons la méthode then dans toutes ces chaînes, car nous renvoyons toujours une promesse depuis la fonction de rendu au sein de la propriété renderComplete. Nous ne sommes jamais à la fin de la chaîne dans ces convertisseurs et nous n'utiliserons donc jamais doneà la fin.

Traitement de miniatures par lot

La dernière optimisation constitue sans aucun doute le coup de grâce pour le contrôle ListView. Dans la fonction nommée batchRenderer, nous trouvons cette structure pour renderComplete (la plupart du code est omis) :

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);
}
})

Cela ressemble beaucoup à la fonction multistageRenderer, à l'exception de l'insertion de cet appel mystérieux destiné à la fonction thumbnailBatch entre l'appel item.loadImage et la vérification item.isOnScreen. La position de thumbnailBatch dans la chaîne indique que la valeur renvoyée doit être un gestionnaire Terminé qui renvoie lui-même une autre promesse.

Ce n'est pas très clair ? Nous y reviendrons en détail ! Mais d'abord, nous avons besoin d'informations complémentaires sur ce que nous tentons d'accomplir.

Si nous n'avions qu'un contrôle ListView avec un seul élément, les différentes optimisations de chargement ne seraient pas significatives. Mais le contrôle ListView comporte généralement de nombreux éléments et la fonction de rendu est appelée pour chacun d'entre eux. Dans la fonction multistageRenderer de la section précédente, le rendu de chaque élément déclenche une opération item.loadImage asynchrone qui télécharge sa miniature à partir d'un URI arbitraire. La durée de chaque opération peut être arbitraire. Ainsi, pour toute la liste, plusieurs appels loadImage peuvent se dérouler simultanément, avec le rendu de chaque élément attendant la fin de sa miniature en particulier. Jusque là, tout va bien.

Une des caractéristiques importantes qui n'est pas du tout visible dans la fonction multistageRenderer, cependant, est que le composant img de la miniature est déjà présent dans le DOM et que la fonction loadImage définit l'attribut src de cette image dès la fin du téléchargement. Ceci déclenche à son tour une mise à jour dans le moteur de rendu dès que nous revenons du reste de la chaîne de promesses, qui est essentiellement synchrone au-delà de ce point.

Il est alors possible que certaines des miniatures réintègrent rapidement le thread d'interface utilisateur. Ceci entraînera une trop grande évolution dans le moteur de rendu et des performances visuelles médiocres. Pour éviter cette situation, nous voulons créer entièrement ces composants imgavant qu'ils ne soient dans le DOM, puis les ajouter par lots afin qu''ils soient tous gérés au cours d'une passe de rendu unique.

L'exemple effectue ces opérations à l'aide de la fonction createBatch. La fonction createBatch est appelée une seule fois pour toute l'application et son résultat (une autre fonction) est stocké dans la variable nommée thumbnailBatch:

var thumbnailBatch;
thumbnailBatch = createBatch();

Un appel à cette fonction thumbnailBatch, comme je vais désormais la désigner, est à nouveau inséré dans la chaîne de promesses du convertisseur. L'objectif de cette insertion, étant donné la nature du code de traitement par lot que nous examinerons plus loin, est de regrouper un ensemble de composants img chargés, en les libérant pour d'autres traitements à intervalles appropriés. Ici encore, en regardant simplement la chaîne de promesses dans le convertisseur, un appel à thumbnailBatch() doit renvoyer une fonction de gestionnaire Terminé qui renvoie une promesse et la valeur de réalisation de cette promesse (en regardant la prochaine étape de la chaîne) doit être un composant img qui peut ensuite être ajouté au DOM. En ajoutant ces images au DOM après le traitement par lot, nous associons l'intégralité du groupe dans la même passe de rendu.

Cette différence entre la fonction batchRenderer et la fonction multistageRenderer de la section précédente est importante : dans la deuxième fonction, le composant img de la miniature existe déjà dans le DOM et est transmis à la méthode loadImage comme deuxième paramètre. Ainsi, lorsque la méthode loadImage définit l'attribut src de l'image, le rendu est mis à jour. Au sein de la fonction batchRenderer toutefois, ce composant img est créé séparément dans la méthode loadImage (où l'attribut src est également défini), mais le composant img n'est pas encore présent dans le DOM. Il n'est ajouté au DOM qu'à la fin de l'étape thumbnailBatch, ce qui lui permet de faire partie d'un groupe au sein de cette passe de disposition unique.

Examinons maintenant le fonctionnement de ce traitement par lot. Voici la fonction createBatch dans son intégralité :

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]();
}
}

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

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

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

Ici encore, la fonction createBatch est appelée une seule fois et son résultat thumbnailBatch est appelé pour chaque élément rendudans la liste. Le gestionnaire Terminé que thumbnailBatch génère est ensuite appelé dès qu'une opération loadImage se termine.

Ce gestionnaire Terminé aurait pu être inséré directement dans la fonction de rendu, mais nous essayons dans ce cas de coordonner les activités sur plusieurs éléments à la fois, et non élément par élément. Cette coordination est possible grâce aux deux variables créées et initialisées au début de la fonction createBatch : batchedTimeout, initialisée en tant que promesse vide, et batchedItems, initialisée en tant que tableau de fonctions vide à l'origine. createBatch déclare également une fonction, completeBatch, qui vide simplement batchedItems, en appelant chaque fonction du tableau :

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]();
}
}

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

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

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

Étudions maintenant ce qui se passe dans thumbnailBatch (la fonction renvoyée par createBatch), qui est à nouveau appelée pour chaque élément rendu. D'abord, nous annulons les variables batchedTimeout existantes et en recréons une immédiatement :

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

La deuxième ligne illustre le futur modèle de distribution/réalisation abordé dans le billet Tout sur les promesses <TODO: link>: elle demande d'appeler completeBatch après un délai de waitPeriod millisecondes (avec une valeur par défaut de 64 ms). Cela signifie que tant que la fonction thumbnailBatch est rappelée dans un intervalle de waitPeriod par rapport à un appel précédent, la variable batchTimeout est réinitialisée sur un autre délai waitPeriod. Et comme la fonction thumbnailBatch est appelée uniquement après la fin d'un appel item.loadImage, cela revient à dire que les opérations loadImage qui se terminent dans l'intervalle waitPeriod par rapport à la précédente seront incluses dans le même lot. Lorsque l'intervalle est supérieur à waitPeriod, le lot est traité (les images sont ajoutées au DOM) et le prochain lot commence.

Une fois ces délais d'expiration gérés, la fonction thumbnailBatch crée une nouvelle promesse qui charge simplement la fonction de répartiteur complète dans le tableau batchedItems :

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

Comme l'indique le billet Tout sur les promesses <TODO: link> n'oubliez pas qu'une promesse n'est ni plus ni moins qu'une construction de code, et c'est tout ce que nous avons ici. La promesse qui vient d'être créée n'est associée à aucun comportement asynchrone : nous ajoutons simplement la fonction de répartiteur complète c dans batchedItems. Il est évident toutefois que nous ne touchons pas au répartiteur tant que la fonction batchedTimeout n'est pas terminée de manière asynchrone. Il existe par conséquent une relation asynchrone dans ce cas. Lorsque le délai d'expiration se produit et que nous effaçons le lot (à l'intérieur de completeBatch), nous appelons les gestionnaires Terminé qui sont indiqués ailleurs dans delayedPromise.then.

Ceci nous amène aux dernières lignes de code dans createBatch, qui est la fonction que thumbnailBatch renvoie réellement. Cette fonction est exactement le gestionnaire Terminé qui est inséré dans l'intégralité de la chaîne de promesses du convertisseur.

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

En fait, insérons ce code directement dans la chaîne de promesses pour voir les relations que nous obtenons :

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

Nous voyons maintenant que l'argument v est le résultat de item.loadImage, qui est le composant img créé à notre intention. Si nous ne voulions pas du traitement par lot, nous pourrions simplement indiquer return WinJS.Promise.as(v) et toute la chaîne fonctionnerait parfaitement : v serait dans ce cas transmis de manière synchrone et apparaîtrait comme newimgà la prochaine étape.

Nous renvoyons toutefois à la place une promesse depuis delayedPromise.then qui ne sera pas réalisée (avec v) tant que la fonction batchedTimeout n'est pas réalisée. À ce stade (lorsqu'un délai de waitPeriod sépare la fin des opérations loadImage), ces composants img sont ensuite distribués à la prochaine étape de la chaîne, où ils sont ajoutés au DOM.

Et c'est tout !

Pour conclure

Les cinq différentes fonctions de rendu illustrées dans l'exemple d'optimisation des performances de contrôle HTML ListView ont toutes une caractéristique en commun : elles montrent comment la relation asynchrone entre le contrôle ListView et le convertisseur (exprimée au moyen de promesses) confère au convertisseur une extrême flexibilité quant à la manière dont il produit des composants pour les éléments de la liste et quant au moment où il les produit. Pour écrire vos propres applications, la stratégie que vous utilisez pour l'optimisation du contrôle ListView dépend énormément de la taille de votre source de données, de la complexité des éléments eux-mêmes et de la quantité de données que vous obtenez de manière asynchrone pour ces éléments (téléchargement d'images distantes, par exemple). Pour de nombreuses raisons, il est souhaitable de simplifier au maximum les convertisseurs d'éléments en vue de satisfaire vos objectifs de performances. Mais dans tous les cas, vous disposez maintenant de tous les outils dont vous avez besoin pour optimiser les performances du contrôle ListView et de votre application.

Kraig Brockschmidt
Chef de projet, équipe Écosystème Windows
Auteur de Programming Windows 8 Apps in HTML, CSS, and JavaScript

ListView の項目レンダリングの最適化

$
0
0

コレクションを利用する JavaScript を使って作成された Windows ストア アプリの場合、アプリのすばらしいパフォーマンスを実現するには、通常、WinJS ListView コントロール (英語) とのスムーズな連携がきわめて重要です。これは意外なことではなく、数千個にもなり得る項目の管理と表示を処理する場合、項目に対するすべての最適化がものを言います。最も重要なことは、各項目がどのように表示されるか、つまり、ListView コントロール内の各項目がどのように、また、どのタイミングで、DOM によって組み立てられ、アプリの一部として表示されるかです。実際、このプロセスの "タイミング" の部分は、リスト内ですばやくパン操作を行った場合に、そのスピードに遅れずにリストの表示が変わることが期待されている場合は、特に重要な要素になります。

ListView 内でレンダリングされる項目は、HTML で定義された宣言型テンプレートか、リスト内の項目ごとに呼び出されるカスタムの JavaScript "レンダリング関数" によって処理されます。宣言テンプレートを使う方法が最も簡単ですが、このプロセスに対して特定の制御を行う場合の自由度は限られます。一方、レンダリング関数を使うと、項目ごとにレンダリングをカスタマイズでき、HTML ListView パフォーマンス最適化のサンプル (英語) に実装されているように、複数の最適化が可能です。この関数では、次のような最適化を実現できます。

  • 項目データと表示要素の非同期配信の実現。これには、基本のレンダリング関数を使用します。
  • ListView の全体的なレイアウトを決めるために必要な、項目の形状の作成と、内部の要素との分離。これには、プレースホルダー レンダラーを使用します。
  • 以前作成した項目要素 (とその子) のデータを置き換えて再利用。要素作成の大半のステップを省略できます。これには、プレースホルダー再利用レンダラーを使用します。
  • 項目が表示され、ListView のパン操作のスピードが落ち着くまで、イメージの読み込みやアニメーションなど、負荷の高いビジュアル操作の遅延。これには、多段階レンダラーを使用します。
  • 同じビジュアル操作をまとめて、DOM の再レンダリングを最小限に抑制。これには、多段階レンダラーを使用します。

この記事では、上記の全ステージを取り上げ、各ステージが ListView の項目レンダリング プロセスとどのように連携するかを説明します。お察しのとおり、項目をレンダリングするタイミングの最適化には、非同期操作、ひいては promise を多用します。したがって、この記事を読み進むにつれて、promise 自体の理解も、このブログの前回の記事「promise の詳細」(英語) の知識からさらに深めることができます。

すべてのレンダラーに言えることですが、常に、中心となる項目のレンダリング時間 (遅延操作を除く) を最小限に抑えることが重要です。ListView の最終的なパフォーマンスは、ListView の更新と画面の更新間隔がどの程度同期するかに大きく左右されるため、1 つの項目レンダラーで数ミリ秒余分にかかると、次の画面の更新までの間の ListView 全体のレンダリング時間が増大し、結果としてフレーム落ちや画面の乱れが発生します。つまり、項目レンダラーは、JavaScript コードの最適化を図る場合、本当に有効な領域です。

基本レンダラー

まず、項目レンダリング機能 (以降は、シンプルに "レンダラー" と呼びます) とはどのようなものかを簡単に確認しましょう。レンダラーは、テンプレート名の代わりに、ListView の itemTemplate (英語) プロパティに割り当てられる関数で、この関数は ListView から DOM に追加される項目に対して、必要に応じて呼び出されます (レンダラーの基本的な情報については、itemTemplateを参照してください。ただし、この最適化が本当によくわかるのはサンプルの方です)。

項目レンダリング関数は、単純に ListView のデータ ソースから項目が渡されたら、その特定の項目に必要な HTML 要素を作成し、ListView が DOM に追加できるルート要素を返すと思われるでしょうか。基本的にはそうですが、さらに 2 点、考慮することがあります。まず、項目データ自体が非同期に読み込まれる可能性があるため、要素の作成をそのデータが提供されるタイミングに合わせる方がよいでしょう。また、項目自体のレンダリング プロセスには、リモート URI からのイメージの読み込みや、項目データで指定されている他のファイルのデータの読み取りなど、他の非同期作業も含まれる場合があります。この記事で説明するさまざまなレベルの最適化では、実際、項目の要素が要求されてから、要求された項目が実際に提供されるまでの非同期の度合いを自由に決定できます。

したがって、やはり、promise が使われると予測できます。まず、ListView では項目データを直接レンダラーに渡さず、そのデータの promise を提供します。また、項目のルート要素を直接返す関数の代わりに、その要素の promise を返します。この方法により、ListView ではさまざまな項目レンダリング promise を組み合わせて、ページ全体の項目がレンダリングされるまで、(非同期に) 待機できます。実際、このように処理することで、ListView では複数のページの作成方法を管理しています。まず、表示される項目のページを作成したら、ユーザーのパン操作の結果、この次に表示されると思われる前後の 2 種類のページを非表示で作成します。また、このような promise をすべて用意しておけば、ユーザーのパン操作によって画面が変わった場合、表示が完了していない項目のレンダリングを ListView によって簡単にキャンセルでき、無駄な要素作成を行わずに済みます。

これらの promise の使用方法は、サンプルの simpleRenderer関数を見るとわかります。

 

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;
});
}

このコードでは、最初に、completed ハンドラーを itemPromiseにアタッチしています。ハンドラーは、項目データが提供され、それを受けて要素が実際に作成された時点で呼び出されます。ただし、実は要素を直接返さず、その要素がフルフィルメント (提供) される promise を返していることに、改めて注意してください。つまり、itemPromise.then()からの戻り値は、ListView によって要素が必要になった場合に必要なタイミングで、"element"がフルフィルメントされる promise です。

promise を返すことで、必要に応じて他の非同期作業を実行できます。この場合は、レンダラーは単に中間の promise をチェーンにまとめ、チェーン内の最後の thenから promise を返しています。以下に例を示します。

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;
});
}

これは、最後の then呼び出しから promise を返しているので、チェーンの最後に doneを使わないケースになります。エラーが発生した場合は、ListView によってエラーは処理されます。

プレースホルダー レンダラー

ListView の最適化の次の段階では、要素の作成を 2 段階に分ける "プレースホルダー レンダラー" を使います。このレンダラーを使うことで、ListView から、リスト全体のレイアウトの定義に必要な要素の部分のみを要求でき、各項目内の要素すべてを作成する必要がありません。その結果、ListView はレイアウト パスを短時間で完成して、この後の入力でも高い応答性を保つことができます。パスが完成したら、後で、要素の残りの部分を要求できます。

プレースホルダー レンダラーでは、promise を 1 つだけ返すのではなく、以下の 2 つのプロパティを持つオブジェクトが 1 つ返されます。

  • "element": 項目の構造内の最上位の要素で、項目のサイズと形状を定義できる必要最低限の、項目データに依存しない要素を示します。
  • "renderComplete": 要素のコンテンツの残りが作成された時点で、フルフィルメントされる promise を示します。つまり、前と同様に itemPromise.thenで始まるチェーンから promise を返します。

ListView では、レンダラーが promise を返す (前と同様に基本のケース) か、elementおよび renderCompleteプロパティを返す (より高度なケース) かどうかを確認できます。したがって、前回の simpleRendererと同等のプレースホルダー レンダラー (サンプル内) は次のようになります。

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' />");
})
};
}

element.innerHTMLの割り当ては、renderComplete内に移動することもできます。これは、サンプルの css/scenario1.css ファイルの itemTemplクラスで、項目の幅と高さを直接指定しているためです。elementプロパティに含まれている理由は、プレースホルダーに既定の "…" テキストを提供するためです。方法は簡単で、すべての項目で共有される、小さいパッケージ内リソースを参照する (したがって短時間でレンダリングされる) img要素を使用するだけです。

プレースホルダーの再利用レンダラー

次の最適化の "プレースホルダーの再利用"レンダラーでは、promise に関しては、新しいものは何もありません。recycledというレンダラーにある 2 つ目のパラメーターに注目してください。これは、前にレンダリングされていて、現在は非表示になっている項目のルート要素です。つまり、再利用された要素には既に子要素が作成されているので、データを置き換えて、おそらく子要素のいくつかを調整するだけです。これにより、再利用をせず、まったく新しい項目をレンダリングする際に必要になる、不可の高い要素作成呼び出しのほとんどを回避し、レンダリング プロセスの時間を大幅に節約できます。

ListView は、loadingBehavior (英語) を "randomaccess" に設定すると、再利用した要素を提供できます。"recycled"が指定された場合、要素 (とその子要素) からデータを消去し、プレースホルダーとしてこれを返します。その後、データを設定し、(必要に応じて) renderComplete内に追加の子を作成します。再利用された要素が提供されない場合 (ListView が初めて作成されたか、loadingBehaviorが "incremental" の場合)、要素を新たに作成します。以下は、その動作のコードをサンプルから抜粋したものです。

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";
})
};
}

"renderComplete"では、"label"など、新しいプレースホルダーに作成しない要素があるかを必ず確認し、必要に応じてここで作成してください。

より一般的に再利用された項目からデータを消去する場合は、ListView の resetItem (英語) プロパティに関数を設定できます。この関数には、上記と同様のコードが含まれます。項目だけでなくグループ ヘッダーにもテンプレート関数を使用できるので、resetGroupHeader (プロパティ) についても同じことが言えます。グループ ヘッダーははるかに少なく、通常、予想されるパフォーマンスは同じになるので、これまでそれほど詳しくは説明していませんが、この機能は存在しています。

多段階レンダラー

次は、最後から 2 番目の最適化、"多段階レンダラー"です。このレンダラーは、プレースホルダー再利用レンダラーを拡張して、DOM に項目の残りの要素が完全に追加されるまで、イメージや他のメディアの読み込みを遅延します。また、項目が実際に画面に表示されるまで、アニメーションなどの効果も遅延します。これは、ユーザーが ListView 内ではかなりすばやくパンを行うことが多いので、ListView の動きが落ち着くまで、負荷の高い操作を非同期に遅延することが有効なためです。

ListView は、itemPromiseから生成された itemのメンバーとして必要なフックを提供します。これらは、readyというプロパティ (promise) と、(さらに) promise を返す loadImageおよび isOnScreenの 2 つのメソッドです。以下が、そのコードです。

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

これらの使い方を説明します。

  • readyチェーン内で最初に completed ハンドラーからこの promise を返します。この promise は、要素の構造全体がレンダリングされ、表示できるようになった時点で、フルフィルメントされます。つまり、completed ハンドラーを使って別の thenをチェーンし、そのチェーン内で、イメージの読み込みなど、他の表示後の作業を実行できます。
  • loadImage URI からイメージをダウンロードして、指定されている "img"要素内に URI 表示し、この要素によってフルフィルメントされる promise を返します。この promise に completed ハンドラーをアタッチします。このハンドラー自体は、isOnScreenから promise を返します。"img" 要素が提供されていない場合は、loadImageによって作成され、completed ハンドラーに渡されます。
  • isOnScreen項目が表示されるかどうかを示すブール値をフルフィルメント値として取る promise を返します。現在の実装では、これは既知の値になるため、promise は同期的にフルフィルメントされます。ただし、これを promise を使ってラップすることで、より長いチェーンで使用できます。

これらはすべてサンプルの multistageRenderer関数内で使われています。この関数では、イメージの読み込みの完了を合図に、フェード イン アニメーションを開始します。以下に、"renderComplete" promise から返される内容のみを示します。

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);
}
})

ここではさまざまな処理をしていますが、基本の promise チェーンしかないことに変わりありません。レンダラー内の最初の同期操作によって、テキストなど、項目の要素構造でシンプルな要素のみが更新されます。次に、item.readyで promise を返しています。promise がフルフィルメントされるとき (より正確には、その promise が実行される場合) に、項目の非同期の loadImageメソッドを使ってイメージのダウンロードを開始し、item.isOnScreen promise をその completed ハンドラーから返します。つまり、可視性を示す "onscreen"フラグがチェーン内で最後に completed ハンドラーに渡されます。その isOnScreen promise がフルフィルメントされる場合、つまり、項目が実際に表示される時点で、アニメーションなどの関連操作を実行できます。

"場合" を強調するのは、これらの処理が行われている最中に、ユーザーが ListView 内でパン操作を行う可能性があるためです。この場合も、これらの promise すべてをチェーンにまとめることで、これらの項目がパン操作によってビューやバッファー済みのページから外れた場合に、ListView では非同期操作をキャンセルできます。ListView コントロールに対して、ここまでで "さまざまな" パフォーマンス テストが実行されたと言えるでしょう。

また、やはり "renderComplete" プロパティ内のレンダリング関数から promise を返しているため、これらのどのチェーンでも全体で thenを使用していることに改めて注意してください。これらのレンダラー内でチェーンの最後に到達することはないため、doneをレンダラーの最後に使うことはありません。

縮小表示のバッチ処理

最後の最適化は、まさに ListView コントロールにとって "最後の一撃" です。以下のコードの batchRendererという関数内に、"renderComplete" を対象とするこの構造があります (ほとんどのコードは省略)。

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);
}
})

これは、multistageRendererとほぼ同じですが、item.loadImage呼び出しと item.isOnScreenチェックの間にある、この不思議な thumbnailBatchという関数への呼び出しが挿入されている点が異なります。チェーン内に thumbnailBatchがある場合、これは、その戻り値が、別の promise を返す completed ハンドラーになる必要があることを示しています。

わかりにくいでしょうか? これについては、詳しく説明しますが、最初に、ここで何をしようとしているか、少し背景を説明する必要があります。

項目が 1 つしかない ListView だけなら、さまざまな読み込みの最適化の効果はわからないでしょう。しかし、通常、ListViews には多数の項目があり、レンダリング関数は項目ごとに呼び出されます。前のセクションの multistageRenderer内で、各項目のレンダリングによって、非同期の item.loadImage操作が開始され、任意の URI から縮小表示がダウンロードされます。この操作のそれぞれに、時間がかかります。したがって、リスト全体では、多数の loadImage呼び出しが同時に実行され、特定の縮小表示が完成するのを各項目のレンダリングが待機することになる可能性があります。ここまではよいでしょう。

ただし、multistageRendererからは計り知れない重要な特徴は、縮小表示の "img"要素は "既に" DOM 内にあり、ダウンロードの完了しだい、loadImage関数によってそのイメージの src属性が設定されることです。属性が設定されたら、promise チェーンの残りが返されしだい、レンダリング エンジンが更新されます。これ以降は、基本的に同期的に処理されます。

さらにその後、多数の縮小表示が、短時間のうちに UI スレッドに返される可能性があります。これにより、レンダリング エンジンに過剰なチャーンが発生し、ビジュアル パフォーマンスがかなり低下します。このチャーンを避けるため、これらの "img"要素が DOM に渡される "前"に、完全に img 要素を作成して、バッチに追加し、これらの要素がすべて単一のレンダリング パスで処理されるようにします。

サンプルでは、promise の一部 (createBatchという魔法の関数) で、これを実装しています。createBatchは、アプリ全体で 1 度だけ呼び出され、その結果 (別の関数) は thumbnailBatchという名前の変数に格納されます。

var thumbnailBatch;
thumbnailBatch = createBatch();

この thumbnailBatch関数への呼び出しは、これ以降参照することになりますが、やはりレンダラーの promise チェーンに挿入されます。この挿入の目的は、この後すぐに説明するバッチ処理コードの性質を踏まえて、読み込まれた複数の "img"要素をグループにまとめ、適切な間を置いて次の処理に渡すことです。レンダラーの promise チェーンだけを見ても、やはり thumbnailBatch()の呼び出しは、promise を返す completed ハンドラー関数を返す必要があります。また、その promise のフルフィルメント値 (チェーンの次のステップを参照) は、DOM に追加できる "img"要素である必要があります。バッチ処理が実行された "後"で、これらのイメージを DOM に追加することで、グループ全体を同じレンダリング パスにまとめています。

これは、batchRendererと、前のセクションの multistageRendererとの重要な違いです。後者では、縮小表示の "img"要素が既に DOM 内に存在し、2 番目のパラメーターとして loadImageに渡されます。したがって、loadImageがイメージの "src"属性が設定されるときに、レンダリングの更新が発生します。ただし、batchRenderer内では、その "img"要素は、loadImage内 (ここでも "src"が設定されている) で別に作成されますが、"img"はまだ DOM に追加されていません。thumbnailBatchステップが完了しないと DOM に追加されません。これにより、同一のレイアウト パス内にグループを組み込んでいます。

では、このバッチ処理のしくみを見ていきましょう。以下は、createBatch関数の全容です。

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]();
}
}

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

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

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

この場合も、createBatchは "1 度だけ"呼び出され、その結果の thumbnailBatchがリスト内のレンダリングされた項目のそれぞれに対して呼び出されています。thumbnailBatchが生成した completed ハンドラーは、loadImage操作が完了するたびに呼び出されます。

このような completed ハンドラーは、同程度の手間で、直接レンダリング関数に挿入することもできますが、ここでは、項目ごとではなく、複数の項目全体でアクティビティを調整することが目的です。この調整は、createBatchの始めに作成および初期化される、2 つの変数によって実現されます。空の promise を初期化している "batchedTimeout"と、最初は空の関数の配列を初期化している "batchedItems"です。createBatchでは関数 completeBatchも宣言していて、配列内の各関数を呼び出し、batchedItemsを空にします。

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]();
}
}

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

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

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

今度は、thumbnailBatch (createBatchから返される関数) 内の処理を見ていきましょう。これも、レンダリングされる項目ごとに呼び出されます。最初に、既存の batchedTimeoutをキャンセルし、すぐに作成し直します。

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

2 行目は、「promise の詳細」<TODO: link>の記事で説明している、将来の配信/フルフィルメント パターンです。ここでは、"waitPeriod" に指定された遅延 (ミリ秒単位、既定値は 64 ミリ秒) の後に、completeBatchを呼び出しています。したがって、thumbnailBatchが以前の呼び出しの "waitPeriod"で再び呼び出される限り、batchTimeoutは新しい waitPeriodにリセットされます。また、thumbnailBatchは、item.loadImage呼び出しが完了した "後で" しか呼び出されないため、以前の呼び出しの "waitPeriod" 内で完了する loadImage操作は同じバッチ内に含めることができます。"waitPeriod"に指定されている間隔を過ぎると、バッチが処理され (DOM にイメージが追加され)、次のバッチが開始されます。

このタイムアウト操作が処理されたら、thumbnailBatchによって新しい promise が作成され、この promise が完了ディスパッチャー関数を "batchedItems"配列にプッシュします。

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

「promise の詳細」<TODO: link>では、promise はコード コンストラクトに過ぎず、ここにはそれしかありません。新しく作成される promise 自体には、非同期動作はありません。完了ディスパッチャー関数の "c"を "batchedItems"に追加しているだけです。ただし、もちろん、"batchedTimeout"が非同期に完了するまで、ディスパッチャーによる処理は何も実行されないため、ここでは実際に非同期な関係が成立しています。タイムアウトが発生し、バッチを消去したら (completeBatch内)、別の場所にある completed ハンドラーを delayedPromise.thenに呼び出します。

今度は、createBatchの最後のコード行です。これは、thumbnailBatchが実際に返される関数です。この関数こそが、レンダラーの promise チェーン全体に挿入される completed ハンドラーです。

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

実際、このコードを直接 promise チェーンに挿入し、最終的な関係を見てみましょう。

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

これで、引数 "v"item.loadImageの結果であることがわかります。これは作成された "img"要素です。バッチ処理を行わない場合は、return WinJS.Promise.as(v)と記述するだけでも、チェーン全体は機能します。その場合 "v"は同期的に渡され、次のステップの "newimg"に設定されます。

しかし、ここでは、delayedPromise.thenから promise を返しています。これは、現在の "batchedTimeout" がフルフィルメントされるまで、("v" によって) 実行されません。その時点で (ここでも 1 つ目の loadImageが完了してから "waitPeriod"で指定されている間隔が空けられます)、"img"要素がチェーン内の次のステップに渡され、そこで DOM に追加されます。

以上です。

終わりに

HTML ListView パフォーマンス最適化のサンプル (英語) に実装されている 5 種類のレンダリング関数すべてに共通することが、1 つあります。それは、promise によって表現される、ListView とレンダラー間の非同期の関係によって、レンダラーがリスト内の項目の要素を生成する方法とタイミングを非常に柔軟にコントロールできることです。独自のアプリを作成する際に、ListView の最適化にどのような戦略を使うかは、データ ソースのサイズ、項目自体の複雑さ、(リモート イメージのダウンロードなど) それらの項目に非同期で取得するデータの量に大きく依存します。それでも、パフォーマンス目標を達成できるように、項目レンダラーはできるだけシンプルにしたいと思うのは当然です。しかし、いずれにせよ、ListView (とアプリ) のパフォーマンスを最大限引き出すために必要なツールは、これですべて揃いました。

Kraig Brockschmidt
- Windows エコシステム担当チーム、プログラム マネージャー
著書『HTML、CSS、JavaScript を使った Windows 8 アプリ開発』(英語)

ListView 항목 렌더링 최적화

$
0
0

컬렉션을 지원하도록 JavaScript로 작성된 Windows 스토어 앱의 성능에 가장 큰 영향을 미치는 것은 WinJS ListView 컨트롤과의 원활한 연동입니다. 사실 별로 놀라운 일은 아닙니다. 수많은 항목을 관리하고 표시할 때 항목 최적화 하나하나가 모두 중요합니다. 그 중에서도 가장 중요한 것은 각 항목을 렌더링하는 방법입니다. 즉, ListView 컨트롤의 각 항목을 언제, 어떻게 DOM에 구축하고 앱에서 표시할 것인지가 가장 중요합니다. 사실, 사용자가 신속하게 목록을 둘러보고 목록의 페이스를 유지하려고 할 때에는 언제와 어디서 중 '언제'가 더 중요합니다.

ListView의 항목 렌더링은 HTML에 정의된 선언적 템플릿 또는 목록의 모든 항목에 대해 호출되는 사용자 지정 JavaScript 렌더링 함수를 통해 이루어집니다. 선언적 템플릿을 사용하는 것이 가장 간단한 방법이지만 전체 프로세스에서 특정 컨트롤을 융통성 있게 사용할 수 없다는 단점이 있습니다. 한편 렌더링 함수는 항목별로 렌더링을 사용자 지정할 수 있어서 HTML ListView 성능 최적화 샘플에서 보여 준 것처럼 다양한 최적화가 가능합니다. 최적화의 종류는 다음과 같습니다.

  • 기본 렌더링 함수에서 지원하는 기능을 사용하여 항목 데이터 및 렌더링된 요소를 비동기식으로 제공할 수 있습니다.
  • ListView의 전체적인 레이아웃에 필요한 항목 모양 생성을 내부 요소와 분리할 수 있습니다. 이 기능은 자리 표시자 렌더러를 통해 지원됩니다.
  • 데이터를 대체하여 이전에 생성한 항목 요소 및 그 하위 요소를 재사용함으로써 요소 생성 단계 대부분을 생략하고 자리 표시자 렌더러를 재활용할 수 있습니다.
  • 항목이 표시되고 ListView가 빠른 속도로 이동되지 않을 때까지 이미지 로딩이나 애니메이션처럼 리소스 소모가 많은 시각화 작업을 지연합니다. 이 기능은 다단계 렌더러를 통해 수행됩니다.
  • 다단계 일괄 처리 렌더러를 통해 동일한 시각화 작업을 일괄 처리함으로써 DOM 재렌더링을 최소화합니다.

이 글에서는 이러한 단계를 살펴보고 ListView의 항목 렌더링 프로세스와 어떻게 연동하는지 알아보겠습니다. 어느 정도는 예상하시겠지만 항목 렌더링의 '시기'를 최적화하는 문제는 비동기 작업과 관련이 깊기 때문에 다양한 promise가 나오게 됩니다. 그러므로 이 블로그의 이전 글 promise의 모든 것을 바탕으로 promise 자체에 대해서도 자세히 알아볼 것입니다.

모든 렌더러에 적용되는 공통 사항으로, 언제나 핵심 항목 렌더링 시간(지연된 작업은 제외)을 최소로 유지하는 것이 중요합니다. ListView의 최종 성능은 업데이트가 화면 새로 고침 간격과 얼마나 일치하느냐에 따라 크게 좌우됩니다. 항목 렌더러에 소비하는 시간(밀리초)이 조금만 길어져도 ListView의 전체 렌더링 시간이 다음 새로 고침 간격까지 늘어나서 프레임이 손실되고 화면 끊김 현상이 발생합니다. 다시 말해서, 항목 렌더러는 JavaScript 코드 최적화에서 매우 중요한 역할을 합니다.

기본 렌더러

항목 렌더링 함수, 줄여서 '렌더러'가 어떤 모양인지 신속하게 살펴보겠습니다. 렌더러는 ListView의 itemTemplate속성에 템플릿 이름 대신 할당하는 함수로, 필요에 따라 ListView가 DOM에 포함하려는 항목에 대해 호출됩니다. (렌더러에 대한 기본 설명서는 itemTemplate페이지에서 찾을 수 있지만 이 페이지에서는 최적화만을 간단하게 보여 줍니다.)

여러분은 항목 렌더링 함수에 ListView 데이터 원본의 항목이 제공되고, 그 후 해당 항목에 필요한 HTML 요소를 생성한 후 ListView가 DOM에 추가할 수 있는 루트 요소를 반환할 것으로 예상하실 것입니다. 기본적으로는 이 예상이 맞지만 두 가지 사항을 더 고려해야 합니다. 첫째, 항목 데이터 자체는 비동기적으로 로드되기 때문에 요소 생성을 해당 데이터의 가용성과 연결할 수 있습니다. 뿐만 아니라 항목 자체를 렌더링하는 프로세스에 원격 URI에서 이미지를 로드한다거나 항목 데이터에서 확인된 다른 파일을 읽는 등의 다른 비동기 작업이 관련될 수 있습니다. 앞으로 보게 될 다양한 최적화 수준에서 항목 요소를 요청하고 해당 요소를 실제로 제공하는 사이에 수많은 비동기 작업이 가능합니다.

그렇기 때문에 이번에도 promise가 관련될 것으로 예상할 수 있습니다! 첫 번째 근거로, ListView는 항목 데이터에 렌더러를 직접 제공하지 않고 해당 데이터에 대한 promise를 제공합니다. 그리고 함수는 항목의 루트 요소를 직접 반환하지 않고 해당 요소에 대한 promise를 반환합니다. 따라서 항목으로 구성된 전체 페이지가 렌더링될 때까지 ListView가 여러 항목 렌더링 promise를 함께 연결하여 기다릴(비동기) 수 있습니다. 이러한 방법을 통해 여러 페이지를 지능적으로 구축합니다. 시각적 항목으로 구성되는 페이지를 먼저 구축한 후 그 페이지 앞뒤로 사용자가 그 다음으로 이동할 확률이 높은 오프스크린 두 페이지를 구축합니다. 뿐만 아니라 이러한 promise를 모두 제 위치에 배치하면 사용자가 페이지를 이동할 때 ListView가 완료되지 않은 항목에 대한 렌더링을 쉽게 취소할 수 있으므로 불필요한 요소를 생성하지 않을 수 있습니다.

다음 샘플을 통해 이러한 promise가 simpleRenderer함수에서 어떻게 사용되는지 살펴보겠습니다.

 

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;
});
}

이 코드는 가장 먼저 completed 처리기를 itemPromise에 연결합니다. 항목 데이터가 제공되면 처리기가 호출되어 호출에 대한 응답으로 요소를 효과적으로 생성합니다. 다시 한 번 강조하지만 요소를 직접 반환하는 것이 아니라 해당 요소로 처리되는 promise를 반환하는 것입니다. 즉, itemPromise.then()의 반환 값은 ListView가 '요소'를 필요로 할 때 해당 요소에 의해 처리되는 promise입니다.

promise를 반환하기 때문에 필요하다면 다른 비동기 작업을 수행할 수 있습니다. 이 사례에서 렌더러는 중간 promise를 연결하여 체인의 마지막 then에서 해당 promise를 반환할 수 있습니다. 예를 들면 다음과 같습니다.

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;
});
}

이 예에서는 마지막 then호출에서 promise를 반환하기 때문에 체인의 마지막에 done을 사용하지 '않는다'는 점에 주의하시기 바랍니다. 발생하는 모든 오류는 ListView에서 처리합니다.

자리 표시자 렌더러

ListView 최적화의 다음 단계에서는 요소 구축을 두 단계로 나누는 '자리 표시자 렌더러'를 사용합니다. 이렇게 하면 ListView가 각 항목 내에 모든 요소를 구축하지 않고도 목록의 전체적인 레이아웃을 정의하는 데 필요한 요소 부분만 요청할 수 있습니다. 결과적으로 ListView가 레이아웃 단계를 신속하게 완료하고 추가 입력에 대해 계속해서 민첩하게 반응할 수 있습니다. 그리고 나머지 요소는 나중에 요청할 수 있습니다.

자리 표시자 렌더러는 하나의 promise만을 반환하는 것이 아니라 다음과 같은 두 가지 속성을 가진 개체를 반환합니다.

  • element - 크기 및 모양을 정의하기에 충분하고 항목 데이터에 종속되지 않는 항목 구조의 최상위 요소입니다.
  • renderComplete - 남은 요소의 콘텐츠가 구성될 때 처리되는 promise입니다. 다시 말해서 앞에서 본 것처럼 itemPromise.then으로 시작하는 체인에서 promise를 반환합니다.

ListView는 렌더러가 promise를 반환하는지(이전과 같은 기본 사례) 아니면 'element' 및 'renderComplete' 속성을 반환하는지(좀 더 발전된 사례) 확인할 수 있을 정도로 똑똑합니다. 따라서 이전 simpleRenderer와 동등한 자리 표시자 렌더러는 다음과 같습니다.

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' />");
})
};
}

샘플의 css/scenario1.css 파일의 'itemTempl' 클래스가 항목의 너비와 높이를 직접 지정하기 때문에 'element.innerHTML'의 배치를 renderComplete내부로 이동할 수 있습니다. 'element' 속성에 포함된 것은 자리 표시자에 기본 “…” 텍스트를 제공하기 때문입니다. 개발자는 모든 항목 간에 공유하는(그래서 렌더링 속도가 빠른) 작은 인패키지(in-package) 리소스를 참조하는 'img' 요소를 간편하게 사용할 수 있습니다.

자리 표시자 렌더러 재활용

다음 최적화인 '자리 표시자 재활용' 렌더러는 promise와 관련이 있는 위치에 새 항목을 전혀 추가하지 않습니다. 그보다는 앞에서 렌더링되었지만 더 이상 표시되지 않는 항목의 루트 요소인 'recycled'라는 렌더러에 두 번째 매개 변수의 존재를 인식시킵니다. 즉, recycled 요소의 하위 요소가 이미 제 위치에 있기 때문에 개발자가 데이터를 대체하거나 일부 요소를 수정할 수 있습니다. 완전히 새로운 항목을 추가할 경우 리소스가 많이 드는 요소 생성 호출 과정이 필요한데, 이 과정을 대부분 생략할 수 있으므로 렌더링 프로세스 시간을 크게 절약할 수 있습니다.

loadingBehavior를 "randomaccess"로 설정할 경우 ListView가 recycled 요소를 제공할 수 있습니다. 'recycled'가 제공되면 요소 및 하위 요소에서 데이터를 삭제하고 자리 표시자로 반환한 다음 데이터를 채우고 필요하다면 'renderComplete' 내에 하위 요소를 추가할 수 있습니다. ListView가 맨 처음으로 생성되거나 loadingBehavior가 "incremental"로 설정되어 있어서 recycled 요소가 제공되지 않을 경우 요소를 새로 생성해야 합니다. 다음은 이러한 변형에 대한 샘플 코드입니다.

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";
})
};
}

'renderComplete'에서 새 자리 표시자에 대해 생성하지 않은 요소(예: 'label')가 있는지 확인하고 필요하다면 요소를 생성합니다.

좀 더 일반적인 방법으로 recycled 항목을 제거하려면 ListView의 resetItem속성에 함수를 제공하는 방법을 고려해 보십시오. 이 함수는 위에서 보여 준 것과 비슷한 코드를 갖고 있습니다. resetGroupHeader속성도 동일합니다. 그룹 헤더뿐만 아니라 항목에도 템플릿 함수를 사용할 수 있기 때문입니다. 그룹 헤더는 그 수가 훨씬 적고 일반적으로 성능에 미치는 영향이 같지 않기 때문에 자세히 다루지는 않았습니다. 하지만 그 성능은 보시는 바와 같습니다.

다단계 렌더러

이번에 살펴 볼 최적화는 '다단계 렌더러'입니다. 이 최적화에서는 나머지 항목이 DOM에 모두 표시될 때까지 재활용 자리 표시자 렌더러를 로드 지연 이미지 및 기타 미디어로 확장합니다. 또한 항목이 실제로 화면에 나타날 때까지 애니메이션 같은 효과를 지연합니다. 이를 통해 사용자가 ListView를 신속하게 이동하는 것을 인식하므로 ListView가 안정적인 상태가 될 때까지 리소스가 많이 필요한 작업을 비동기적으로 지연할 수 있습니다.

item의 멤버는 itemPromise즉, ready라는 속성(promise)과 두 개의 메서드 loadImageisOnScreen에서 오기 때문에 ListView가 필요한 후크를 제공하며 둘 모두 (더 많은) promise를 반환합니다. 예를 들어 다음과 같습니다.

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

사용 방법은 다음과 같습니다.

  • ready체인의 첫 번째 completed 처리기에서 이 promise를 반환합니다. 이 promise는 요소의 전체 구조가 렌더링되어 표시될 때 처리됩니다. 즉, 또 다른 then을 이미지 로딩 등의 시각화 후속 작업을 하는 completed 처리기와 체인으로 연결할 수 있습니다.
  • loadImage URI에서 이미지를 다운로드하여 제공된 'img' 요소에 표시하고, 같은 요소에 의해 처리되는 promise를 반환합니다. 이 promise에 completed 처리기를 연결하면 스스로 isOnScreen에서 promise를 반환합니다. img 요소를 제공하지 않을 경우 loadImage'img' 요소를 생성하여 completed 처리기에 제공합니다.
  • isOnScreen처리 값이 항목의 표시 여부를 나타내는 부울인 promise를 반환합니다. 현재 구축에서는 알려진 값이기 때문에 promise가 동기적으로 처리됩니다. 하지만 promise에 래핑하면 보다 긴 체인에 사용할 수 있습니다.

이 모든 것을 적용한 것이 아래 샘플의 multistageRenderer함수이며, 이미지 로드가 완료되는 위치에서 페이드 인 애니메이션이 시작됩니다. 'renderComplete' promise에서 무엇이 반환되는지 잘 보시기 바랍니다.

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);
}
})

다양한 작업이 진행되지만 여전히 기본 promise 체인만을 사용하고 있습니다. 렌더러의 첫 번째 비동기 작업은 항목 요소 구조에서 텍스트처럼 간단한 부분을 업데이트하는 것입니다. 그러면 item.ready에서 promise가 반환됩니다. 해당 promise가 처리될 때, 좀 더 정확하게 말해서 해당 promise가 처리될 경우 항목의 비동기 loadImage메서드를 사용하여 이미지 다운로드를 시작하면 해당 completed 처리기에서 item.isOnScreen promise가 반환됩니다. 즉, '화면' 가시성 플래그가 체인의 마지막 completed 처리기에 전달됩니다. isOnScreen promise가 처리될 때, 조금 더 정확히 말해서 처리될 경우, 즉 항목이 실제로 표시될 때 애니메이션과 같은 관련 작업을 수행할 수 있습니다.

처리될 경우를 강조해서 말씀 드렸는데, 이 동작이 수행될 때 사용자가 ListView 내부를 돌아다니고 있을 확률이 매우 높기 때문입니다. 모든 promise를 체인으로 연결해 놓으면 항목이 화면에 표시되지 않거나 버퍼링된 페이지가 사라질 때 ListView가 비동기 작업을 취소할 수 있습니다. 이 정도면 ListView 컨트롤이 '수많은' 성능 테스트를 거쳤다는 충분한 증거가 되지 않을까요?

또한 여전히 'renderComplete' 속성 내의 렌더링 함수에서 promise를 반환하고 있기 때문에 모든 체인에서 then을 사용한다는 점을 다시 한 번 강조하겠습니다.이러한 렌더러에서 체인 마지막까지 갈 일이 없기 때문에 마지막 부분에 done을 사용할 일도 없습니다.

미리 보기 일괄 처리

마지막 최적화는 ListView 컨트롤을 향해 날리는 '마지막 일격'입니다.batchRenderer라는 함수를 보면 다음과 같은 renderComplete 구조를 확인할 수 있습니다(대부분의 코드 생략).

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);
}
})

item.loadImage호출과 item.isOnScreen확인 사이에 thumbnailBatch라는 이 신기한 함수를 호출하는 것만 빼면 multistageRenderer와 거의 차이가 없습니다. 체인에 thumbnailBatch를 배치했기 때문에 그 반환 값은 자체적으로 또 다른 promise를 반환하는 completed 처리기여야 합니다.

어려운가요? 그렇다면 자세히 알아보겠습니다. 하지만 그 전에 우리가 하려는 것에 대한 배경 지식을 좀 더 쌓아야 합니다.

ListView에 항목이 하나밖에 없다면 다양한 로드 최적화가 눈에 띄지 않을 수 있습니다. 하지만 일반적으로 ListView에는 여러 항목이 있고 각 항목마다 렌더링 함수가 호출됩니다. 이전 단원의 multistageRenderer에서 각 항목을 렌더링하면 비동기 item.loadImage작업이 시작되어 임의의 URI에서 축소판이 다운로드되며, 각 작업마다 임의의 시간이 배정될 수 있습니다. 따라서 목록 전체를 놓고 보면 여러 loadImage호출이 동시에 진행되고, 각 항목의 렌더링은 특정 미리 보기가 완료될 때까지 기다립니다. 여기까지는 별 문제가 없을 것입니다.

하지만 multistageRenderer에서 전혀 보이지 않는 중요한 특징 한 가지가 있습니다. 그것은 바로 미리 보기의 'img' 요소가 '이미' DOM에 있으며, 다운로드가 완료되는 즉시 loadImage함수가 해당 이미지의 'src' 특성을 설정한다는 것입니다. 결국 promise 체인의 나머지 부분에서 돌아오는 즉시 렌더링 엔진에서 업데이트가 트리거되고, 해당 시점 이후부터는 근본적으로 동기 상태가 됩니다.

따라서 짧은 시간 내에 다수의 미리 보기가 UI 스레드로 돌아올 수 있고 그렇게 될 경우 렌더링 엔진에 과도한 변동이 일어나 표시 성능이 떨어지게 됩니다. 과도한 변동을 피하려면 이러한 'img' 요소가 DOM에 있기 '이전에' img 요소를 완전히 생성한 후 단일 렌더링 경로에서 모두 처리할 수 있도록 img 요소를 일괄적으로 추가해야 합니다.

샘플에서는 createBatch라는 마법과 같��� promise 코드 함수를 통해 이 작업을 처리했습니다. createBatch는 전체 앱에 대해 단 한 번만 호출되며, 그 결과(또 다른 함수)는 thumbnailBatch라는 변수에 저장됩니다.

var thumbnailBatch;
thumbnailBatch = createBatch();

지금부터 설명할 이 thumbnailBatch함수를 호출하면 다시 렌더러의 promise 체인으로 삽입됩니다. 이렇게 삽입하는 이유는 잠시 후 보게 될 일괄 처리 코드의 특성을 감안한 것으로, 로드된 'img' 요소를 그룹화해 두었다가 나중에 적절한 간격으로 릴리스하여 처리하기 위함입니다.다시 한 번 강조하지만, 렌더러의 promise 체인을 살펴보면 thumbnailBatch()호출의 결과로 promise를 반환하는 completed 처리기가 반환되어야 하며, 해당 promise의 처리 값(체인의 다음 단계를 살펴볼 것)은 이후에 DOM에 추가할 수 있는 'img' 요소여야 합니다. 일괄 처리 '후' DOM에 이미지를 추가하여 전체 그룹을 동일한 렌더링 경로로 결합할 수 있습니다.

이것이 바로 이전 단원에서 본 batchRenderermultistageRenderer의 결정적인 차이점입니다. 후자의 경우 미리 보기의 'img' 요소가 이미 DOM에 있으며 loadImage에 두 번째 매개 변수로 전달됩니다. 따라서 loadImage가 이미지의 'src' 특성을 설정할 때 렌더링 업데이트가 트리거됩니다. batchRenderer내에서는 'img' 요소가 loadImage내에 별도로 생성되지만('src' 또한 설정됨) 'img'가 아직 DOM에 없습니다. thumbnailBatch단계가 완료된 후에만 DOM에 추가되어 단일 레이아웃 단계 내에 있는 그룹에 속하게 됩니다.

그러면 지금부터 일괄 처리 작업이 어떻게 수행되는지 살펴보겠습니다. 다음은 완전한 createBatch함수입니다.

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]();
}
}

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

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

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

다시 한 번 말씀 드리지만 createBatch는 '한 번만' 호출되며 목록의 '각' 렌더링된 항목마다 thumbnailBatch결과가 호출됩니다. 그 후 loadImage작업이 완료될 때 thumbnailBatch에서 생성하는 completed 처리기가 호출됩니다.

이러한 completed 처리기를 아주 간단하게 렌더링 함수에 직접 삽입할 수 있지만 여기서 우리가 하려는 것은 항목별 조정 작업이 아닌 '여러 항목에 걸친' 조정 작업입니다. 이 조정 작업은 createBatch시작 부분에서 생성 및 초기화되는 두 개의 변수를 통해 수행됩니다. 하나는 빈 promise로 초기화되는 'batchedTimeout'이고, 다른 하나는 처음에는 비어 있는 여러 함수를 초기화하는 'batchedItems'입니다. createBatch역시 completeBatch함수를 선언하며, 이 함수는 'batchedItems'를 비우고 배열의 각 함수를 호출합니다.

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]();
}
}

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

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

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

렌더링되는 각 항목에 대해 다시 호출되는 thumbnailBatch(createBatch에서 반환되는 함수) 내에서 어떤 일이 벌어지는지 살펴봅시다. 먼저 기존 'batchedTimeout'을 '취소'하고 바로 다시 만듭니다.

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

두 번째 줄을 보면 Promises의 모든 것<TODO: link>에서 살펴본 미래의 제공/처리 패턴이 보입니다. 'waitPeriod' 밀리초(기본값은 64밀리초) 후 completeBatch호출하라고 합니다. 다시 말해서 이전 호출의 'waitPeriod' 내에 thumbnailBatch를 다시 호출할 경우 batchTimeout이 또 다른 waitPeriod에 대해 설정된다는 뜻입니다.그리고 item.loadImage호출이 완료된 '후' thumbnailBatch만 호출되기 때문에 이전 호출의 'waitPeriod' 내에 완료되는 모든 loadImage작업이 같은 배치에 포함된다고 자신 있게 말할 수 있습니다.'waitPeriod'보다 더 긴 간격이 있을 경우 배치가 처리되고(이미지가 DOM에 추가되고) 다음 배치가 시작됩니다.

이 시간 제한 작업을 처리한 후 thumbnailBatch가 complete 디스패처 함수를 'batchedItems' 배열에 단순히 푸시하는 새로운 promise를 생성합니다.

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

Promise의 모든 것 <TODO: link>에서 promise는 단지 코드 구조일 뿐이라고 했던 것을 기억하시기 바랍니다. 오늘 우리가 하는 모든 것이 그 연장선에 있습니다. 새로 생성된 promise는 그 자체로는 비동기 동작이 전혀 없습니다. 우리는 지금 complete 디스패처 함수 'c'를 'batchedItems'에 추가하고 있습니다. 물론 'batchedTimeout'이 비동기적으로 완료되기 전에는 디스패처에 아무 것도 하지 않습니다. 따라서 여기서는 사실상 비동기 관계가 하나 있습니다. 시간 제한이 발생하고 completeBatch내의 배치를 비울 경우 다른 곳에서 delayedPromise.then에 제공되는 completed 처리기를 호출하게 됩니다.

그러면 thumbnailBatch의 코드 마지막 줄로 이동합니다. 이 함수는 createBatch가 실제로 반환하는 함수입니다. 이 함수는 렌더러의 전체 promise 체인에 삽입되는 바로 그 completed 처리기입니다.

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

이 코드를 promise 체인에 바로 넣어서 그 결과로 나타나는 관계를 보겠습니다.

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

이제 인수 'v'가 item.loadImage의 결과이고, 그 결과는 우리가 원하는 'img' 요소라는 것을 알 수 있습니다. 일괄 처리를 원하지 않을 경우 return WinJS.Promise.as(v)만 사용해서 체인 전체가 작동하도록 하면 됩니다. 그렇게 하면 'v'가 동기적으로 전달되어 다음 단계에서 'newimg'로 나타납니다.

그 대신 현재 'batchedTimeout'이 처리될 때까지 'v'로 처리되지 않는 delayedPromise.then에서 promise를 반환하게 됩니다.이때 loadImage완료 사이에 'waitPeriod' 간격이 또 있을 경우 'img' 요소가 체인의 다음 단계로 전달되어 DOM에 추가됩니다.

그것으로 끝입니다!

결론

HTML ListView 성능 최적화 샘플에서 보여 준 다섯 가지 렌더링 함수 사이에 한 가지 공통점이 있습니다. promise로 표현되는 ListView와 렌더러 간의 비동기 관계로 인해 렌더러가 매우 유연하게 언제 어떻게 목록에 있는 항목에 사용할 요소를 생성할지 알 수 있다는 점입니다. 여러분이 앱을 작성할 때 ListView 최적화에 사용하는 전략은 데이터 원본의 크기, 항목 자체의 복잡성, 해당 항목에 대해 비동기적으로 얻는 데이터의 양(예: 원격 이미지 다운로드)에 크게 좌우됩니다. 당연한 말이겠지만 여러분은 원하는 성능에 도달할 때까지 항목 렌더러를 최대한 단순하게 유지하고 싶을 것입니다. 이제는 어떤 경우에도 ListView와 여러분의 앱이 최고의 성능을 발휘하도록 도와 주는 도구가 준비되어 있습니다.

Kraig Brockschmidt
Windows 에코시스템 팀 프로그램 관리자
작성자, HTML, CSS 및 JavaScript로 Windows 8 앱 프로그래밍하기


Otimizar a renderização de item do ListView

$
0
0

Para muitos aplicativos da Windows Store escritos em JavaScript , que trabalham com coleções, colaborar bem com o controle WinJS ListViewé essencial para o bom desempenho do aplicativo. Isto não é nenhuma surpresa: quando se está lidando com o gerenciamento e a exibição de, possivelmente, milhares de itens, qualquer otimização que você fizer com esses itens conta. Ainda mais importante é como cada um desses itens é renderizado, o que significa, como — e quando — cada item no controle ListView é construído no DOM e se torna uma parte visível do aplicativo. Na verdade, a parte quando dessa equação se torna um fator crucial quando um usuário realiza uma panorâmica com rapidez em uma lista e espera que aquela lista mantenha o ritmo.

A renderização de itens em um ListView acontece por meio de um modelo declarativo, definido em HTML ou por meio de uma função de renderização personalizada do JavaScript que é exigida para cada item na lista. Embora o modelo declarativo seja o método mais simples, ele não oferece muita margem para o controle específico sobre o processo. Por outro lado, uma função de renderização o permite personalizar a renderização por cada item e possibilita diversas otimizações demonstradas na amostra de desempenho de otimização do ListView em HTML. Essas otimizações são:

  • A possibilidade de entregas assíncronas de dados dos itens e um elemento renderizado, com o suporte das funções básicas de renderização.
  • Separação da criação do formato de um item dos seus elementos internos, o que é necessário para o layout geral de um ListView. Este processo é possível por meio de um renderizador de espaço reservado.
  • Reutilização de um elemento de item previamente criado (e dos seus filhos) substituindo os seus dados, evitando a maioria das etapas de criação de elemento, possibilitado por meio de um renderizador de reciclagem de espaço reservado.
  • Adiar operações visuais caras, como carregamento e animação de imagens, até que um item se torne visível e um ListView não esteja sendo pesquisado panoramicamente, o que é realizado por meio de um renderizador de várias etapas.
  • Agrupar essas mesmas operações visuais para minimizar a novas renderizações do DOM, por meio de um renderizador de agrupamento de várias etapas.

Nesta postagem analisaremos todas essas etapas e aprenderemos como elas cooperam com o processo de renderização de item do ListView. Como você pode imaginar, otimizações relacionadas a quando renderizar um item envolvem muitas operações assíncronas e, portanto, muitas promessas. Portanto, durante o processo, também aprenderemos muito sobre as próprias promessas, adicionando ao artigo anterior All about promises (Tudo sobre promessas) neste blog.

Uma observação geral, que se aplica a todos os renderizadores: é sempre importante manter o tempo de renderização do seu item principal (sem contar as operações adiadas) ao mínimo. Como o desempenho final do ListView depende muito de como as suas atualizações estão alinhadas com os intervalos de atualização da tela, uns poucos milissegundos gastos em um renderizador de item pode aumentar o tempo geral de renderização do ListView no curso do intervalo de atualização seguinte, resultando em quadros eliminados e falhas na imagem. Em outras palavras, os renderizadores de item são um lugar onde otimizar o seu código JavaScript realmente conta.

Renderizadores básicos

Vamos começar com rápida análise do que é uma função de renderização de item — que chamarei simplesmente de renderizador. Um renderizador é uma função designada por você à propriedade itemTemplate do ListView, em vez de um nome do modelo, e essa função será utilizada, conforme necessário, para itens que o ListView quer incluir no DOM. (Observe que as informações básicas para renderizadores podem ser encontradas na página itemTemplate, mas é a amostra que realmente apresenta as otimizações.)

Você pode esperar que uma função de renderização de item seria simplesmente designada a um item a partir da fonte de dados do ListView. Ela, então, criaria os elementos HTML necessários para aquele item específico e retornaria o elemento raiz que o ListView pode adicionar ao DOM. Basicamente, é isto que acontece, mas existem duas considerações adicionais. Primeiro, os próprios dados do item podem ser carregados de forma assíncrona, portanto, faz sentido associar a criação do elemento à disponibilidade daqueles dados. Além disso, o próprio processo de renderização do item pode envolver trabalhos assíncronos, como carregar imagens de URIs remotos ou a leitura de dados em outros arquivos identificados nos dados do item. Os diferentes níveis de otimização que analisaremos, na verdade, permitem um volume arbitrário de trabalho assíncrono entre a solicitação dos elementos do item e a entrega, de fato, desses elementos.

Portanto, repito, você pode confiar que promessas estarão envolvidas! Primeiro porque o ListView não fornece ao renderizador apenas os dados do item diretamente, ele fornece uma promessa para esses dados. Em vez da função retornar diretamente ao elemento raiz do item, ela retorna uma promessa para aquele elemento. Isto permite que o ListView agrupe diversas promessas de renderização de item e aguarde (de forma assíncrona) até que a página inteira de itens tenha sido renderizada. Na verdade, ele faz isso para gerenciar, de maneira inteligente, como ele constrói as diferentes páginas. Primeiro, construindo a página de itens visíveis. Depois, construindo duas páginas não exibidas na tela, para frente e para trás, onde é mais provável que os usuários façam uma panorâmica para avançar. Além disso, ter todas estas promessas nos devidos lugares significa que o ListView pode cancelar facilmente a renderização de itens inacabados, caso o usuário deixe de fazer a panorâmica, evitando assim, a criação desnecessária de elementos.

Podemos ver como essas promessas são usadas na função simpleRenderer da amostra:

 

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;
});
}

Este código primeiro anexa um manipulador concluído ao itemPromise. O manipulador é chamado quando os dados do item estão disponíveis e efetivamente cria os elementos em resposta. Observe, novamente, que não estamos retornando o elemento diretamente. Estamos retornando uma promessa que é cumprida com esse elemento. Ou seja, o valor de retorno do itemPromise.then()é uma promessa que é cumprida com o element se e quando o ListView precisar dele.

Retornar uma promessa nos permite realizar outras tarefas assíncronas, se necessário. Neste caso, o renderizador pode simplesmente encadear estas promessas intermediárias, retornando a promessa da última then na cadeia. Por exemplo:

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;
});
}

Observe que, o dele é um caso onde não usamos done no final da cadeia, porque estamos retornando uma promessa da última chamada then. O ListView se responsabiliza por lidar com qualquer erro que possa surgir.

Renderizadores de espaço reservado

A próxima etapa da otimização do ListView utiliza um renderizador de espaço reservado que divide a construção do elemento em duas etapas. Isto permite que o ListView solicite apenas as partes de um elemento que são necessárias para definir o layout geral da lista, sem ter que construir todos os elementos dentro de cada item. Consequentemente, o ListView pode completar o seu cálculo de layout rapidamente e continuar a responder eficazmente a mais entradas. Ele pode, então, solicitar o restante do elemento mais tarde.

Um renderizador de espaço reservado retorna um objeto com duas propriedades, em vez de apenas uma promessa:

  • element O elemento de nível superior na estrutura do item, que basta para definir o seu tamanho e forma, e que não depende dos dados do item.
  • renderComplete Uma promessa que é cumprida quando o restante dos conteúdos do elemento são construídos, ou seja, retorna a promessa de qualquer cadeia que você tenha iniciado com o itemPromise.then, como anteriormente.

O ListView é inteligente o bastante para verificar se o seu renderizador retornou uma promessa (o caso básico anterior) ou um objeto com propriedades element e renderComplete (casos mais avançados). Portanto, o renderizador de espaço reservado equivalente (na amostra) para o simpleRenderer anterior é o seguinte:

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' />");
})
};
}

Observe que a atribuição element.innerHTML pode ser movida dentro do renderComplete porque a classe itemTempl no arquivo de amostra css/scenario1.css especificou diretamente a largura e a altura do item. A razão para ela estar incluída na propriedade elementé porque ela fornece o padrão de texto “…” no espaço reservado. Você também poderia facilmente utilizar um elemento img que se refira a um pequeno recurso no pacote que é compartilhado por todos os itens e, portanto, seja renderizado rapidamente.

Reciclar renderizadores de espaço reservado

A próxima otimização, reciclar renderizadores de espaço reservado não adiciona nada de novo no que se refere às promessas. Ela adiciona o reconhecimento de um segundo parâmetro para o renderizador chamado recycled, que é o elemento raiz de um item que foi anteriormente renderizado, mas não está mais visível. Ou seja, o elemento reciclado já possui os seus elementos filho nos devidos lugares, para que você possa substituir os dados ou talvez alterar alguns desses elementos. Isto evita a maioria das solicitações caras de criação de elementos, necessárias para um item totalmente novo, economizando um tempo significativo no processo de renderização.

O ListView pode fornecer um elemento reciclado quando o seu loadingBehavior estiver configurado para "randomaccess". Se recycled for fornecido, você pode simplesmente limpar dados do elemento (e dos seus filhos), retorná-lo ao seu espaço reservado e, então, preencher os dados e criar filhos adicionais (se necessário) dentro do renderComplete. Se um elemento reciclado não for fornecido (como acontece quando o ListView é criado pela primeira vez ou quando o loadingBehavior for "incremental"), você criará o elemento novamente. Aqui está o código da amostra para esta variação:

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";
})
};
}

Na opção renderComplete, certifique-se de verificar a existência de elementos não criados para um novo espaço reservado, como por exemplo label, e crie-os aqui, se necessário.

Se você deseja livrar-se de itens reciclados de forma mais generalizada, é possível fornecer uma função à propriedade resetItem do ListView. Esta função pode conter código similar ao mostrado acima. O mesmo vale para a propriedade resetGroupHeader , já que você pode usar funções modelo para cabeçalhos de grupo e também para itens. Não nos referimos muito a isto porque os cabeçalhos de grupos são em número muito menor e, normalmente, não têm as mesmas implicações no desempenho. Não obstante, a funcionalidade existe.

Renderizadores de várias etapas

Isto nos trás à penúltima otimização, o renderizador de várias etapas. Ele estende o renderizador de reciclagem de espaço reservado para atrasar o carregamento de imagens e outras mídias, até que o resto do item esteja completamente presente no DOM. Ele também atrasa efeitos, como animações, até que o item esteja realmente na tela. Isto reconhece que os usuários frequentemente fazem panorâmica bem rápidas no ListView. Portanto, faz sentido adiar as operações mais caras até o ListView ter chegado a uma posição estável.

O ListView fornece os ganchos necessários conforme os membros no item resultem do itemPromise: uma propriedade chamada ready (uma promessa) e dois métodos, o loadImage e o isOnScreen, retornam (ainda mais!) promessas. Ou seja:

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

Aqui está como usá-los:

  • ready Retorna esta promessa a partir do primeiro manipulador concluído na sua cadeia. Essa promessa é cumprida quando a estrutura completa do elemento foi renderizada e está visível. Isto significa que você pode encadear um outro then com um manipulador concluído no qual você executa tarefas após a visibilidade, como, por exemplo carregar imagens.
  • loadImage Baixa uma imagem de um URI e a exibe no elemento img designado, retornando uma promessa que é cumprida com esse mesmo elemento. Você anexa um manipulador concluído à esta promessa e ele mesmo retorna a promessa da isOnScreen. Observe que o loadImage criará um elemento img caso ele não seja fornecido e entregue a seu manipulador concluído.
  • isOnScreen Retorna uma promessa cujo valor de cumprimento é uma parâmetro booliano indicando se o item está visível ou não. Nas implementações correntes, este é um valor conhecido, portanto, a promessa é cumprida de forma síncrona. Entretanto, encapsulando-o em uma promessa, ele pode ser usado em uma cadeia mais longa.

Pode-se observar tudo isto na função multistageRenderer da amostra, onde a conclusão do carregamento da imagem é usada para dar início a uma animação de fade-in. Aqui estou apenas mostrando o que foi retornado da promessa renderComplete:

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);
}
})

Embora haja muita coisa acontecendo, ainda temos apenas uma cadeia de promessas básica. A primeira operação assíncrona no renderizador atualiza partes simples da estrutura do elemento do item, tais como texto. Ela, então, retorna a promessa em item.ready. Quando a promessa é cumprida — ou, mais precisamente, se a promessa é cumprida — utilizamos o método assíncrono loadImage do item para dar início ao download de uma imagem, retornando a promessa item.isOnScreen daquele manipulador concluído. Isto significa que o sinalizador de visibilidade onscreené passado para o manipulador concluído final na cadeia. Quando e se aquela promessa isOnScreené cumprida — o que significa que o item está realmente visível — podemos executar operações relevantes como, por exemplo, animações.

Enfatizo a parte "se", porque pode ser que, novamente, aquele usuário esteja fazendo uma panorâmica dentro do ListView enquanto isto está acontecendo. Ter todas estas promessas encadeadas torna possível para o ListView cancelar operações assíncronas, sempre que estes itens não estejam visíveis e/ou desativados de uma página com buffer. Basta dizer que, o controle do ListView passou por muitos testes de desempenho!

Também é importante lembrar, novamente, que estamos usando o then em todas estas cadeias, porque ainda estamos retornando uma promessa da função de renderização dentro da propriedade renderComplete. Nunca somos o final da cadeia destes renderizadores, portanto, nunca usaremos o done no final.

Agrupamento de miniaturas

A última otimização é, verdadeiramente, o coup de grace para o controle do ListView. Na função chamada batchRenderer, encontramos esta estrutura para o renderComplete (com a maior parte do código omitido):

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);
}
})

Isto é quase o mesmo que o multistageRenderer com exceção da misteriosa solicitação para a inserção de uma função chamada thumbnailBatch entre a solicitação item.loadImage e a verificação do item.isOnScreen. O posicionamento do thumbnailBatch na cadeia indica que o seu valor de retorno deve ser um manipulador concluído, que retorna uma outra promessa.

Confuso? Não se preocupe, chegaremos ao fundo disto! Mas antes precisamos de um pouco mais de informações sobre o que estamos tentando fazer.

Se tivéssemos apenas um ListView, com um único item, várias otimizações de carregamento não seriam percebidas. Mas os ListViews normalmente têm vários itens e a função de renderização é necessária para cada um deles. Na opção multistageRenderer da seção anterior, e renderização de cada item dá início a uma operação assíncrona item.loadImage para baixar a sua miniatura de um URI arbitrário, e cada operação pode levar um período de tempo arbitrário. Portanto, para a lista completa, poderemos ter uma série de solicitações simultâneas de loadImage ocorrendo, com a renderização de cada item aguardando a conclusão da sua miniatura específica. Até aqui, tudo bem.

Entretanto, uma característica importante que não está visível no multistageRendereré que o elemento img para a miniatura já está no DOM, e a função loadImage configura o atributo src daquela imagem assim que o download tiver concluído. Por sua vez, isto aciona uma atualização no mecanismo de renderização assim que retornarmos do restante da cadeia de promessas que, depois disso, é essencialmente síncrona.

É possível, então, que uma série de miniaturas possam voltar ao thread da interface do usuário dentro um curto período de tempo. Isso causará excesso de rotatividade no mecanismo de renderização e um fraco desempenho visual. Para evitar essa rotatividade o que queremos é criar por completo estes elements img antes deles estarem no DOM, e depois adicioná-los em lotes, de tal maneira que sejam todos manipulados em uma única passagem de renderização.

A amostra realiza isto por meio de um código de promessa mágico, uma função chamada createBatch. createBatché solicitado apenas uma vez para todo o aplicativo e o seu resultado — uma outra função — é armazenado na variável chamada thumbnailBatch:

var thumbnailBatch;
thumbnailBatch = createBatch();

Uma solicitação à esta função thumbnailBatch , como a chamarei daqui para frente, é novamente inserida na cadeia de promessa de renderizador. O propósito dessa inserção, dada a natureza do código de loteamento, como logo veremos, é agrupar um conjunto de elementos img carregados, liberando-os para mais processamento em intervalos adequados. Repetindo, apenas ao olhar para a cadeia de promessa no renderizador, uma solicitação ao thumbnailBatch() deve retornar uma função concluída de manipulador que retorna uma promessa, e o valor de cumprimento dessa promessa (analisando a próxima etapa na cadeia) deve ser um elemento img que pode ser adicionado ao DOM. Ao adicionar estas imagens ao DOM, após o loteamento ter sido concluído, combinamos todo este grupo para a mesma passagem de renderização.

Esta é uma importante diferença entre o batchRenderer e o multistageRenderer da seção anterior: no último o elemento da miniatura img já existe no DOM e é passado ao loadImage como um segundo parâmetro. Portanto, quando loadImage configura o atributo src da imagem, uma atualização de renderização é acionada. Entretanto, dentro do batchRenderer, este elemento imgé criado separadamente dentro do loadImage (onde o src também é configurado), mas o img ainda não está no DOM. Ele só é adicionado ao DOM após a conclusão da etapa do thumbnailBatch , tornando-o parte de um grupo dentro daquele cálculo de layout único.

Agora, vamos ver como funciona este loteamento. Aqui está a função createBatch completa:

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]();
}
}

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

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

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

Novamente, o createBatché solicitado apenas uma vez e o resultado do seu thumbnailBatché solicitado para cada item renderizadona lista. O manipulador concluído que o thumbnailBatch cria é, então, solicitado sempre que uma operação loadImageé concluída.

Este manipulador concluído poderia também ter sido diretamente inserido na função de renderização, mas o que estamos tentando fazer aqui é coordenar atividades em vários itens em vez de um item de cada vez. Esta coordenação é obtida por meio de duas variáveis criadas e inicializadas no início do createBatch: batchedTimeout, inicializado como uma promessa vazia e batchedItems, inicializado como uma matriz de funções que, inicialmente, está vazia. createBatch também declara uma função completeBatch, que simplesmente descarta os batchedItems, solicitando cada função na matriz:

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]();
}
}

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

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

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

Agora, vejamos o que acontece dentro do thumbnailBatch (a função retornada do createBatch), que é novamente solicitada para cada item sendo renderizado. Primeiro, cancelamos qualquer batchedTimeout existente e imediatamente o recriamos:

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

A segunda linha mostra o padrão futuro de entrega/cumprimento que discutimos na postagem All About Promises <TODO: link>: ele diz para solicitar o completeBatch após um atraso de waitPeriod milissegundos (com um padrão de 64ms). Isto significa que enquanto o thumbnailBatch estiver sendo solicitado novamente dentro do waitPeriod de uma solicitação anterior, o batchTimeout será redefinido para um outro waitPeriod. E, como o thumbnailBatch só é solicitado após que uma solicitação item.loadImage seja concluída, estamos, na verdade dizendo que quaisquer operações loadImage que forem concluídas dentro do waitPeriod da anterior, será incluída no mesmo lote. Quando existe uma lacuna maior do que o waitPeriod, o lote é processado — imagens são adicionadas ao DOM — e o próximo lote começa.

Após manipular este tempo de espera, o thumbnailBatch cria uma nova promessa que simplesmente envia a função despachante concluída para dentro da matriz batchedItems:

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

Lembre-se do All About Promises <TODO: link> que uma promessa é apenas uma construção de código e isso é tudo o que temos aqui. A nova promessa criada não possui um comportamento assíncrono por si própria: estamos apenas adicionando a função despachante concluída c, para o batchedItems. Obviamente, não fazemos nada com o despachante até que o batchedTimeout seja concluído de forma assíncrona, portanto, existe de fato, um relacionamento assíncrono aqui. Quando acontece o tempo de espera e limpamos o lote (dentro do completeBatch), invocaremos quaisquer manipuladores concluídos que tenham sido dados ao delayedPromise.then.

Isto nos traz às últimas linhas de código no createBatch, que é a função retornada pelo thumbnailBatch. Essa função é exatamente o manipulador concluído que é inserido dentro de toda a cadeia de promessa do renderizador:

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

Na verdade, vamos inserir este código diretamente na cadeia de promessa para vermos os relacionamentos resultantes:

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

Agora, podemos ver que o argumento vé o resultado do item.loadImage, que é o elemento img criado para nós. Se não quiséssemos fazer o loteamento, bastaria dizer return WinJS.Promise.as(v) e a cadeia inteira ainda funcionaria: v seria, então, passado de forma assíncrona e seria exibido como newimg na etapa seguinte.

Entretanto, em vez disso, estamos retornando a promessa do delayedPromise.then que não será cumprida — com v— até que a atual batchedTimeout seja cumprida. Neste momento — quando houver novamente uma lacuna de waitPeriod entre as conclusões do loadImage—, estes elementos img serão entregues à etapa seguinte na cadeia, onde serão adicionados ao DOM.

E é isso!

Conclusão

As cinco diferentes funções de renderização demonstradas na amostra de desempenho de otimização do ListView em HTML têm uma coisa em comum: elas mostram como o relacionamento assíncrono entre o ListView e o renderizador — expressado por meio de promessas — possibilita uma enorme flexibilidade ao renderizador sobre como e quando ele produz elementos para os itens na lista. Ao escrever os seus próprios aplicativos, a estratégia a ser usada para a otimização do ListView depende muito do tamanho da sua fonte de dados, da complexidade dos próprios itens e do volume de dados que você está obtendo de forma assíncrona para esses itens (como baixar imagens remotas). Com certeza, você desejará manter os renderizadores de itens o mais simples que puder, para ainda atender os seus objetivos de desempenho. Em todo caso, você agora tem todas as ferramentas de que precisa para ajudar o ListView e o seu aplicativo a alcançarem o melhor desempenho.

Kraig Brockschmidt
Gerente de programas, equipe de ecossistema do Windows
Autor, Programming Windows 8 Apps in HTML, CSS, and JavaScript (Programando aplicativos do Windows 8 em HTML, CSS e JavaScript)

Оптимизация отрисовки элементов ListView

$
0
0

Для многих приложений Магазина Windows, написанных на JavaScriptи работающих с коллекциями, хорошее взаимодействие с элементом управления WinJS ListViewпросто необходимо для оптимизации производительности. И это неудивительно: когда вы занимаетесь отображением тысяч элементов и их управлением, ценится любое усилие по их оптимизации. Самое важное — как каждый из этих элементов отрисовывается, т.е. как и когда каждый элемент из ListView формируется в DOM и становится видимой частью приложения. Время (компонент "когда") в этом уравнении становится критически важным фактором, если пользователь быстро перемещается по списку и ожидает, что список будет "успевать" за его перемещениями.

Отрисовка элементов в ListView осуществляется с помощью декларативного шаблона, определенного в HTML, или настраиваемой функции отрисовки JavaScript, вызываемой для каждого элемента в списке. Хотя использование декларативного шаблона — самый простой путь, он не обеспечивает достаточно широких возможностей управления этим процессом. Функция отрисовки наоборот позволяет поэлементно настроить отрисовку и оптимизировать процесс, как показано в примере оптимизации производительности HTML ListView. Используются следующие возможности оптимизации:

  • Асинхронная доставка данных элемента и отображаемого элемента с поддержкой базовых функций отрисовки.
  • Раздельное создание формы элементы, необходимой для общей разметки ListView, и его внутренних компонентов. Это поддерживается с помощью средства отрисовки заполнителя.
  • Повторное использование ранее созданного элемента (и дочерних элементов) с заменой данных, что позволяет пропустить большинство действий по созданию элемента. Предоставляется с помощью средства отрисовки заполнителя с повторным использованием.
  • Задержка выполнения ресурсоемких визуальных операций, таких как загрузка изображений и анимация, до отображения элемента (если для ListView не используется быстрое панорамирование) с помощью средства многоэтапной отрисовки.
  • Пакетное выполнение одинаковых визуальных операций, чтобы свести к минимуму повторную отрисовку DOM, с помощью средства многоэтапной пакетной отрисовки.

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

Для всех средств отрисовки всегда важно свести к минимуму базовое время отрисовки элемента (не учитывая отложенные операции). Поскольку конечная производительность ListView сильно зависит от того, насколько его обновления согласованы с интервалами обновления экрана, несколько лишних миллисекунд в отрисовке элемента могут значительно увеличить общее время визуализации ListView в течение следующего интервала обновления. Из-за этого могут пропадать кадры, а изображение будет казаться нестабильным. Другими словами, если вы хотите оптимизировать код JavaScript, делайте это в средствах отрисовки элементов.

Базовые средства отрисовки

Начнем с краткого обзора функции отрисовки элемента, которую я буду называть средством отрисовки. Средство отрисовки — это функция, которую вы назначаете свойству itemTemplateэлемента управления ListView вместо имени шаблона. Она при необходимости вызывается для элементов, которые ListView хочет включить в DOM. (Кстати, базовую документацию по средствам отрисовки можно найти на странице itemTemplate. Но описанные возможности оптимизации демонстрируются именно в примере.)

Можно ожидать, что функция отрисовки просто получает элемент из источника данных ListView. Затем она создает необходимые HTML-элементы и возвращает корневой элемент, который ListView может добавить в DOM. В целом, именно эти и происходит, но стоит отметить два момента. Во-первых, сами данные элемента могут загружаться асинхронно, поэтому имеет смысл привязать создание элемента к доступности данных. Кроме того, в процесс отрисовки элемента могут входить другие асинхронные операции, например загрузка изображений из удаленных URI или чтение данных из других файлов, определенных в данных элемента. Как мы увидим, разные уровни оптимизации позволяют выполнять произвольный объем асинхронных операций между запросом компонентов элемента и их фактической доставкой.

Опять же, можно ожидать использования обещаний! ListView не просто напрямую передает данные элемента средству отрисовки, а предоставляет обещание об этих данных. А функция не возвращает корневой компонент элемента напрямую, а возвращает обещание об этом компоненте. Это позволяет ListView объединить множество обещаний отрисовки элемента и ждать (асинхронно) визуализации целой страницы. Так ListView будет интеллектуально управлять формированием разных страниц, сначала создавая страницу видимых элементов, а затем — предыдущую и следующую страницы, на которые вероятнее всего перейдут пользователи. Кроме того, наличие всех этих обещаний означает, что ListView может легко отменить отрисовку незаконченных элементов, если пользователь перейдет к другим элементам. Это позволит избежать создания ненужных элементов.

Использование таких обещаний можно увидеть в функции simpleRendererв примере:

 

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;
});
}

Этот код сначала присоединяет обработчик выполнения к itemPromise. Этот обработчик вызывается, когда данные элемента доступны, и создает в ответ компоненты. Но снова обратите внимание, что мы не возвращаем компонент напрямую, а возвращаем обещание, которое будет выполнено с этим компонентом. Следовательно возвращаемое itemPromise.then()значение — это обещание, выполняемое с компонентом, когда и если он потребуется ListView.

Возвращая обещания, мы можем при необходимости выполнять другие асинхронные операции. В этом случае средство отрисовки может объединять промежуточные обещания, возвращая обещания от последней функции thenв цепочке. Например:

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;
});
}

Обратите внимание, что в этом случае мы неиспользуем функцию doneв конце цепочки, поскольку мы возвращаем обещание от последнего вызова функции then. ListView отвечает за обработку всех ошибок, которые могут возникнуть.

Средства отрисовки заполнителя

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

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

  • element— компонент верхнего уровня в структуре отображаемого элемента. Его достаточно для определения размера и формы, и он не зависит от данных элемента.
  • renderComplete— обещание, которое выполняется после формирования оставшихся частей компонента, т. е. когда возвращается обещание из цепочки, начинающейся с itemPromise.then, как и ранее.

ListView реализован достаточно интеллектуально, поэтому он проверяет, возвращает ли средство отрисовки обещание (базовая ситуация, как описано ранее) или объект со свойствами elementи renderComplete (более сложные ситуации). Таким образом, эквивалентное средство отрисовки заполнителя (в примере) для предыдущей функции simpleRendererвыглядит следующим образом:

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' />");
})
};
}

Обратите внимание на то, что назначение element.innerHTMLможно было бы переместить в renderComplete, поскольку класс itemTemplв файле css/scenario1.css примера напрямую определяет ширину и высоту элемента. Это назначение указано в свойстве element, потому что оно задает в заполнителе текст по умолчанию "…". Вы можете так же просто использовать компонент img, который ссылается на небольшой ресурс в пакете, общий для всех элементов (и поэтому быстро визуализируемый).

Средства отрисовки заполнителя с повторным использованием

Следующая возможность оптимизации — применение средства отрисовки заполнителя с повторным использованием— не добавляет ничего нового в отношении обещаний. Она добавляет знание о втором параметре средства отрисовки, recycled, который является корневым компонентом ранее отрисованного элемента, который больше не виден. Это значит, что дочерние компоненты повторно используемого компонента уже сформированы, поэтому можно просто заменить данные и, возможно, подправить несколько компонентов. Так можно избежать ресурсоемких вызовов создания компонентов, необходимых для отображения совершенно нового элемента. Это позволяет сэкономить много времени в процессе отрисовки.

ListView может предоставить повторно используемый компонент, если для его свойства loadingBehaviorзадано значение "randomaccess". Если указан параметр recycled, можно просто удалить данные этого компонента (и его дочерних компонентов), вернуть его как заполнитель, а затем вставить в него данные и создать дополнительные дочерние компоненты (при необходимости) в renderComplete. Если повторно используемый компонент не указан (как при первом создании ListView или если для параметра loadingBehaviorзадано значение "incremental"), компонент создается заново. Вот код из примера для этого варианта:

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";
})
};
}

В renderCompleteнеобходимо проверить наличие компонентов, которые не создаются для нового заполнителя, например label, и создать их при необходимости.

Если вы хотите очистить повторно используемые элементы с помощью более общего процесса, можно создать функцию для свойства resetItemэлемента управления ListView. Эта функция будет содержать код, аналогичный приведенному выше. То же относится и к свойству resetGroupHeader, поскольку вы можете использовать функции шаблона для заголовков групп и элементов. Мы не обсуждали их подробно, поскольку заголовков групп не так много и обычно они не так сильно влияют на производительность. Но тем не менее такая возможность существует.

Средства многоэтапной отрисовки

Теперь перейдем к предпоследней возможности оптимизации — средству многоэтапной отрисовки. Оно расширяет возможности средства отрисовки заполнителя с повторным использованием, задерживая загрузку изображений и других объектов мультимедиа, пока весь элемент не появится в DOM. Оно также задерживает эффекты, например анимацию, пока элемент не появится на экране. Это связано с тем, что зачастую пользователи довольно быстро перемещаются по элем��нту управления ListView, поэтому имеет смысл асинхронно отложить более ресурсоемкие операции, пока ListView не окажется в стабильном положении.

ListView предоставляет необходимые обработчики как члены item, полученного от itemPromise: свойство ready (обещание) и два метода, loadImageи isOnScreen, также возвращающие обещания. Т. е.:

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

Вот как они используются:

  • ready— возвращайте это обещание из первого обработчика выполнения в цепочке. Это обещание выполняется, когда полная структура элемента отрисована и видима. Это значит, что другой метод thenможно объединить с обработчиком выполнения, в котором выполняются другие операции, такие как загрузка изображений.
  • loadImage— загружает изображение по URI и отображает его в указанном компоненте img, возвращая обещание, которое выполняется с тем же элементом. Обработчик выполнения присоединяется к этому обещанию, которое само возвращает обещание от isOnScreen. Обратите внимание, что loadImageсоздает компонент img, если он не указан, и предоставляет его обработчику выполнения.
  • isOnScreen— возвращает обещание с логическим значением выполнения, указывающим, видим элемент или нет. В текущих реализациях это известное значение, поэтому обещание выполняется синхронно. Однако, заключив в обещание, его можно использовать в более длинной цепочке.

Все это видно в функции multistageRendererв примере, где завершение загрузки изображения используется для начала анимации исчезания. Здесь я просто покажу, что возвращает обещание renderComplete:

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);
}
})

Хотя и происходит много всего, здесь используется всего лишь базовая цепочка обещаний. Первая асинхронная операция в средстве отрисовки обновляет простые части структуры компонентов элемента, такие как текст. Затем она возвращает обещание в item.ready. Когда это обещание выполняется или, говоря точнее, если оно выполняется, мы используем асинхронный метод loadImageэлемента, чтобы инициировать загрузку изображения. Обработчик выполнения возвращает обещание item.isOnScreen. Это значит, что флаг видимости onscreenпередается в конечный обработчик выполнения в цепочке. Если и когда обещание isOnScreenвыполняется (элемент полностью видим), мы можем заняться соответствующими операциями, например анимацией.

Я подчеркиваю часть с "если", так как есть вероятность, что пользователь быстро перемещается по элементу управления ListView, когда все это происходит. Объединив все обещания в цепочку, мы позволяем ListView отменять асинхронные операции, если эти элементы выходят из поля зрения или за пределы страниц, хранящихся в буфере. Достаточно сказать, что производительность элемента управления ListView была тщательнопротестирована!

Также важно напомнить еще раз, что во всех этих цепочках мы используем then, потому что функция отрисовки в свойстве renderCompleteвозвращает обещание. Эти функции отрисовки никогда не будут концом цепочки, поэтому doneникогда не будет использоваться.

Пакетная обработка эскизов

Последняя возможность оптимизации — это воистину Священный Граальдля элемента управления ListView. В функции batchRendererмы находим следующую структуру для renderComplete (большая часть кода опущена):

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);
}
})

Она практически совпадает со структурой для multistageRenderer, за исключением загадочного вызова функции thumbnailBatchмежду вызовом item.loadImageи проверкой свойства item.isOnScreen. Наличие thumbnailBatchв цепочке указывает на то, что возвращаемым значением должен быть обработчик выполнения, возвращающий еще одно обещание.

Запутались? Ничего страшного, мы во всем разберемся. Но сначала мы должны побольше узнать о том, чего пытаемся добиться.

Если бы в ListView был всего один элемент, различные способы оптимизации загрузки были бы незаметны. Но в ListView обычно содержится множество элементов, и функция отрисовки вызывается для каждого из них. В функции multistageRendererиз предыдущего раздела отрисовка каждого элемента инициирует асинхронную операцию item.loadImageдля загрузки эскиза из произвольного URI. Каждая такая операция может занять определенное время. Поэтому для всего списка может одновременно выполняться много вызовов loadImage, причем отрисовка каждого элемента будет ожидать завершения обработки соответствующего эскиза. Пока все понятно.

Важная характеристика, которая совсем не видна в multistageRenderer, состоит в том, что компонент imgдля эскиза уженаходится в DOM, а функция loadImageустанавливает атрибут srcэтого изображения после завершения загрузки. Это в свою очередь инициирует обновление модуля отрисовки после возврата из оставшейся цепочки обещаний, которая после этой точки выполняется синхронно.

Может случиться так, что множество эскизов вернутся в поток пользовательского интерфейса через короткое время. Это приведет к усложнению работы модуля отрисовки и низкой производительности визуализации. Чтобы избежать этого, компоненты imgнеобходимо полностью создать доих помещения в DOM, а затем добавить в пакеты для обработки в одном цикле отрисовки.

В примере это реализуется с помощью обещаний — функции createBatch. createBatchвызывается один раз для всего приложения, а ее результат (другая функция) сохраняется в переменной thumbnailBatch:

var thumbnailBatch;
thumbnailBatch = createBatch();

Вызов этой функции thumbnailBatch, как я ее буду теперь называть, снова вставляется в цепочку обещаний функции отрисовки. Цель этой вставки с учетом природы кода пакетной обработки (как мы скоро увидим) — сгруппировать набор загруженных компонентов img, освободив их для дальнейшей обработки через подходящие интервалы. Если посмотреть на цепочку обещаний в функции отрисовки, вызов thumbnailBatch()должен вернуть функцию обработчика выполнения, возвращающую обещание. Значением выполнения этого обещания (если посмотреть на следующее звено в цепочке) должен быть компонент img, который можно добавить в DOM. Добавляя изображения в DOM послепакетной обработки, мы обрабатываем эту группу в одном цикле отрисовки.

В этом состоит важное отличие batchRendererот функции multistageRendererиз предыдущего раздела: в последней из этих двух функций компонент imgуже существует в DOM и передается в loadImageкак второй параметр. Поэтому когда loadImageустанавливает атрибут srcизображения, инициируется обновление отрисовки. А в функции batchRendererэлемент imgсоздается отдельно в loadImage (где атрибут srcтакже установлен), но элемента imgеще нет в DOM. Он будет добавлен в DOM только после выполнения thumbnailBatch, что делает его частью группы на этом этапе разметки.

Теперь рассмотрим, как действует пакетная обработка. Далее представлена полная функция createBatch:

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]();
}
}

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

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

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

Опять функция createBatchвызывается всего один раз, а ее результат, функция thumbnailBatch, вызывается для каждого отрисованного элементав списке. Обработчик выполнения, формируемый thumbnailBatch, вызывается после завершения каждой операции loadImage.

Такой обработчик выполнения можно было бы легко вставить в функцию отрисовки, но мы хотим скоординировать операции для разных элементов, а не для каждого из них. Для этого в начале функции createBatchсоздаются и инициализируются две переменные: batchedTimeout, инициализируемая как пустое обещание, и batchedItems, инициализируемая как изначально пустой массив функций. createBatchтакже объявляет функцию completeBatch, которая просто очищает batchedItems, вызывая каждую функцию в массиве:

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]();
}
}

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

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

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

Теперь посмотрим, что происходит в функции thumbnailBatch (возвращаемой createBatch), которая опять вызывается для каждого отображаемого элемента. Сначала мы отменяемлюбую существующую функцию batchedTimeoutи сразу же повторно создаем ее:

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

Во второй строке показан шаблон будущей доставки или выполнения, описанный в записи "Все об обещаниях" <TODO: link>: функция completeBatchвызывается после задержки в waitPeriodмс (значение по умолчанию — 64 мс). Это значит, что если thumbnailBatchопять вызывается в течение waitPeriodс момента предыдущего вызова, batchTimeoutсбрасывается и устанавливается другое значение waitPeriod. Поскольку функция thumbnailBatchвызывается только после завершения вызова item.loadImage, можно сказать, что все операции loadImage, завершающиеся в течение waitPeriodпредыдущего вызова, будут включены в один пакет. Если задержка превышает waitPeriod, выполняется обработка пакета (изображения добавляются в DOM) и начинается следующий пакет.

После этого thumbnailBatchсоздает новое обещание, которое просто записывает функцию диспетчера выполнения в массив batchedItems:

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

Помните, в записи "Все об обещания" <TODO: link>было сказано, что обещание — это просто конструкция кода. Это можно сказать и сейчас. Новое обещание не содержит асинхронных операций: мы просто добавляем функцию диспетчера выполнения, c, в batchedItems. Но, разумеется, мы ничего не делаем с диспетчером, пока batchedTimeoutне будет выполнена асинхронно. Так что здесь есть асинхронная связь. Когда истекает период ожидания и мы очищаем пакет (в функции completeBatch), мы вызываем все указанные обработчики выполнения в методе delayedPromise.then.

Перейдем к последним строкам кода в createBatch— функции, возвращаемой thumbnailBatch. Эта функция — обработчик выполнения, вставляемый в цепочку обещаний функции отрисовки:

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

Давайте добавим этот фрагмент кода напрямую в цепочку обещаний, чтобы увидеть полученные связи:

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

Теперь мы видим, что аргумент v— это результат item.loadImage, т. е. компонент img, созданный для нас. Если бы мы не хотели выполнять пакетную обработку, мы могли бы просто добавить выражение return WinJS.Promise.as(v), и вся цепочка все равно бы работала: аргумент vпередавался бы асинхронно и отображался как newimgна следующем этапе.

Вместо этого мы возвращаем обещание из delayedPromise.then, которое не будет выполнено (с аргументом v), пока не выполнено обещание batchedTimeout. В это время (если между завершением операций loadImageесть задержка не меньше waitPeriod) данные компоненты imgпередаются на следующий этап цепочки, где они добавляются в DOM.

Вот и все!

Заключение

У пяти разных функций отрисовки, продемонстрированных в примере оптимизации производительности HTML ListView, есть одна общая черта: они показывают, как асинхронная связь между ListView и функцией отрисовки, выраженная обещаниями, делает функцию отрисовки невероятно гибкой с точки зрения способа и времени создания элементов в списке. При написании собственных приложений стратегия оптимизации ListView сильно зависит от размера источника данных, сложности самих элементов, объема получаемых асинхронно данных для них (например, при загрузке удаленных изображений). Очевидно, для достижения необходимой производительности функции отрисовки элементов должны быть максимально простыми. Но в любом случае теперь у вас есть все инструменты, необходимые для обеспечения оптимальной производительности ListView (и вашего приложения).

Крэйг Брокшмидт (Kraig Brockschmidt)
Руководитель программы, рабочая группа по экосистеме Windows
Автор книги Programming Windows 8 Apps in HTML, CSS, and JavaScript (Программирование приложений для Windows 8 на HTML, CSS и JavaScript)

How to Assign Users, Roles and Permissions to a LightSwitch HTML Mobile Client

$
0
0

I’ve gotten a few questions lately on how to assign user permissions to a LightSwitch HTML mobile app so I thought I’d post a quick How To. The short answer is you need to deploy a desktop client to perform the security administration for your application. Typically an administration console also manages other types of global data that your app may use, like lookup tables and other reference data, and is used by one or a few system administrators. However, if you just need access to the Users and Roles screens so you can grant users access to the system, then the steps are simple.

Let’s take an example. I have a simple HTML client application and I’ve enabled Forms Authentication on the Access Control tab of the project properties.

image

I’ve already added permission checks in code to perform business rules and control access to application functionality. If you’re not familiar with how to do this, please read: LightSwitch Authentication and Authorization. The basic gist is that you use the access control hooks (_CanInsert, _CanDelete, _CanRead, etc.) on your data service (via the data designer) to perform permission checks in the middle-tier. If you also need to access user permissions on the HTML client in order to enable/disable UI elements then see my post: Using LightSwitch ServerApplicationContext and WebAPI to Get User Permissions.

In order to add a desktop client (our administration console), right-click on the project and select “Add Client”.

image

Then give it a name and click OK.

image

Now your solution will contain a desktop client. (Note: Once you add it, the desktop client will be set as the startup client for debug. Right-click on the HTMLClient and select “Set as StartUp Client” to switch it back.)

image

You actually do not need to add any screens to the desktop client. The Users and Roles admin screens will appear to anyone logged in with the SecurityAdministration permission. In order to get the first administrator into the database, you need to deploy your application, but first there’s a couple options to consider around the desktop client.

Right-click on the DesktopClient and select Properties. This will open the client-specific properties where you can specify a logo, icon, theme, etc. You can also change the screen navigation here. On the Client Type tab you can decide whether you want to deploy the desktop client as in-browser or out-of-browser. The LightSwitch desktop client is a Silverlight 5 client so it will run on a variety of desktop browsers (see system requirements here).

image

By default, when you add a Desktop client to a LightSwitch application the client type will be set to Web. This is a good choice if you are simply managing administrative data. If you need to automate other programs or devices on the Windows desktop via COM (i.e. Excel, Word, eye scanners, etc.) then you will want to choose “Desktop” option. This option will only run on Windows machines but it runs with higher trust so you can talk to other programs.

For this simple administrative console, leave it as Web. Now right-click on the LightSwitch application in the Solution Explorer and select Publish.  The key piece of information that the publish wizard needs is the Application Administrator information on the Security Settings tab. This is the user that will be added to the database the first time the application runs.

image

For more information on deploying see: How to: Deploy a 3-tier Application

Once we’ve deployed the application navigate to the DesktopClient and provide the same credentials you specified in the Publish Wizard. The application now has two clients so remember to navigate the correct virtual directory to run the associated client. For example, the name of our desktop client is “DesktopClient” so to run this one navigate to: http://www.mydomain.com/DesktopClientand to run the mobile client named “HTMLClient’ navigate to: http://www.mydomain.com/HTMLClient

When you open the desktop client and log in, you will see the Users and Roles screens under the Administration menu.

image

Once the administrator sets up the Roles and Users, those users can navigate to the HTMLClient on their mobile devices and log in.

image

Enjoy!

Why You Will Love The XBoxOne Cloud

$
0
0
There’s been a lot said about XBoxOne moving to a cloud-based gaming platform.  I’ve been pretty surprised with how many people are hating on it.  I’m of the opinion that once people fully understand what the cloud is and how it benefits gaming, this will be a transformational evolution in gaming platforms that people will … Continued...(read more)

Why You Will Love The XBoxOne Cloud

$
0
0
There’s been a lot said about XBoxOne moving to a cloud-based gaming platform.  I’ve been pretty surprised with how many people are hating on it.  I’m of the opinion that once people fully understand what the cloud is and how it benefits gaming, this will be a transformational evolution in gaming platforms that people will … Continued...(read more)
Viewing all 29128 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>