首頁  >  文章  >  web前端  >  非同步JavaScript程式設計中的Promise使用方法_node.js?1.1.5

非同步JavaScript程式設計中的Promise使用方法_node.js?1.1.5

PHP中文网
PHP中文网原創
2016-05-16 15:48:361115瀏覽


非同步?

我在很多地方都看過異步(Asynchronous)這個詞,但在我還不是很理解這個概念的時候,卻發現自己常常會被當做「已經很清楚」(*  ̄? ̄)。

如果你也有類似的情況,沒關係,搜尋一下這個詞,就可以得到大致的說明。在這裡,我會對JavaScript的非同步做一點額外解釋。

看一下這段程式碼:

var start = new Date();
setTimeout(function(){
  var end = new Date();
  console.log("Time elapsed: ", end - start, "ms");
}, 500);
while (new Date - start < 1000) {};

這段程式碼運作後會得到類似Time elapsed: 1013ms這樣的結果。 setTimeout()所設定的在未來500ms時執行的函數,實際上等了比1000ms更多的時間後才執行。

要如何解釋呢?當呼叫setTimeout()時,一個延時事件被排入佇列。然後,繼續執行這之後的程式碼,以及更後邊的程式碼,直到沒有任何程式碼。沒有任何程式碼後,JavaScript執行緒進入空閒,此時JavaScript執行引擎才去翻看佇列,在佇列中找到「應該觸發」的事件,然後呼叫這個事件的處理器(函數)。處理器執行完成後,再返回佇列,然後查看下一個事件。

單執行緒的JavaScript,就是這樣透過佇列,以事件循環的形式工作的。所以,前面的程式碼中,是用while將執行引擎拖在程式碼運行期間長達1000ms,而在全部程式碼運行完回到佇列前,任何事件都不會觸發。這就是JavaScript的非同步機制。
JavaScript的非同步難題

JavaScript中的非同步操作可能不總是簡單易行的。

Ajax也許是我們用得最多的非同步操作。以jQuery為例,啟動一個Ajax請求的程式碼一般是這樣的:

// Ajax请求示意代码
$.ajax({
  url: url,
  data: dataObject,
  success: function(){},
  error: function(){}
});

這樣的寫法有什麼問題嗎?簡單來說,不夠輕便。為什麼一定要在發起請求的地方,就要把success和error這些回呼給寫好呢?假如我的回調要做很多很多的事情,是要我想起一件事情就跑回這裡加碼嗎?

再比如,我們要完成這樣一件事:有4個供Ajax訪問的url地址,需要先Ajax訪問第1個,在第1個訪問完成後,用拿到的返回數據作為參數再訪問第2個,第2個訪問完成後再第3個...以此到4個全部訪問完成。依照這樣的寫法,似乎會變成這樣:

$.ajax({
  url: url1,
  success: function(data){
    $.ajax({
      url: url2,
      data: data,
      success: function(data){
        $.ajax({
          //...
        });
      }  
    });
  }
})

你一定會覺得這種稱為Pyramid of Doom(金字塔厄運)的程式碼看起來很糟。習慣了直接附加回呼的寫法,就可能會對這種一個傳遞到下一個的非同步事件感到無從入手。為這些回調函數分別命名並分離存放可以在形式上減少嵌套,使程式碼清晰,但仍然無法解決問題。

另一個常見的困難是,同時發送兩個Ajax請求,然後要在兩個請求都成功返回後再做一件接下來的事,想一想如果只按前面的方式在各自的呼叫位置去附加回調,這是不是好像也有點難辦?

適於應付這些非同步操作,可以讓你寫出更優雅程式碼的就是Promise。
Promise上場

Promise是什麼呢?先繼續以前面jQuery的Ajax請求示意程式碼為例,那段程式碼其實可以寫成這個樣子:

var promise = $.ajax({
  url: url,
  data: dataObject
});
promise.done(function(){});
promise.fail(function(){});

這和前面的Ajax請求示意代碼是等效的。可以看到,Promise的加入使得程式碼形式改變了。 Ajax請求就好像變數賦值一樣,被「保存」了起來。這就是封裝,封裝將真正意義上讓非同步事件變得容易起來。
封裝是有用的

Promise物件就像是一個封裝好的對非同步事件的引用。想要在這個非同步事件完成後做點事情?給它附加回呼就可以了,不管附加多少個也沒問題!

jQuery的Ajax方法會傳回一個Promise物件(這是jQuery1.5重點增加的特性)。如果我有do1()、do2()兩個函數要在非同步事件成功完成後執行,只需要這樣做:

promise.done(do1);
// Other code here.
promise.done(do2);

這樣可要自由多了,我只要保存這個Promise對象,就在寫代碼的任何時候,給它附加任意數量的回調,而不用管這個非同步事件是在哪裡發起的。這就是Promise的優勢。
正式的介紹

Promise應對非同步操作是如此有用,以至於發展為了CommonJS的一個規範,叫做Promises/A。 Promise代表的是某一作業結束後的回傳值,它有3種狀態:

  1.     肯定(fulfilled或resolved),顯示該Promise的操作成功了。

  2.     否定(rejected或failed),表示該Promise的操作失敗了。

  3.     等待(pending),還沒有得到肯定或否定的結果,進行中。

此外,還有1種名義上的狀態用來表示Promise的操作已經成功或失敗,也就是肯定和否定狀態的集合,叫做結束(settled)。 Promise還具有以下重要的特性:

  •     一个Promise只能从等待状态转变为肯定或否定状态一次,一旦转变为肯定或否定状态,就再也不会改变状态。

  •     如果在一个Promise结束(成功或失败,同前面的说明)后,添加针对成功或失败的回调,则回调函数会立即执行。

想想Ajax操作,发起一个请求后,等待着,然后成功收到返回或出现错误(失败)。这是否和Promise相当一致?

进一步解释Promise的特性还有一个很好的例子:jQuery的$(document).ready(onReady)。其中onReady回调函数会在DOM就绪后执行,但有趣的是,如果在执行到这句代码之前,DOM就已经就绪了,那么onReady会立即执行,没有任何延迟(也就是说,是同步的)。
Promise示例
生成Promise

Promises/A里列出了一系列实现了Promise的JavaScript库,jQuery也在其中。下面是用jQuery生成Promise的代码:

var deferred = $.Deferred();
deferred.done(function(message){console.log("Done: " + message)});
deferred.resolve("morin"); // Done: morin

jQuery自己特意定义了名为Deferred的类,它实际上就是Promise。$.Deferred()方法会返回一个新生成的Promise实例。一方面,使用deferred.done()、deferred.fail()等为它附加回调,另一方面,调用deferred.resolve()或deferred.reject()来肯定或否定这个Promise,且可以向回调传递任意数据。
合并Promise

还记得我前文说的同时发送2个Ajax请求的难题吗?继续以jQuery为例,Promise将可以这样解决它:

var promise1 = $.ajax(url1),
promise2 = $.ajax(url2),
promiseCombined = $.when(promise1, promise2);
promiseCombined.done(onDone);

$.when()方法可以合并多个Promise得到一个新的Promise,相当于在原多个Promise之间建立了AND(逻辑与)的关系,如果所有组成Promise都已成功,则令合并后的Promise也成功,如果有任意一个组成Promise失败,则立即令合并后的Promise失败。
级联Promise

再继续我前文的依次执行一系列异步任务的问题。它将用到Promise最为重要的.then()方法(在Promises/A规范中,也是用“有then()方法的对象”来定义Promise的)。代码如下:

var promise = $.ajax(url1);
promise = promise.then(function(data){
  return $.ajax(url2, data);
});
promise = promise.then(function(data){
  return $.ajax(url3, data);
});
// ...

Promise的.then()方法的完整形式是.then(onDone, onFail, onProgress),这样看上去,它像是一个一次性就可以把各种回调都附加上去的简便方法(.done()、.fail()可以不用了)。没错,你的确可以这样使用,这是等效的。

但.then()方法还有它更为有用的功能。如同then这个单词本身的意义那样,它用来清晰地指明异步事件的前后关系:“先这个,然后(then)再那个”。这称为Promise的级联。

要级联Promise,需要注意的是,在传递给then()的回调函数中,一定要返回你想要的代表下一步任务的Promise(如上面代码的$.ajax(url2, data))。这样,前面被赋值的那个变量才会变成新的Promise。而如果then()的回调函数返回的不是Promise,则then()方法会返回最初的那个Promise。

应该会觉得有些难理解?从代码执行的角度上说,上面这段带有多个then()的代码其实还是被JavaScript引擎运行一遍就结束。但它就像是写好的舞台剧的剧本一样,读过一遍后,JavaScript引擎就会在未来的时刻,依次安排演员按照剧本来演出,而演出都是异步的。then()方法就是让你能写出异步剧本的笔。
将Promise用在基于回调函数的API

前文反复用到的$.ajax()方法会返回一个Promise对象,这其实只是jQuery特意提供的福利。实际情况是,大多数JavaScript API,包括Node.js中的原生函数,都基于回调函数,而不是基于Promise。这种情况下使用Promise会需要自行做一些加工。

这个加工其实比较简单和直接,下面是例子:

var deferred = $.Deferred();
setTimeout(deferred.resolve, 1000);
deferred.done(onDone);

这样,将Promise的肯定或否定的触发器,作为API的回调传入,就变成了Promise的处理模式了。
Promise是怎么实现出来的?

本文写Promise写到这里,你发现了全都是基于已有的实现了Promise的库。那么,如果要自行构筑一个Promise的话呢?

位列于Promises/A的库列表第一位的Q可以算是最符合Promises/A规范且相当直观的实现。如果你想了解如何做出一个Promise,可以参考Q提供的设计模式解析。

限于篇幅,本文只介绍Promise的应用。我会在以后单独开一篇文章来详述Promise的实现细节。

作为JavaScript后续版本的ECMAScript 6将原生提供Promise,如果你想知道它的用法,推荐阅读JavaScript Promises: There and back again。
结语

Promise這個字頑強到不適合翻譯,一眼之下都會覺得意義不明。不過,在JavaScript裡做比較複雜的非同步任務時,它的確可以提供相當多的幫助。

 以上是非同步JavaScript程式設計中的Promise使用方法_node.js?1.1.5的內容,更多相關內容請關注PHP中文網(www.php.cn)!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn