理解JavaScript中Promise的使用
Javascript 采用回調(diào)函數(shù)(callback)來處理異步編程。從同步編程到異步回調(diào)編程有一個適應的過程,但是如果出現(xiàn)多層回調(diào)嵌套,也就是我們常說的厄運的回調(diào)金字塔(Pyramid of Doom),絕對是一種糟糕的編程體驗。于是便有了 CommonJS 的 Promises/A 規(guī)范,用于解決回調(diào)金字塔問題。本文先介紹 Promises 相關(guān)規(guī)范,然后再通過解讀一個迷你的 Promises 以加深理解。
什么是 Promise
一個 Promise 對象代表一個目前還不可用,但是在未來的某個時間點可以被解析的值。它允許你以一種同步的方式編寫異步代碼。例如,如果你想要使用 Promise API 異步調(diào)用一個遠程的服務器,你需要創(chuàng)建一個代表數(shù)據(jù)將會在未來由 Web 服務返回的 Promise 對象。唯一的問題是目前數(shù)據(jù)還不可用。當請求完成并從服務器返回時數(shù)據(jù)將變?yōu)榭捎脭?shù)據(jù)。在此期間,Promise 對象將扮演一個真實數(shù)據(jù)的代理角色。接下來,你可以在 Promise 對象上綁定一個回調(diào)函數(shù),一旦真實數(shù)據(jù)變得可用這個回調(diào)函數(shù)將會被調(diào)用。
Promise 對象曾經(jīng)以多種形式存在于許多語言中。
去除厄運的回調(diào)金字塔(Pyramid of Doom)
Javascript 中最常見的反模式做法是回調(diào)內(nèi)部再嵌套回調(diào)。
// 回調(diào)金字塔 asyncOperation(function(data){ // 處理 `data` anotherAsync(function(data2){ // 處理 `data2` yetAnotherAsync(function(){ // 完成 }); }); });
引入 Promises 之后的代碼
promiseSomething() .then(function(data){ // 處理 `data` return anotherAsync(); }) .then(function(data2){ // 處理 `data2` return yetAnotherAsync(); }) .then(function(){ // 完成 });
Promises 將嵌套的 callback,改造成一系列的.then的連綴調(diào)用,去除了層層縮進的糟糕代碼風格。Promises 不是一種解決具體問題的算法,而已一種更好的代碼組織模式。接受新的組織模式同時,也逐漸以全新的視角來理解異步調(diào)用。
各個語言平臺都有相應的 Promise 實現(xiàn)
- Java's java.util.concurrent.Future
- Python's Twisted deferreds and PEP-3148 futures
- F#'s Async
- .Net's Task
- C++ 11's std::future
- Dart's Future
- Javascript's Promises/A/B/D/A+
下面我來相信了解一下 javascript 語言環(huán)境下各個規(guī)范的一些細節(jié)。
Promises/A 規(guī)范
promise 表示一個最終值,該值由一個操作完成時返回。
- promise 有三種狀態(tài):**未完成** (unfulfilled),**完成** (fulfilled) 和**失敗** (failed)。
- promise 的狀態(tài)只能由**未完成**轉(zhuǎn)換成完成,或者**未完成**轉(zhuǎn)換成**失敗** 。
- promise 的狀態(tài)轉(zhuǎn)換只發(fā)生一次。
promise 有一個 then 方法,then 方法可以接受 3 個函數(shù)作為參數(shù)。前兩個函數(shù)對應 promise 的兩種狀態(tài) fulfilled 和 rejected 的回調(diào)函數(shù)。第三個函數(shù)用于處理進度信息(對進度回調(diào)的支持是可選的)。
promiseSomething().then(function(fulfilled){ //當promise狀態(tài)變成fulfilled時,調(diào)用此函數(shù) },function(rejected){ //當promise狀態(tài)變成rejected時,調(diào)用此函數(shù) },function(progress){ //當返回進度信息時,調(diào)用此函數(shù) });
如果 promise 支持如下連個附加方法,稱之為可交互的 promise
- get(propertyName)
獲得當前 promise 最終值上的一個屬性,返回值是一個新的 promise。
- call(functionName, arg1, arg2, ...)
調(diào)用當然 promise 最終值上的一個方法,返回值也是一個新的promise。
Promises/B 規(guī)范
在 Promises/A 的基礎上,Promises/B 定義了一組 promise 模塊需要實現(xiàn)的 API
when(value, callback, errback_opt)
如果 value 不是一個 promise ,那么下一事件循環(huán)callback會被調(diào)用,value 作為 callback 的傳入值。如果 value 是一個 promise,promise 的狀態(tài)已經(jīng)完成或者變成完成時,那么下一事件循環(huán) callback 會被調(diào)用,resolve 的值會被傳入 callback;promise 的狀態(tài)已經(jīng)失敗或者變成失敗時,那么下一事件循環(huán) errback 會被調(diào)用,reason 會作為失敗的理由傳入 errback。
asap(value, callback, errback_opt)
與 when 最大的區(qū)別,如果 value 不是一個 promise,會被立即執(zhí)行,不會等到下一事件循環(huán)。
enqueue(task Function)
盡可能快地在接下來的事件循環(huán)調(diào)用 task 方法。
get(object, name)
返回一個獲得對象屬性的 promise。
post(object, name, args)
返回一個調(diào)用對象方法的 promise。
put(object, name, value)
返回一個修改對象屬性的 promise。
del(object, name)
返回一個刪除對象屬性的 promise。
makePromise(descriptor Object, fallback Function)
返回一個 promise 對象,該對象必須是一個可調(diào)用的函數(shù),也可能是可被實例化的構(gòu)造函數(shù)。
- 第一個參數(shù)接受一個描述對象,該對象結(jié)構(gòu)如下,
{ "when": function(errback){...}, "get": function(name){...}, "put": function(name, value){...}, "post": function(name, args){...}, "del": function(name){...}, }
上面每一個注冊的 handle 都返回一個 resolved value或者 promise。
- 第二個參數(shù)接受一個 fallback(message,...args) 函數(shù),當沒有 promise 對象沒有找到對應的 handle 時該函數(shù)會被觸發(fā),返回一個 resolved value 或者 promise。
defer()
返回一個對象,該對象包含一個 resolve(value) 方法和一個 promise 屬性。
當 resolve(value) 方法被第一次調(diào)用時,promise 屬性的狀態(tài)變成 完成,所有之前或之后觀察該 promise 的 promise 的狀態(tài)都被轉(zhuǎn)變成 完成。value 參數(shù)如果不是一個 promise ,會被包裝成一個 promise 的 ref。resolve 方法會忽略之后的所有調(diào)用。
reject(reason String)
返回一個被標記為 失敗 的 promise。
一個失敗的 promise 上被調(diào)用 when(message) 方法時,會采用如下兩種方法之一
1. 如果存在 errback,errback 會以 reason 作為參數(shù)被調(diào)用。when方法會將 errback 的返回值返回。
2. 如果不存在 errback,when 方法返回一個新的 reject 狀態(tài)的promise 對象,以同一 reason 作為參數(shù)。
ref(value)
如果 value 是 promise 對象,返回 value 本身。否則,返回一個resolved 的 promise,攜帶如下 handle。
1. when(errback),忽略 errback,返回 resolved 值
2. get(name),返回 resolved 值的對應屬性。
3. put(name, value) ,設置 resolved 值的對應屬性。
4. del(name),刪除 resolved 值的對應屬性。
5. post(name, args), 調(diào)用 resolved 值的對應方法。
6. 其他所有的調(diào)用都返回一個 reject,并攜帶 "Promise does not handle NAME" 的理由。
isPromise(value) Boolean
判斷一個對象是否是 promise
method(name String)
獲得一個返回 name 對應方法的 promise。返回值是 "get", "put", "del" 和 "post" 對應的方法,但是會在下一事件循環(huán)返回。
Promises/D 規(guī)范
為了增加不同 promise 實現(xiàn)之間的可互操作性,Promises/D 規(guī)范對promise 對象和 Promises/B 規(guī)范做了進一步的約定。以達到鴨子類型的效果(Duck-type Promise)。
簡單來說Promises/D 規(guī)范,做了兩件事情,
1、如何判斷一個對象是 Promise 類型。
2、對 Promises/B 規(guī)范進行細節(jié)補充。
甄別一個 Promise 對象
Promise 對象必須是實現(xiàn) promiseSend 方法。
1. 在 promise 庫上下文中,如果對象包含 promiseSend 方法就可以甄別為promise 對象
2. promiseSend 方法必須接受一個操作名稱,作為第一個參數(shù)
3. 操作名稱是一個可擴展的集合,下面是一些保留名稱
1. when,此時第三個參數(shù)必須是 rejection 回調(diào)。
1. rejection回調(diào)必須接受一個 rejection 原因(可以是任何值)作為第一個參數(shù)
2. get,此時第三個參數(shù)為屬性名(字符串類型)
3. put,此時第三個參數(shù)為屬性名(字符串類型),第四個參數(shù)為新屬性值。
4. del,此時第三個參數(shù)為屬性名
5. post,此時第三個參數(shù)為方法的屬性名,接下來的變參為方法的調(diào)用參數(shù)
6. isDef
4. promiseSend方法的第二個參數(shù)為 resolver 方法
5. promiseSend方法可能接受變參
6. promiseSend方法必須返回undefined
對 Promises/B 規(guī)范的補充
Promises/D 規(guī)范中對 Promises/B 規(guī)范中定義的ref、reject、def、defer方法做了進一步細致的約束,此處略去這些細節(jié)。
Promises/A+ 規(guī)范
前面提到的 Promises/A/B/D 規(guī)范都是有CommonJS組織提出的,Promises/A+是有一個自稱為Promises/A+ 組織發(fā)布的,該規(guī)范是以Promises/A作為基礎進行補充和修訂,旨在提高promise實現(xiàn)之間的可互操作性。
Promises/A+ 對.then方法進行細致的補充,定義了細致的Promise Resolution Procedure流程,并且將.then方法作為promise的對象甄別方法。
此外,Promises/A+ 還提供了兼容性測試工具,以確定各個實現(xiàn)的兼容性。
實現(xiàn)一個迷你版本的Promise
上面扯了這么多規(guī)范,現(xiàn)在我們看看如何實現(xiàn)一個簡單而短小的Promise。
1、狀態(tài)機
var PENDING = 0; var FULFILLED = 1; var REJECTED = 2; function Promise() { // store state which can be PENDING, FULFILLED or REJECTED var state = PENDING; // store value or error once FULFILLED or REJECTED var value = null; // store sucess & failure handlers attached by calling .then or .done var handlers = []; }
2、狀態(tài)變遷
僅支持兩種狀態(tài)變遷,fulfill和reject
// ... function Promise() { // ... function fulfill(result) { state = FULFILLED; value = result; } function reject(error) { state = REJECTED; value = error; } }
fulfill和reject方法較為底層,通常更高級的resolve方法開放給外部。
// ... function Promise() { // ... function resolve(result) { try { var then = getThen(result); if (then) { doResolve(then.bind(result), resolve, reject) return } fulfill(result); } catch (e) { reject(e); } } }
resolve方法可以接受一個普通值或者另一個promise作為參數(shù),如果接受一個promise作為參數(shù),等待其完成。promise不允許被另一個promise fulfill,所以需要開放resolve方法。resolve方法依賴一些幫助方法定義如下:
/** * Check if a value is a Promise and, if it is, * return the `then` method of that promise. * * @param {Promise|Any} value * @return {Function|Null} */ function getThen(value) { var t = typeof value; if (value && (t === 'object' || t === 'function')) { var then = value.then; if (typeof then === 'function') { return then; } } return null; } /** * Take a potentially misbehaving resolver function and make sure * onFulfilled and onRejected are only called once. * * Makes no guarantees about asynchrony. * * @param {Function} fn A resolver function that may not be trusted * @param {Function} onFulfilled * @param {Function} onRejected */ function doResolve(fn, onFulfilled, onRejected) { var done = false; try { fn(function (value) { if (done) return done = true onFulfilled(value) }, function (reason) { if (done) return done = true onRejected(reason) }) } catch (ex) { if (done) return done = true onRejected(ex) } }
這里resolve和doResolve之間的遞歸很巧妙,用來處理promise的層層嵌套(promise的value是一個promise)。
構(gòu)造器
// ... function Promise(fn) { // ... doResolve(fn, resolve, reject); }
.done方法
// ... function Promise(fn) { // ... function handle(handler) { if (state === PENDING) { handlers.push(handler); } else { if (state === FULFILLED && typeof handler.onFulfilled === 'function') { handler.onFulfilled(value); } if (state === REJECTED && typeof handler.onRejected === 'function') { handler.onRejected(value); } } } this.done = function (onFulfilled, onRejected) { // ensure we are always asynchronous setTimeout(function () { handle({ onFulfilled: onFulfilled, onRejected: onRejected }); }, 0); } // ... }
.then方法
// ... function Promise(fn) { // ... this.then = function (onFulfilled, onRejected) { var self = this; return new Promise(function (resolve, reject) { return self.done(function (result) { if (typeof onFulfilled === 'function') { try { return resolve(onFulfilled(result)); } catch (ex) { return reject(ex); } } else { return resolve(result); } }, function (error) { if (typeof onRejected === 'function') { try { return resolve(onRejected(error)); } catch (ex) { return reject(ex); } } else { return reject(error); } }); }); } // ... }
$.promise
jQuery 1.8 之前的版本,jQuery的 then 方法只是一種可以同時調(diào)用 done 、fail 和 progress 這三種回調(diào)的速寫方法,而 Promises/A 規(guī)范的 then 在行為上更像是 jQuery 的 pipe。 jQuery 1.8 修正了這個問題,使 then 成為 pipe 的同義詞。不過,由于向后兼容的問題,jQuery 的 Promise 再如何對 Promises/A 示好也不太會招人待見。
此外,在 Promises/A 規(guī)范中,由 then 方法生成的 Promise 對象是已執(zhí)行還是已拒絕,取決于由 then 方法調(diào)用的那個回調(diào)是返回值還是拋出錯誤。在 JQuery 的 Promise 對象的回調(diào)中拋出錯誤是個糟糕的主意,因為錯誤不會被捕獲。
小結(jié)
最后一個例子揭示了,實現(xiàn) Promise 的關(guān)鍵是實現(xiàn)好 doResolve 方法,在完事以后觸發(fā)回調(diào)。而為了保證異步 setTimeout(fun, 0); 是關(guān)鍵一步。
Promise 一直用得蠻順手的,其很好的優(yōu)化了 NodeJS 異步處理時的代碼結(jié)構(gòu)。但是對于其工作原理卻有些懵懂和好奇,于是花了些精力查閱并翻譯了Promise 的規(guī)范,以充分的理解 Promise 的細節(jié)。
以上就是關(guān)于JavaScript中Promise的使用方法介紹,希望對大家的學習有所幫助。
相關(guān)文章
Vue3基于countUp.js實現(xiàn)數(shù)字滾動的插件
本文主要介紹了Vue3基于countUp.js實現(xiàn)數(shù)字滾動的插件,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04JavaScript使用Max函數(shù)返回兩個數(shù)字中較大數(shù)的方法
這篇文章主要介紹了JavaScript使用Max函數(shù)返回兩個數(shù)字中較大數(shù)的方法,涉及javascript中Max函數(shù)的使用技巧,非常具有實用價值,需要的朋友可以參考下2015-04-04