Windows 8.1 上的 WinJS

使用 JavaScript 构建更高效的 Windows 应用商店应用程序:性能

Eric Schmidt

下载代码示例

在探索如何构建更高效的 Windows 应用商店的应用时,我最先介绍了错误处理。在这第二篇文章中,我将介绍用于改进 Windows 应用商店的应用性能的几个方法,重点讨论内存使用情况和 HTML UI 响应能力。我将介绍 Windows 8.1 的 Windows JavaScript 库 (WinJS 2.0) 中新的可预测对象生命周期模型。然后,介绍 Web 工作线程以及 WinJS 2.0 中的新 Scheduler API,这两者都可以完成后台任务而不会将 UI 锁定。与上一篇文章一样,我将介绍用于查找和解决问题的两个诊断工具。

我假设您对使用 JavaScript 构建 Windows 应用商店的应用已相当熟悉。如果您对这个平台相对陌生,建议您从基本的“Hello World”示例 (bit.ly/vVbVHC) 开始,或者从挑战性更大的 JavaScript 的“Hilo”示例 (bit.ly/SgI0AA) 开始。如果未阅读过上一篇文章,可在 msdn.microsoft.com/magazine/dn519922 找到这篇文章。

设置示例

本文将利用具体示例进行说明,您可以用自己的代码对这些示例进行测试。您可以浏览这些代码,或将完整代码下载以便在闲暇时细读。

我将使用不同于上一篇文章的测试用例,因此,如果您要按照这些用例执行,需要向全局导航栏添加一些新按钮。(如果您愿意,也可以开始一个全新的导航布局应用程序项目。)图 1 显示了新的 NavBarCommand。

图 1 Default.html 中的其他 NavBarCommand

<div data-win-control="WinJS.UI.NavBar">
  <div data-win-control="WinJS.UI.NavBarContainer">
    <!-- Other NavBarCommand elements. -->
    <div id="dispose"
      data-win-control="WinJS.UI.NavBarCommand"
      data-win-options="{
        location: '/pages/dispose/dispose.html',
        icon: 'delete',
        label: 'Dispose pattern in JS'
    }">
    </div>
    <div id="scheduler"
      data-win-control="WinJS.UI.NavBarCommand"
      data-win-options="{
        location: '/pages/scheduler/scheduler.html',
        icon: 'clock',
        label: 'Scheduler'
    }">
    </div>
    <div id="worker"
      data-win-control="WinJS.UI.NavBarCommand"
      data-win-options="{
        location: '/pages/worker/worker.html',
        icon: 'repair',
        label: 'Web worker'
    }">
    </div>
  </div>
</div>

对于这些测试用例,我使用一个更实际的应用程序方案,让应用程序显示来自 Web 的内容。该应用程序从美国国会图书馆印刷品与照片联机目录 Web 服务 (1.usa.gov/1d8nEio) 提取数据。我编写了一个模块,它将对该 Web 服务的调用包装在 promise 对象中,并定义用于存储所接收数据的类。图 2 显示了该模块,它在名为 searchLOC.js (/js/searchLOC.js) 的文件中。

图 2 访问印刷品与照片联机目录 Web 服务

(function () {
  "use strict";
  var baseUrl = "http://loc.gov/pictures/"
  var httpClient = new Windows.Web.Http.HttpClient();
  function searchPictures(query) {
    var url = baseUrl + "search/?q=" + query + "&fo=json";
    var queryURL = encodeURI(url);
    return httpClient.getStringAsync(
      new Windows.Foundation.Uri(queryURL)).
      then(function (response) {
        return JSON.parse(response).results.map(function (result) {
          return new SearchResult(result);
        });
     });
  }
  function getCollections() {
    var url = baseUrl + "?fo=json";
    return httpClient.getStringAsync(new Windows.Foundation.Uri(url)).
      then(function (response) {
         return JSON.parse(response).featured.
           map(function (collection) {
             return new Collection(collection);
         });
      });
  }
  function getCollection(collection) {
    var url = baseUrl + "search/?co=" + collection.code + "&fo=json";
    var queryUrl = encodeURI(url);
    return httpClient.getStringAsync(new Windows.Foundation.Uri(queryurl)).
      then(function (response) {
        collection.pictures = JSON.parse(response).
          results.map(function (picture) {
            return new SearchResult(picture);
        });
        return collection;
      });
  }
  function Collection(info) {
    this.title = info.title;
    this.featuredThumb = info.thumb_featured;
    this.code = info.code;
    this.pictures = [];
  }
  function SearchResult(data) {
    this.pictureThumb = data.image.thumb;
    this.title = data.title;
    this.date = data.created_published_date;
  }
  WinJS.Namespace.define("LOCPictures", {
    Collection: Collection,
    searchPictures: searchPictures,
    getCollections: getCollections,
    getCollection: getCollection
  });
})();

请注意,要从项目根目录处的 default.html 链接到 searchLOC.js 文件,然后再调用它。

释放对象

在 JavaScript 中,可通过词汇环境或引用链访问的对象都保留在内存中。删除对对象的所有引用后,垃圾收集器取消分配给该对象的内存。只要存在对该对象的引用,该对象就保留在内存中。如果对某个对象的引用(因而该对象自身)在不再需要时仍然存在,则会发生内存泄漏。

JavaScript 应用程序中发生内存泄漏的一个常见原因是“僵尸”对象,这种泄漏通常在某个 JavaScript 对象引用 DOM 对象、而该 DOM 对象已从文档中删除(通过对 removeChild 或 innerHTML 的调用)的情况下发生。相应的 JavaScript 对象保留在内存中,即使相应的 HTML 已消失也是如此:

var newSpan = document.createElement("span");
document.getElementById("someDiv").appendChild(newSpan);
document.getElementById("someDiv").innerHTML = "";
WinJS.log && WinJS.log(newSpan === "undefined");
// The previous statement outputs false to the JavaScript console.
// The variable "newSpan" still remains even though the corresponding
// DOM object is gone.

对于普通网页,对象的寿命仅会延长为浏览器显示页面的时间长短。Windows 应用商店的应用无法忽略这种内存泄漏。应用程序通常将单一 HTML 页面用作内容宿主,该页面在整个应用程序会话期间一直存在(可能持续数天甚至数月)。在应用程序改变状态时(例如,用户从一个页面导航到另一个页面,或者滚动 ListView 控件使某些内容不再显示)而又没有清理分配给不需要的 JavaScript 对象的内存时,应用程序就无法使用这部分内存。

检查内存泄漏

幸运的是,Visual Studio 2013 有一些新功能,可帮助开发人员跟踪内存泄漏,尤其是“性能和诊断”窗口。在此测试用例和下一个测试用例中,我将演示几个它所包含的工具。

在此测试用例中,我向解决方案添加一个自定义控件,故意允许内存泄漏。此控件名为 SearchLOCControl (/js/SearchLOCControl.js),它创建一个搜索文本框,然后在接收到查询响应后显示结果。图 3 显示了 SearchLOCControl.js 的代码。同样请注意从 default.html 链接到这个新的 JavaScript 文件。

图 3 自定义 SearchLOCControl

(function () {
  "use strict";
  WinJS.Namespace.define("SearchLOCControl", {
    Control: WinJS.Class.define(function (element) {
      this.element = element;
      this.element.winControl = this;
      var htmlString = "<h3>Library of Congress Picture Search</h3>" +
        "<div id='searchQuery' data-win-control='WinJS.UI.SearchBox'" +
          "data-win-options='{ placeholderText: \"Browse pictures\" }'></div>" +
          "<br/><br/>" +
          "<div id='searchResults' class='searchList'></div>" +
          "<div id='searchResultsTemplate'" +
            "data-win-control='WinJS.Binding.Template'>" +
            "<div class='searchResultsItem'>" +
              "<img src='#' data-win-bind='src: pictureThumb' />" +
              "<div class='details'>" +
                "<p data-win-bind='textContent: title'></p>" +
                "<p data-win-bind='textContent: date'></p>" +
              "</div>" +
            "</div>"+
        "</div>";
   // NOTE: This is an unusual technique for accomplishing this
   // task. The code here is written for extreme brevity.       
      MSApp.execUnsafeLocalFunction(function () {
        $(element).append(htmlString);
        WinJS.UI.processAll();
      });
      this.searchQuery = $("#searchQuery")[0];
      searchQuery.winControl.addEventListener("querysubmitted", this.submitQuery);
      }, {
        submitQuery: function (evt) {
          var queryString = evt.target.winControl.queryText;
          var searchResultsList = $("#searchResults")[0];
          $(searchResultsList).append("<progress class='win-ring'></progress>");
          if (queryString != "") {
            var searchResults = LOCPictures.searchPictures(queryString).
              then(function (response) {
                var searchList = new WinJS.Binding.List(response),
                  searchListView;
                if (searchResultsList.winControl) {
                  searchListView = searchResultsList.winControl;
                  searchListView.itemDataSource = searchList.dataSource;
                }
                else {
                  searchListView = new WinJS.UI.ListView(searchResultsList, {
                    itemDataSource: searchList.dataSource,
                    itemTemplate: $("#searchResultsTemplate")[0],
                    layout: { type: WinJS.UI.CellSpanningLayout}
                  });
                }
                WinJS.UI.process(searchListView);
             });
           }
         }
      })
   })
})();

请注意,我使用 jQuery 构建自定义控件,这个控件是使用 NuGet 程序包管理器添加到解决方案的。将 NuGet 程序包下载到解决方案中后,需要在 default.html 中手动添加对 jQuery 库的引用。

SearchLOCControl 依赖于我已添加到 default.css (/css/default.css) 的某种样式,如图 4 所示。

图 4 添加到 Default.css 的样式

.searchList {
  height: 700px !important;
  width: auto !important;
}
.searchResultsItem {
  display: -ms-inline-grid;
  -ms-grid-columns: 200px;
  -ms-grid-rows: 150px 150px
}
  .searchResultsItem img {
    -ms-grid-row: 1;
    max-height: 150px;
    max-width: 150px;
  }
  .searchResultsItem .details {
    -ms-grid-row: 2;
  }

现在,向解决方案添加一个名为 dispose.html (/pages/­dispose/dispose.html) 的新页面控件,然后在 dispose 的 <section> 标记内添加以下 HTML 标记,创建自定义控件:

<button id="dispose">Dispose</button><br/><br/>
<div id="searchControl" data-win-control="SearchLOCControl.Control"></div>

最后,向 dispose.js 文件 (/pages/dispose/dispose.js) 中的 PageControl.ready 事件处理程序添加代码,这段代码将控件宿主 <div> 的 innerHTML 设置为空字符串,轻易销毁控件,产生内存泄漏,如图 5 所示。

图 5 Dispose.js 中用于“销毁”自定义控件的代码

(function () {
  "use strict";
  WinJS.UI.Pages.define("/pages/dispose/dispose.html", {
    ready: function (element, options) {
      WinJS.UI.processAll();
      $("#dispose").click(function () {
        var searchControl = $("#searchControl")[0];
        searchControl.innerHTML = "";
      });
    }
  // Other page control code.
  });
})();

现在,我可以测试控件的内存使用情况。“性能和诊断”窗口提供几个工具来测量 Windows 应用商店的应用的性能,包括 CPU 采样、应用程序能量消耗、UI 响应能力和 JavaScript 函数计时。(您可以在 Visual Studio 团队博客 bit.ly/1bESdOH 详细了解这些工具。)如果“性能和诊断”窗格还不可见,则需要通过“调试”菜单 (Visual Studio Express 2013 for Windows) 或通过“分析”菜单(Visual Studio Professional 2013 和 Visual Studio Ultimate 2013)将其打开。

对于此测试,我使用 JavaScript 内存监视工具。测试执行步骤如下:

  1. 在“性能和诊断”窗口中,选择“JavaScript 内存”,然后单击“开始”。项目随后在调试模式下运行。如果出现“用户帐户控制”对话框提示,单击“是”。
  2. 在应用程序项目运行的同时,导航到释放页面,然后切换到桌面。在 Visual Studio 中的当前诊断会话(标题为“Report*.diagsession”的选项卡”)中,单击“拍摄堆快照”。
  3. 切换回正在运行的应用程序。在搜索框中,输入一个查询词(例如,“Lincoln”),然后按 Enter。此时出现一个 ListView 控件,它显示图像搜索结果。
  4. 切换回桌面。在 Visual Studio 中的当前诊断会话(标题为“Report*.diagsession”的选项卡”)中,单击“拍摄堆快照”。
  5. 切换回正在运行的应用程序。单击“释放”按钮。自定义控件从页面消失。
  6. 切换回桌面。在 Visual Studio 中的当前诊断会话中(标题为“Report*.diagsession”的选项卡”),单击“拍摄堆快照”,然后单击“停止”。诊断会话中现在列出了三个快照,如图 6 所示。

Memory Usage Before Implementing the Dispose Pattern
图 6 实现 Dispose 模式前的内存使用情况

通过诊断数据,我可以分析自定义控件的内存使用情况。通过快速浏览诊断会话,我怀疑“释放”控件并没有释放与它关联的所有内存。

在报告中,可以检查每个快照的堆上的 JavaScript 对象。我需要知道,从 DOM 中删除自定义控件之后,什么还保留在内存中。单击与第三个快照(图 6 中的快照 3)中的堆上的对象数目相关联的链接。

首先,查看“控制器”视图,它显示按保留大小排序的对象的列表。占用最多内存、可能最容易释放的对象列在最上面。在“控制器”视图中,我看到一个对 <div> 的引用,其 ID 值为“searchControl”。当我将其展开时,我看到搜索框 ListView 以及与它关联的数据都在内存中。

我右键单击 searchControl <div> 所在的行,然后在根视图中选择“显示”,我看到按钮单击事件处理程序也还在内存中,如图 7 所示。

Unattached Event Handler Code Taking up Memory
图 7 占用内存的未连接事件处理程序代码

令人欣慰的是,只对代码进行少量更改,我就可以方便地解决问题。

在 WinJS 中实现 Dispose 模式

在 WinJS 2.0 中,所有 WinJS 控件都实现“释放”模式以解决内存泄漏问题。每当 WinJS 控件超出作用域时(例如,当用户导航至其他页面时),WinJS 都会清理对它的所有引用。该控件会标记为释放,这意味着内部垃圾收集器知道要释放分配给该对象的所有内存。

WinJS 中的 Dispose 模式有三个重要特性,控件必须提供这三个特性才能正确释放:

  • 顶层容器 DOM 元素必须具有 CSS 类“win-disposable”。
  • 控件的类必须包含一个名为 _disposed、初始设置为 false 的字段。您可以通过调用 WinJS.Utilities.markDisposable 将此成员添加到控件(连同 win-disposable CSS 类)。
  • 定义控件的 JavaScript 类必须公开一个“dispose”方法。在 dispose 方法中:
    • 需要释放分配给与控件关联的对象的所有内存。
    • 所有事件处理程序都需要与子 DOM 对象分离。
    • 控件的所有子项必须调用其 Dispose 方法。为此,最好对 host 元素调用 WinJS.Utilities.disposeSubTree。
    • 需要取消可能在控件内引用的所有未解决的 promise(通过调用 Promise.cancel 方法,然后使该变量为空)。

这样,在 SearchLOCControl.Control 的构造函数中,我添加以下代码行:

this._disposed = false;
WinJS.Utilities.addClass(element, "win-disposable");

接下来,在 SearchLOCControl 类定义内(对 WinJS.Class.define 的调用),我添加一个名为 dispose 的新实例成员。dispose 方法的代码如下:

dispose: function () {
  this._disposed = true;
  this.searchQuery.winControl.removeEventListener("querysubmitted",
    this.submitQuery);
  WinJS.Utilities.disposeSubTree(this.element);
  this.searchQuery = null;
  this._element.winControl = null;
  this._element = null;
}

一般而言,您无需清理自己代码中的任何变量,它们完全包含在元素代码内。执行完控件的代码后,所有内部变量也都消失。但是,如果控件的代码引用其自身之外的内容(如 DOM 元素),则需要使该引用为空。

最后,我在 dispose.js (/pages/dispose/dispose.js) 中添加对 dispose 方法的显式调用。下面是 dispose.html 中按钮更新后的单击事件处理程序:

$("#dispose").click(function () {
  var searchControl = $("#searchControl")[0];
  searchControl.winControl.dispose();
  searchControl.innerHTML = "";
});

现在,当我运行同样的 JavaScript 内存测试时,诊断会话看起来要好得多(请参见图 8)。

Memory Usage After Implementing Dispose
图 8 实现 Dispose 的内存使用情况

通过检查内存堆可以看到,“searchControl”<div> 不再有关联子元素(请参见图 9)。内存中没有任何子控件,也没有关联的事件处理程序(请参加图 10)。

Dominators View After Implementing Dispose
图 9 实现 Dispose 后的“控制器”视图

Roots View After Implementing Dispose
图 10 实现 Dispose 后的“根”视图

改善响应能力:计划程序和 Web 工作线程

当 UI 基于外部进程在等待更新时,应用程序可能会变得无响应。例如,如果应用程序为了填充 UI 控件而对 Web 服务进行多次请求,则该控件(这种情况下是整个 UI)可能会在等待请求时没有响应。这会造成应用程序中断或看起来无响应。

为说明这一点,我创建另一个测试用例,用国会图书馆 Web 服务所提供的“特色收藏”填充 Hub 控件。我将测试用例名为 scheduler.html 的新页面控件添加到我的项目 (/pages/scheduler/scheduler.js)。在该页面的 HTML 中,声明一个 Hub 控件,它包含六个 HubSection 控件(每个特色收藏各使用一个)。图 11 显示了 scheduler.html 中 <section> 标记内的 Hub 控件的 HTML。

图 11 Scheduler.html 中声明的 Hub 和 HubSection 控件

<div id="featuredHub" data-win-control="WinJS.UI.Hub">
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 1'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 2'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 3'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 4'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 5'
    }"
    class="section">
  </div>
  <div data-win-control="WinJS.UI.HubSection"
    data-win-options="{
      header: 'Featured Collection 6'
    }"
    class="section">
  </div>
</div>

接下来,我从 Web 服务获取特色收藏数据。我向解决方案添加一个名为 data.js 的新文件 (/js/data.js),解决方案调用 Web 服务并返回 WinJS.Binding.List 对象。图 12 显示了用于获取特色收藏数据的代码。同样请注意从 default.html 链接到 data.js。

图 12 从 Web 服务获取数据

(function () {
  "use strict";
  var data = LOCPictures.getCollections().
  then(function (message) {
    var data = message;
    var dataList = new WinJS.Binding.List(data);
    var collectionTasks = [];
    for (var i = 0; i < 6; i++) {
      collectionTasks.push(getFeaturedCollection(data[i]));
    }
    return WinJS.Promise.join(collectionTasks).then(function () {
      return dataList;
    });
  });
  function getFeaturedCollection(collection) {
    return LOCPictures.getCollection(collection);
  }
 WinJS.Namespace.define("Data", {
   featuredCollections: data
 });
})();

现在,我需要将数据插入到 Hub 控件中。在 scheduler.js 文件 (/pages/scheduler/scheduler.js) 中,我将一些代码添加到 PageControl.ready 函数,定义一个新函数 populateSection。图 13 显示了完整代码。

图 13 动态填充 Hub 控件

(function () {
  "use strict";
  var dataRequest;
  WinJS.UI.Pages.define("/pages/scheduler/scheduler.html", {
    ready: function (element, options) {
      performance.mark("navigated to scheduler");
      dataRequest = Data.featuredCollections.
        then(function (collections) {
          performance.mark("got collection");
          var hub = element.querySelector("#featuredHub");
            if (!hub) { return; }
            var hubSections = hub.winControl.sections,
            hubSection, collection;
            for (var i = 0; i < hubSections.length; i++) {
              hubSection = hubSections.getItem(i);
              collection = collections.getItem(i);
              populateSection(hubSection, collection);
            }
        });
    },
    unload: function () {
      dataRequest.cancel();
    }
    // Other PageControl members ...
  });
  function populateSection(section, collection) {
    performance.mark("creating a hub section");
    section.data.header = collection.data.title;
    var contentElement = section.data.contentElement;
    contentElement.innerHTML = "";
    var pictures = collection.data.pictures;
    for (var i = 0; i < 6; i++) {
      $(contentElement).append("<img src='" 
        + pictures[i].pictureThumb + "' />");
      (i % 2) && $(contentElement).append("<br/>")
    }
    }
})();

请注意,在图 13 中,我捕获了对 promise(由对 Data.getFeaturedCollections 的调用返回)的引用,然后在页面卸载时显式取消该 promise。这样可避免在以下情况中可能出现的争用情况:用户导航至该页面,在对 getFeaturedCollections 的调用返回之前离开该页面。

按 F5 并导航至 scheduler.html 时,我注意到 Hub 控件会在页面加载后缓慢填充。在我的计算机上这有点让人烦,不过在功能逊色的计算机上,这种滞后会相当明显。

Visual Studio 2013 中有一些工具可测量 Windows 应用商店的应用中 UI 的响应能力。在“性能和诊断”窗格中,选择“HTML UI 响应能力”测试,然后单击“开始”。应用程序开始运行后,导航至 scheduler.html,观察 Hub 控件中出现的结果。完成任务后,我切换回桌面,然后在诊断会话选项卡上单击“停止”。图 14 显示了结果。

HTML UI Responsiveness for Scheduler.html
图 14 Scheduler.html 的 HTML UI 响应能力

我看到帧速率下降到 3 FPS 大约半秒。我选择较低的帧速率周期以查看详细信息(请参见图 15)。

Timeline Details Where the UI Thread Evaluates Scheduler.js
图 15 UI 线程计算 Scheduler.js 时的时间线详细信息

在时间线上的这个时间处(图 15),该 UI 线程被吸收在运行中的 scheduler.js 中。如果仔细查看时间线详细信息,会看到几个用户标记(橙色标记)。这些标记指示出对代码中 performance.mark 的特定调用。在 scheduler.js 中,对 performance.mark 的第一次调用发生在 scheduler.html 加载时。用内容填充每个 HubSection 控件会启动后续调用。从结果上看,用在计算 scheduler.js 上的一半以上时间发生在我导航到该页面时(第一个用户标记)以及用图像填充第六个 HubSection 时(最后一个用户标记)。

(请注意,结果因硬件而异。本文中介绍的 HTML UI 响应能力测试是在 Microsoft Surface Pro 上运行的,该设备采用运行速度为 1.7Ghz 的第三代 Intel Core i5-3317U 处理器和 Intel HD Graphics 400 显卡。)

为减少滞后,我可以重构代码,以便以交错方式填充 HubSection 控件。用户在导航到应用程序后不久,就会看到其中的内容。第一节到两个 Hub 节的内容应在导航之后立即加载,其他的 HubSection 可随后再加载。

计划程序

JavaScript 是单线程环境,这意味着所有内容都在 UI 线程上。在 WinJS 2.0 中,Microsoft 推出 WinJS.Utilities.Scheduler 来组织在 UI 线程上执行的工作(有关详细信息,请参见 bit.ly/1bFbpfb)。

调度程序创建一个要在应用程序中的 UI 线程上运行的作业的队列。作业根据优先级完成,优先级较高的作业可以抢占或推迟优先级较低的作业。将围绕 UI 线程上的实际用户交互对作业进行计划,计划程序将调用之间的时间进行分割,尽可能多地完成排队的作业。

如前所述,计划程序基于使用 WinJS.Utilities.Scheduler.Priority 枚举设置的优先级来执行作业。该枚举有 7 个值(按降序排列):max、high、aboveNormal、normal、belowNormal、idle 和 min。优先级相同的作业按先进先出原则运行。

回到测试用例,我在计划程序上创建一个作业,以便在 scheduler.html 加载时填充每个 HubSection。对每个 HubSection 调用 Scheduler.schedule,并传入填充 HubSection 的函数。前两个作业以 normal 优先级运行,所有其他作业在 UI 线程空闲时运行。在计划方法 thisArg 的第三个参数中,为作业传入一些上下文。

计划方法返回一个 Job 对象,此对象可让我监视作业的进度或将其取消。对于每个作业,我向其 Owner 属性分配相同的 OwnerToken 对象。这样,我就可以取消归属于该 Owner 标记的所有已计划的作业。参见图 16

图 16 使用 Scheduler API 更新的 Scheduler.js

(function () {
  "use strict";
  var dataRequest, jobOwnerToken;
  var scheduler = WinJS.Utilities.Scheduler;
  WinJS.UI.Pages.define("/pages/scheduler/scheduler.html", {
    ready: function (element, options) {
      performance.mark("navigated to scheduler");
      dataRequest = Data.featuredCollections.
        then(function (collections) {
          performance.mark("got collection");
          var hub = element.querySelector("#featuredHub");
          if (!hub) { return; }
          var hubSections = hub.winControl.sections,
          hubSection, collection, priority;
          jobOwnerToken = scheduler.createOwnerToken();
          for (var i = 0; i < hubSections.length; i++) {
            hubSection = hubSections.getItem(i);
            collection = collections.getItem(i);
            priority ==  (i < 2) ? scheduler.Priority.normal :
              scheduler.Priority.idle;
            scheduler.schedule(function () {
                populateSection(this.section, this.collection)
              },
              priority,
              { section: hubSection, collection: collection },
              "adding hub section").
            owner = jobOwnerToken;
          }
        });
      },
      unload: function () {
       dataRequest && dataRequest.cancel();
       jobOwnerToken && jobOwnerToken.cancelAll();
    }
    // Other PageControl members ...
  });
  function populateSection(section, collection) {
    performance.mark("creating a hub section");
    section.data.header = collection.data.title;
    var contentElement = section.data.contentElement;
    contentElement.innerHTML = "";
    var pictures = collection.data.pictures;
    for (var i = 0; i < 6; i++) {
      $(contentElement).append("<img src='" 
        + pictures[i].pictureThumb + "' />");
      (i % 2) && $(contentElement).append("<br/>")
    }
  }
})();

现在,运行 HTML UI 响应能力诊断测试时,可以看到不同的结果。图 17 显示了第二个测试的结果。

HTML UI Responsiveness After Using Scheduler.js
图 17 使用 Scheduler.js 后的 HTML UI 响应能力

在第二个测试期间,应用程序在更短时间内丢失了更少的帧。应用程序体验也改善了:Hub 控件的填充速度更快,几乎没有滞后。

处理 DOM 会影响 UI 响应能力

向 HTML 页面上的 DOM 添加新元素可能对性能造成不利影响,尤其是在添加许多新元素时。页面需要重新计算页面上其他项的位置,然后重新应用样式,最后重新绘制页面。例如,一个用于设置某个元素的上面和左侧边距、宽度和高度或显式样式的 CSS 指令会造成对页面进行重新计算。(我建议使用 WinJS 中的内置动画功能,或 CSS3 中提供的用于处理 HTML 元素位置的动画转换。)

不过,注入和显示动态内容是一种常用的应用程序设计。对于性能,最好是尽量使用平台提供的数据绑定。WinJS 中的数据绑定已针对快速和响应能力较强的 UX 进行优化。

否则,您需要决定是通过 innerHTML 将原始 HTML 作为字符串注入到其他元素中,还是使用 createElement 和 appendChild 每次添加一个元素。使用 innerHTML 通常会实现更好的性能,但是可能无法处理已插入的 HTML。

在我的示例中,我选择了 jQuery 中的 $.append 方法。通过 append,我可以以字符串形式传递原始 HTML,直接对新的 DOM 节点进行编程访问。(它还提供相当不错的性能。)

Web 工作线程

标准 Web 平台包括 Web Worker API,它可让应用程序脱离 UI 线程运行后台任务。简言之,Web 工作线程(或简称工作线程)允许在 JavaScript 应用程序中实现多线程处理。您可以将简单消息(字符串或简单 JavaScript 对象)传递给工作线程,工作线程使用 postMessage 方法将消息返回到主线程。

工作线程在与应用程序其余部分不同的脚本上下文中运行,这样它们无法访问 UI。您无法使用 createElement 创建新的 HTML 元素,也无法利用依赖于文档对象的第三方库的功能(例如,jQuery 函数 $)。但是,工作线程可以访问 Windows 运行时 API,这意味着它们可以写入应用程序数据,发出 Toast 和磁贴更新,甚至可以保存文件。工作线程非常适合不需要用户输入、计算成本较高或需要对某个 Web 服务进行多次调用的后台任务。如果需要有关 Web Worker API 的详细信息,请参阅工作线程参考文档 bit.ly/1fllmip

使用工作线程的好处是,UI 响应能力不受后台工作的影响。UI 保持响应,几乎没有帧丢失。此外,工作线程还可以导入不依赖于 DOM 的其他 JavaScript 库,包括用于 WinJS 的基本库 (base.js)。例如,您可以在工作线程中创建 promise。

另一方面,工作线程不是性能问题的万能药方。工作线程的周期仍会从计算机上可用的总 CPU 周期中分配,即使这些周期不是来自 UI 线程,也是如此。您需要明智地使用工作线程。

在下一个测试用例中,我将使用一个工作线程从国会图书馆检索一个图片集,并用这些图片来填充一个 ListView 控件。首先,我添加一个新脚本,将名为 LOC-worker.js 的工作线程存储到我的项目,如下所示:

(function () {
  "use strict";
  self.addEventListener("message", function (message) {
    importScripts("//Microsoft.WinJS.2.0/js/base.js", "searchLoC.js");
    LOCPictures.getCollection(message.data).
      then(
        function (response) {
          postMessage(response);
        });
  });
})();

我使用 importScripts 函数将 WinJS 库中的 base.js 和 seachLOC.js 脚本置于该工作线程的上下文中,使它们可供使用。

接下来,我将一个名为 worker.html 的新页面控件项添加到我的项目中 (/pages/worker/worker.html)。为了包含 ListView 控件并定义其布局,我在 worker.html 中的 <section> 标记内添加一个小标记。当工作线程返回时,将动态创建控件:

<div id="collection" class='searchList'>
  <progress class="win-ring"></progress>
</div>
<div id='searchResultsTemplate' data-win-control='WinJS.Binding.Template'>
  <div class='searchResultsItem'>
    <img src='#' data-win-bind='src: pictureThumb' />
    <div class='details'>
      <p data-win-bind='textContent: title'></p>
      <p data-win-bind='textContent: date'></p>
    </div>
  </div>
</div>

最后,我向 worker.js 添加代码以创建新的工作线程,然后基于响应来填充 HTML。图 18 显示了工作线程中的代码。

图 18 创建工作线程后填充 UI

(function () {
  "use strict";
  WinJS.UI.Pages.define("/pages/worker/worker.html", {
    ready: function (element, options) {
      performance.mark("navigated to Worker");
      var getBaseballCards = new Worker('/js/LOC-worker.js'),
        baseballCards = new LOCPictures.Collection({
          title: "Baseball cards",
          thumbFeatured: null,
          code: "bbc"
      });
      getBaseballCards.onmessage = function (message) {
         createCollection(message.data);
         getBaseballCards.terminate();
      }
      getBaseballCards.postMessage(baseballCards);
    }
  // Other PageControl members ...
  });
  function createCollection(info) {
    var collection = new WinJS.Binding.List(info.pictures),
      collectionElement = $("# searchResultsTemplate")[0],
      collectionList = new WinJS.UI.ListView(collectionElement, {
        itemDataSource: collection.dataSource,
        itemTemplate: $('#collectionTemplate')[0],
        layout: {type: WinJS.UI.GridLayout}
      });
  }
})();

在运行该应用程序并导航至该页面时,您会注意到导航与用图像填充该 ListView 控件之间有极小的滞后。如果通过 HTML UI 响应能力工具运行此测试用例,可以看到与图 19 类似的输出。

HTML UI Responsiveness When Using a Worker Thread
图 19 使用工作线程时的 HTML UI 响应能力

请注意,在我导航到 worker.html 页面之后(在时间线中的第一个用户标记之后),应用程序丢失的帧很少。UI 保持了非常高的响应性,因为数据的提取被卸载到了工作线程上。

何时在计划程序与工作线程 API 之间进行选择

因为计划程序和工作线程 API 可都用来在代码中管理后台任务,您可能想知道何时适合使用哪一个。(请注意,本文提供的两个代码示例并不是对两个 API 进行公平的完全对等比较。)

因为工作线程 API 在不同的线程上运行,在对等比较中,它可以实现比计划程序更好的性能。但是,因为计划程序使用 UI 线程的时间片,它采用当前页面的上下文。您可以使用调度程序来更新页面上的 UI 元素,也可以动态创建页面上的新元素。

如果后台代码需要以任何有意义的方式与 UI 交互,则应使用调度程序。但是,如果代码不依赖于应用程序的上下文,仅来回传递简单数据,则应考虑使用工作线程。使用工作线程的好处是,UI 响应能力不受后台工作的影响。

在 Windows 应用商店的应用中生成多个线程时,调度程序和 Web Worker API 不是唯一的选择。您还可以使用可创建新线程的 C++、C# 或 Visual Basic .NET 来构建 Windows 运行时组件。WinRT 组件可以公开 JavaScript 代码能够调用的 API。有关详细信息,请参见 bit.ly/19DfFaO

我不认为许多开发人员会着意编写有错误、有问题或无响应的应用程序(除非他们要撰写有关有错误、有问题和无响应的应用程序的文章)。关键是在用户使用应用程序之前,找到应用程序代码中的这些错误,修复它们。在本文章系列中,我演示了几个工具用于发现代码中的问题,以及编写更高效应用程序代码的方法。

当然,没有哪种方法或工具是可以自动解决问题的灵丹妙药。它们有助于改善体验,但还是需要您采取良好的编码做法。对于任何其他平台,这些编程基础知识通常适用于使用 JavaScript 生成的 Window 应用商店的应用。

Eric Schmidt 是 Microsoft 的 Windows 开发人员内容团队中的内容开发人员,负责撰写有关 Windows JavaScript 库 (WinJS) 的文章。以前,他在 Microsoft Office 部门为 Office 平台的应用程序编写代码示例。工作之余,他陪伴家人,演奏弦贝司、制作 HTML5 视频游戏,或者撰写有关塑料积木的博客文章 (historybricks.com)。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Kraig Brockschmidt、Greg Bulmash 和 Josh Williams
Kraig Brockschmidt 是 Windows Ecosystem 团队的高级程序经理,直接与开发人员社区和核心合作伙伴在构建 Windows 应用商店的应用领域进行协作。他是《Programming Windows Store Apps in HTML, CSS, and JavaScript》(现在为第二版)一书的作者,在 http://www.kraigbrockschmidt.com/blog 上分享其他见解。

Josh Williams 是 Windows 开发人员体验团队的软件开发工程师主管。他和他的团队构建了 Windows JavaScript 库 (WinJS)。

Greg Bulmash 为 Internet Explorer 开发人员中心 (https://msdn.microsoft.com/ie) 撰写文章,为 IE11 的 F12 开发人员工具编写文档。在业余时间,他尽力掌握最新编码技能,帮助孩子们通过组织 CoderDojo 的 Seattle 一章来学习编码。他偶尔会在 http://www.olddeveloper.com 发表博客文章。