关于 promise(用于使用 JavaScript 编写的 Windows 应用商店应用)
当您使用 JavaScript 编写 Windows 应用商店应用时,一旦执行涉及异步 API 的操作时,就会遇到称为 promise 的构造。此外,无需很长时间,为顺序异步操作编写 promise 链就会成为您的一种习惯。
但是,在开发的过程中,您可能会遇到其他使用 promise 的情形,此时您可能无法完全了解具体情况。为 ListView 控件优化项目呈现功能就是一个很好的例子,如在 HTML ListView 优化性能示例中所示。我们将在随后的一篇博文中继续探究这一主题。或者,您也可以了解一下 Josh Williams 在 //build 2012 大会的深入探讨 WinJS 讨论(略有修改)中展示的以下重要代码段:
list.reduce(function callback (prev, item, i) { var result = doOperationAsync(item); return WinJS.Promise.join({ prev: prev, result: result}).then(function (v) { console.log(i + ", item: " + item+ ", " + v.result); }); })
该代码段将加入到 promise 来完成并行异步操作,并根据 list 中的命令按顺序提供结果。如果您一看到该代码就可以立即了解其功能,那么您完全可以跳过本博文!否则,让我们来仔细看一下 promise 的真正工作原理,以及其在 WinJS(Windows JavaScript 库)中的表达式,以便了解这些模式类型。
promise 究竟是什么?承诺关系
首先,让我们来了解一下基本情况:promise 实际上无非就是一个代码构造或一个调用约定(如果您愿意)。因此,promise 与异步操作没有内在的关系,它们在这方面非常有用实属巧合!promise 只是一个对象,代表着可能会在将来某一时间可用(或现已可用)的值。这样,promise 的含义就像我们在人际关系中使用该术语一样。如果我说,“我承诺投递给您一打甜甜圈”,则很明显我无需即刻得到这些甜甜圈,而毫无疑问我假定自己可以在将来得到它们。一旦得到甜甜圈,我就会投递给您。
Promise 故暗示着两个代理方之间的关系:承诺发货的发货人与既是承诺的接受方又是收货人的消费者。发货人如何获得这些货物是其自己的事情。同样,消费者可以随意处理承诺和投递的货物。消费者甚至可与其他的消费者分享承诺。
在发货人和消费者之间,也存在着两个关系阶段,创建和履行。全部内容如下图所示。
通过该图中显示的流程,我们可以了解到分为两个阶段的关系是 promise 通过异步交货的方式得以履行的原因所在。这个过程的关键部分是一旦消费者认可请求(即 promise),其可以继续生活(异步),而不是等待(同步)。这意味着消费者在等待承诺履行的同时可以做其他的事情,例如响应其他的请求,而这也是异步 API 的初衷。如果已得到货物,情况会怎样?此时,promise 将立即履行,就像是一种同步调用约定。
当然,对于这一关系,我们还要考虑一些额外的东西。在日常生活中,您一定作出过承诺,并且别人也对您作出过承诺。尽管许多承诺已经履行,但许多承诺无法兑现也是事实,例如快递员在为您送披萨的途中发生了交通事故!背弃的承诺是无法改变的事实,无论是在我们的个人生活还是异步编程方面,我们都必须接受。
在承诺关系中,这意味着发货人需要一种表达,“对不起,我无法兑现该承诺”的方式,而消费者也需要一种了解该事实的方式。其次,作为消费者,我们有时会对别人向我们作出的承诺失去耐心!因此,如果发货人可以在履行承诺的过程中跟踪进展情况,消费者也需要一种获得该信息的方式。第三,消费者还可以取消订单,并告知发货人其不再需要该货物。
将这些要求添加到图中之后,我们就会看到完整的关系:
现在,我们来看看这些关系如何通过代码来体现。
promise 构造和 promise 链
事实上,promise 有着许多不同的建议或规范。Windows 和 WinJS 中使用的 promise 称为 Common JS/Promises A,表示 promise(由发货人返回并代表一个在未来提供的值)是一个包含 then 函数的对象。消费者通过调用 then 表示订阅承诺的履行。(Windows 中的 promise 还支持称为 done 的类似函数,不久我们将会看到其用于 promise 链。)
消费者可向该函数传递三个作为参数的可选函数,具体顺序如下:
- completed 处理程序。当承诺的值可用时,发货人将调用该函数,而当该值现已可用时,completed 处理程序将从 then 中被立即调用(同步)。
- 可选的 error 处理程序。它将在获取承诺的值失败时调用。对于任何特定的承诺,均无法在调用 error 处理程序的情况下调用 completed 处理程序。
- 可选的 progress 处理程序。在操作支持的情况下,它将通过中间结果定期调用。(在 WinRT 中,这意味着 API 包含一个 IAsync[Action | Operation]WithProgress 的返回值;而 IAsync[Action | Operation] 则没有该值。)
请注意,您可以为任何这些参数传递 null,例如当您只想附加 error 处理程序,而不是 completed 处理程序的时候。
就关系中的另一方而言,消费者可通过多次调用 then,对同一个 promise 订阅任意数量的处理程序。另外,消费者还可与其他可随意调用 then 的消费者分享 promise。这完全受支持。
这意味着 promise 必须管理其收到的所有处理程序的列表,并在适当的时候调用它们。Promise 还需要允许取消,如完整关系中所述。
Promises A 规范中的另一项要求是 then 方法本身返回 promise。当传递给第一个 promise.then 的 completed 处理程序返回时,履行第二个 promise,并且返回值将作为第二个 promise 的结果提供。请看以下代码段:
var promise1 = someOperationAsync(); var promise2 = promise1.then(function completedHandler1 (result1) { return 7103; } ); promise2.then(function completedHandler2 (result2) { });
此处的执行链是启动 someOperationAsync,并返回 promise1。当操作进行时,我们调用 promise1.then,其将立即返回 promise2。除非异步操作的结果已经可用,否则 completedHandler1 不会被调用,请务必明确这一点。假定我们仍然在等待,因此我们直接调用 promise2.then,此时 completedHandler2 也不会调用。
过了一段时间后,someOperationAsync 完成并提供一个值(例如 14618)。现在,promise1 将履行,其将使用该值调用 completedHandler1,因而 result1 是 14618。现在,completedHandler1 执行并返回值 7103。此时,promise2 将履行,因此其使用等于 7103 的 result2 调用 completedHandler2。
如果 completed 处理程序返回其他 promise 会怎样呢?这种情况的处理方式有些不同。假设上述代码中的 completedHandler1 返回了以下 promise:
var promise2 = promise1.then(function completedHandler1 (result1) { var promise2a = anotherOperationAsync(); return promise2a; });
在此情况下,completedHandler2 中的 result2 本身将不会成为 promise2a,而是会成为 promise2a 的 fulfillment 值。也就是说,由于 completed 处理程序返回了一个 promise,promise1.then 返回的 promise2 将使用 promise2a 的结果履行。
准确地说,正是这一特性使顺序异步操作能够链接在一起,每个操作的结果将提供给下一个操作。在没有中间变量或命名的处理程序的情况下,您通常会看到 promise 链的这一模式:
operation1().then(function (result1) { return operation2(result1) }).then(function (result2) { return operation3(result2); }).then(function (result3) { return operation4(result3); }).then(function (result4) { return operation5(result4) }).then(function (result5) { //And so on });
当然,每个 completed 处理程序可能会使用其收到的结果执行更多的操作,但在所有的链中,您都可以看到该核心构造。此处一个接一个执行的 then 方法亦是如此,它们所做的一切就是单独保存特定的 completed 处理程序并返回其他 promise。因此,当到达代码结尾处时,operation1 将已启动,并且不会调用 completed 处理程序。但是,来自所有 then 调用的大量中间 promise 将被创建出来并彼此连结在一起,以随着顺序操作的进展对链进行管理。
值得一提的是,通过在之前的 completed 处理程序中嵌套各个顺序操作,可以获得相同的顺序,这样您就可以避免使用大量的 return 语句。但是,这种嵌套会遇到严重的缩进问题,特别是在您开始向 then 添加包含每个调用的 progress 处理程序和 error 处理程序的情况下。
说到这里,WinJS 中的 promise 的特性之一就是,当链中的任一环节发生错误时,错误即会自动传播至链的末端。这意味着您应该在最后一次调用 then 时附加一个单个的 error 处理程序,而不是在每一个级别都附加它。不过需要引起注意的是,出于各种微妙的原因,如果链中的最后一环是 then 调用,则这些错误就会消失。因此,WinJS 还提供了 promise 的 done 方法。该方法可接受与 then 相同的参数,但表明链已结束(它将返回 undefined,而不是其他 promise)。因此,附加到 done 的 error 处理程序会针对整个链中的任意错误进行调用。此外,在没有 error 处理程序的情况下,done 将向应用级别抛出异常,以便由 WinJS.Application.onerror 事件的 window.onerror 进行处理。简而言之,理想的情况下,所有链都应结束于 done,以确保出现的异常可以得到妥善的处理。
当然,如果您编写的函数的目的是从长长的 then 调用链中返回最后一个 promise,则您仍然需要在结尾处使用 then:接下来,将由可能在其他链中使用该 promise 的调用者来完全负责处理错误。
创建 promise:WinJS.Promise 类
尽管您始终可以基于 Promises A 规范创建您自己的 promise 类,但实际上要有大量的工作要做,因此最后交给库来完成。为此,WinJS 提供了强大、灵活且经过良好测试的 promise 类,称为 WinJS.Promise。这样,您就可以基于不同的值和操作轻松创建 promise,而无需管理发货人/消费者关系的详细信息或 then 的行为。
当需要时,您可以(应该)使用 new WinJS.Promise 或合适的 helper 函数(如下一节中所述)来基于异步操作和现有的(同步)值创建 promise。请记住,promise 只是一个代码构造:没有要求规定 promise 必须封装异步操作或其他任何异步内容。同样,仅仅在 promise 中封装一些代码无法使其自动异步运行。当您自己创建 promise 类时,情况依然如此。
作为直接使用 WinJS.Promise 的简单示例,假设我们希望执行一个长时间的计算(把从 1 到某最大值之间的大量数字加起来),但通过异步方式完成。我们可以为该例程创建我们自己的回调机制,但如果我们将其封装在 promise 中,则允许它被链接起来或加入到其他 API 中的其他 promise。(除此之外,WinJS.xhr 函数还在 promise 中封装了 JavaScript 的异步 XmlHttpRequest,从而您无需处理后者的特定事件构造。)
当然,我们也可以使用 JavaScript worker 进行长时间的计算,但为了便于说明,我们将其保留在 UI 线程上,并使用 setImmediate 将操作拆分为多个步骤。以下显示了在 promise 构造中使用 WinJS.Promise 的具体实施方法:
function calculateIntegerSum(max, step) { //The WinJS.Promise constructor's argument is an initializer function that receives //dispatchers for completed, error, and progress cases. return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) { var sum = 0; function iterate(args) { for (var i = args.start; i < args.end; i++) { sum += i; }; if (i >= max) { //Complete--dispatch results to completed handlers completeDispatch(sum); } else { //Dispatch intermediate results to progress handlers progressDispatch(sum); setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) }); } } setImmediate(iterate, { start: 0, end: Math.min(step, max) }); }); }
当调用 new WinJS.Promise 时,其构造函数中的唯一参数是一个 initializer 函数(此时为匿名函数)。initializer 可以封装要执行的操作,但请务必明确该函数将在 UI 线程上同步执行。因此,如果我们在不使用 setImmediate 的情况下执行长时间的计算,就会阻塞整个这段时间的 UI 线程。另外,将代码放到 promise 中无法使其自动异步运行 — 需要使用 initializer 函数进行相应的设置。
就参数而言,initializer 函数可针对“已完成”、“错误”和“进行中”的情况收到三个调度程序(受 promise 支持)。显而易见,在操作过程中,我们可以使用适当的参数在适当的时间调用这些调度程序。
我将这些函数称为“调度程序”,这是因为它们与消费者向 promise 订阅的 then 方法(或 done,但为不会一直提醒您)完全不同。在系统内部,WinJS 管理着大量的处理程序,这正是任意数量的消费者可订阅任意数量的处理程序的原因所在。当您调用这些调度程序之一时,WinJS 通过其内部列表进行迭代,并以您的名义调用所有这些处理程序。WinJS.Promise 也可以确保它的 then 按照链接的要求返回其他 promise。
简而言之,WinJS.Promise 提供了与 promise 有关的所有详细信息。这样,您就可以将精力集中放在 promise 所代表的核心操作上,这在 initializer 函数中得到了很好的体现。
创建 promise 的 helper
创建 promise 的主要 helper 函数是静态方法 WinJS.Promise.as,其可以将 any 值封装到 promise 中。基于已存在的值的封装程序反过来可以调用传递到 then 的任何 completed 处理程序。这样,您就可以明确将任意已知值视为 promise,从而能够混合 promise 并(通过加入或链接)将它们与其他 promise 组合。将 as 用于一个现有的 promise 只能返回该 promise。
另一个静态的 helper 函数是 WinJS.Promise.timeout,其可以基于 setTimeout 和 setImmediate 提供便捷的封装程序。当第二个 promise 无法在特定时间(毫秒)内履行时,您也可以创建一个 promise 来将其取消。
请注意,基于 setTimeout 和 setImmediate 的 timeout promise 本身通过 undefined 来履行。于是出现了一个常见的问题,“在超时之后,如何使用这些 promise 来提供一些其他的结果?”答案中包含了一个事实,那就是 then 将返回另一个 promise,该 promise 使用 completed 处理程序的返回值来履行。例如,下面这行代码:
var p = WinJS.Promise.timeout(1000).then(function () { return 12345; });
创建在一秒钟之后使用值 12345 履行的 promise p。换言之,WinJS.Promise.timeout(…).then(function () { return <value>} ) 是在给定超时之后提供 <value> 的模式。如果 <value> 本身是另一个 promise,则意味着在超时之后的某个时间点提供该 promise 的履行值。
取消和生成 promise 错误
在我们刚刚看到的代码中,您可能已经注意到了两个缺陷。首先,操作在开始之后将无法被取消。其次,我们无法非常有效地处理错误。
应对这两种情况的技巧是,生成 promise 的函数(例如 calculateIntegerSum 函数)必须始终返回 promise。如果操作无法完成或最初根本未启动,则该 promise 将处于错误状态。这意味着该 promise 没有(也绝不会有)可传递给任何 completed 处理程序的结果:它只会调用其 error 处理程序。实际上,如果消费者调用已处于错误状态的 promise 的 then 方法,则该 promise 将立即(同步)调用传递给 then 的 error 处理程序。
WinJS.Promise 进入错误状态的原因有两个:消费者调用其 cancel 方法或 initializer 函数中的代码调用 error 调度程序。在发生这种情况时,将会向 error 处理程序传递在 promise 中捕获或传播的错误值。如果您正在 WinJS.Promise 中创建操作,则您也可以使用 WinJS.ErrorFromName 的实例。这只是一个 JavaScript 对象,它包含识别错误的 name 属性和包含更多信息的 message 属性。例如,当取消 promise 时,error 处理程序将会收到一个 name 和 message 名称均设置为“Canceled” 的错误对象。
但是,如果您无法从一开始就启动操作,情况会怎样?例如,如果您调用包含错误参数(如 0, 0)的 calculateIntegerSum,其甚至无法尝试启动计数,而是返回一个处于错误状态的 promise。这就是静态方法 WinJS.Promise.wrapError 的用途。它获取 WinJS.ErrorFromName 的实例,并且在此情况下将返回处于错误状态的 promise,而不是新的 WinJS.Promise 实例。
另一方面,尽管调用 promise 的 cancel 方法会使 promise 本身处于错误状态,但我们如何停止已经开始的异步操作呢?在之前的 calculateIntegerSum 实施中,将一直调用 setImmediate,直到操作完成为止,无论我们创建的 promise 的状态如何。事实上,如果操作在 promise 已取消之后调用 complete 调度程序,则 promise 将忽略操作完成。
因此,需要使用一种方法来让 promise 告知操作其不再需要继续工作。为此,WinJS.Promise 构造函数需要在取消 promise 的情况下调用的第二个函数参数。在我们的示例中,对该函数的调用需要避免接下来调用 setImmediate,从而停止计算的情况。下方显示了具体的情形和正确的处理方法:
function calculateIntegerSum(max, step) { //Return a promise in the error state for bad arguments if (max < 1 || step < 1) { var err = new WinJS.ErrorFromName("calculateIntegerSum", "max and step must be 1 or greater"); return WinJS.Promise.wrapError(err); } var _cancel = false; //The WinJS.Promise constructor's argument is an initializer function that receives //dispatchers for completed, error, and progress cases. return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) { var sum = 0; function iterate(args) { for (var i = args.start; i < args.end; i++) { sum += i; }; //If for some reason there was an error, create the error with WinJS.ErrorFromName //and pass to errorDispatch if (false /* replace with any necessary error check -- we don’t have any here */) { errorDispatch(new WinJS.ErrorFromName("calculateIntegerSum (scenario 7)", "error occurred")); } if (i >= max) { //Complete--dispatch results to completed handlers completeDispatch(sum); } else { //Dispatch intermediate results to progress handlers progressDispatch(sum); //Interrupt the operation if canceled if (!_cancel) { setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) }); } } } setImmediate(iterate, { start: 0, end: Math.min(step, max) }); }, //Cancellation function for the WinJS.Promise constructor function () { _cancel = true; }); }
总而言之,创建 WinJS.Promise 的实例有许多用途。例如,如果您有一个通过一些其他的异步方法与 Web 服务通信的库,则您可以将这些操作封装在 promise 中。您还可能还需要使用新的 promise 将来自不同源的多个异步操作(或其他 promise)合并到一个单一的 promise 中,以便控制所涉及的所有关系。在 WinJS.Promise 的 initializer 的代码中,毫无疑问您可以包含面向其他异步操作及其 promise 的您自有的处理程序。您可以使用这些处理程序封装针对网络超时等情况的自动重试机制、挂接到一般进程更新程序 UI,或者添加底层日志记录或分析。通过这些方式,您的其余代码无需了解详细信息,并且可以直接处理来自消费者一方的 promise。
除此之外,将 JavaScript worker 封装到 promise 中的过程也变得相当简单,这样一来其外观和行为均变得与 WinRT 中的其他异步操作相同。如您所知,worker 通过 postMessage 调用提供他们的结果,该调用将引发应用程序中 worker 对象的 message 事件。以下代码可将该事件与履行的 promise 相关联,无论该消息中提供何种结果:
// This is the function variable we're wiring up. var workerCompleteDispatch = null; var promiseJS = new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) { workerCompleteDispatch = completeDispatch; }); // Worker is created here and stored in the 'worker' variable // Listen for worker events worker.onmessage = function (e) { if (workerCompleteDispatch != null) { workerCompleteDispatch(e.data.results); /* event args depends on the worker */ } } promiseJS.done(function (result) { // Output for JS worker });
要扩展该代码以处理 worker 中的错误,您需要在其他变量中保存 error 调度程序,使用 message 事件处理程序检查其事件参数中的错误信息,并在适当的时候调用 error 调度程序,而不是 complete 调度程序。
加入并行的 promise
由于 promise 通常用来封装异步操作,您完全可以并行执行多个操作。此时,您可能想知道是在履行组中的一个 promise 还是在履行组中的所有 promise 时执行。静态函数 WinJS.Promise.any 和 WinJS.Promise.join 为此提供了解决方案。
这两种函数可接受一组值或一个包含值属性的对象。这些值可以是 promise 和任何使用 WinJS.Promise.as 封装,从而让整个数组或对象由 promise 组成的非 promise 值。
以下是 any 的特性:
- any 在履行一个其他的 promise 或在该 promise 遇到错误(逻辑“或”)而失败时,创建一个单一的 promise。本质上,any 将 completed 处理程序附加到所有 promise,而一旦调用 completed 处理程序,其将调用 any promise 本身收到的任何 completed 处理程序。
- 在履行 any promise 之后(也就是履行列表中的第一个 promise 之后),列表中的其他操作将继续运行,并调用向这些 promise 单独分配的任何 completed、error 或 progress 处理程序。
- 如果您从 any 中取消 promise,则列表中的所有 promise 都将被取消。
关于 join:
- join 在履行所有其他的 promise 或在这些 promise 遇到错误(逻辑“或”)而失败时,创建一个单一的 promise。本质上,join将 completed 和 error 处理程序附加到所有这些 promise,并等待所有处理程序调用完成,然后调用其本身收到的 completed 处理程序。
- join promise 还将向您提供的任何 progress 处理程序报告进度。此时,中间结果是来自到目前为止已履行的各个 promise 的一系列结果。
- 如果您从 join 中取消 promise,则它将取消仍然处于挂起状态的所有其他的 promise。
除 any 和 join 之外,您还需要了解一下可派上用场的其他两种静态 WinJS.Promise 方法:
- is 确定是否任意值是返回布尔值的 promise。它主要确保对象包含名为“then”的函数;但不会测试“done”。
- theneach将 completed、error 和 progress 处理程序应用于一组 promise(使用 then),并在 promise 中将结果返回为另一组值。任何处理程序都可以为 null。
包含顺序结果的并行 promise
借助 WinJS.Promise.join 和 WinJS.Promise.any,我们能够处理并行 promise,也就是处理并行异步操作。另外,join 返回的 promise 将在整个数组中的 promise 全部履行之后履行。但是,这些 promise 可能将以随机顺序完成。当您有一组可通过此方式执行的操作,但您想以精心定义的顺序(即按照它们出现在数组中的顺序)处理其结果时,该怎么办?
为此,您需要将每个后续 promise 加入到以前出现的所有 promise 的 join 中,本文开始处的那一小段代码可准确地完成此操作。下方再次显示了该代码,但经过了重新编写以使 promise 变得更加明确。(假定 list是一组某种类型的值,并作为参数用于假设的生成 promise 的异步调用 doOperationAsync):
list.reduce(function callback (prev, item, i) { var opPromise = doOperationAsync(item); var join = WinJS.Promise.join({ prev: prev, result: opPromise}); return join.then(function completed (v) { console.log(i + ", item: " + item+ ", " + v.result); }); })
要了解该代码,我们必须首先了解该数组的 reduce 方法的工作原理。对于数组中的每个项目,reduce 将调用函数参数(此处称为 callback),它将收到四个参数(本代码中只用到三个):
- prev 该值是之前对 callback 的调用的返回值(对于第一个项目,该值为 null)。
- item 数组中的当前值。
- i 列表中的项目索引。
- source 原始数组。
对于第一个项目,我们将获得我称之为 opPromise1 的 promise。由于 prev 是 null,我们将加入 [WinJS.Promise.as(null), opPromise1] 。但是,请注意我们将不会返回 join 本身。而是向该 join 附加一个 completed 处理程序(我已调用 completed),并从它的 then 返回 promise。
请记住,从 then 返回的 promise 将在 completed 处理程序返回时履行。这意味着我们从 callback 返回的 promise 将在第一个项目的 completed 处理程序已处理 opPromise1 的结果之后完成。如果您回顾 join 的结果,就会发现它是通过一个包含原始列表中 promise 的结果的对象实现的。这意味着履行值 v 将包含一个 prev 属性和一个 result 属性,其中后者是 opPromise1 的结果。
对于 list 中的下一个项目,callback 将收到包含上一个 join.then 中的 promise 的 prev。接下来,我们创建 opPromise1.then 和 opPromise2 的新的 join。因此,该 join 将在已经履行 opPromise2,并且 opPromise1 的 completed 处理程序已返回之后完成。就是这样!我们向该 join 附加的 completed2 处理程序将不会被调用,直至 completed1 已返回。
这种相同的依赖关系将针对 list 中的每个项目而构建 — 项目 n 的 join.then 中的 promise 将不会被履行,直至 completedn 返回。这样可以保证 completed 处理程序以与 list 相同的顺序调用。
结束语
在本博文中,我们已了解到 promise 本身只是一个代码构造或一个调用约定(尽管其很强大),它代表发货人和消费者之间的特定关系,发货人将在以后的任意时间点提供值,而消费者想知道他们何时可以获得这些值。因而,promise 非常适用于代表异步操作的结果,并可广泛用于使用 JavaScript 编写的 Windows 应用商店应用。promise 的规范还允许将顺序异步操作链接在一起,使每个中间结果从一个链接流向下一个。
Windows JavaScript 库 (WinJS) 提供了可靠的 promise 实现,您可以使用它来封装您的任何类型的操作。另外,它还为常见的应用场景(例如,拼接并行操作的 promise)提供了 helper。借助这些,WinJS 可允许使用更加有效和高效的方式处理异步操作。
Kraig Brockschmidt
Windows 生态系统团队项目经理
使用 HTML、CSS 和 JavaScript 编程 Windows 8 应用作者
Comments
- Anonymous
September 28, 2014
我也写了篇,www.cnblogs.com/.../promise.html