摘要:參數如前面所提到的,方法只是方法的一個語法糖,原因就在于方法的參數為實際上是兩個回調函數,分別用于處理調用它的對象的和狀態,而方法就等價于狀態處理函數。對象狀態傳遞和改變的方法利用回調的返回值,可以控制某個操作后方法返回的對象及其狀態。
一分鐘快速入門注意,本文主要針對ES6標準實現的Promise語法進行闡述,實例代碼也都使用ES6語法,快速入門ES6請參見ECMAScript 6 掃盲。
被回調地獄整怕了?快試Promise吧!。Promise的核心思想其實很簡單,就是將異步操作結果處理交給Promise對象的方法注冊,然后等到異步操作完了再去取用這些處理操作。至于取用哪個處理操作,就得看Promise對象狀態了。Promise對象一共有三種狀態:Pending(初始狀態)、Fulfilled(異步操作成功)、Rejected(異步操作失敗)。而三者間的轉換只有兩種情況:Pending—>Fulfilled、Pending—>Rejected;詳見下圖:
了解了狀態及其轉換后,我們就可以來使用Promise對象了:
let promise = new Promise((resolve, reject)=> { // 異步操作 // 異步操作成功時調用 resolve(value) // 異步操作失敗時調用 reject(error) });
上述代碼中傳給Promise構造函數的兩個函數resolve, reject,分別用于觸發Promise對象的Fullfilled和Rejected狀態。當處于Fullfilled狀態時Promise會調用then方法,而處于Rejected狀態時則會調用catch方法,這兩個方法都會返回Promise對象,所以我們可以采用鏈式寫法:
promise.then((value)=> {...}) .catch((error)=> {...});
上面的方法鏈中,then方法里注冊了Fullfilled狀態的處理函數、catch方法則注冊了Rejected狀態的處理函數。這種簡單明了的寫法把異步操作的結果處理函數分離了出來,如果這些處理本身又是異步操作,那我們自然也就把層層異步回調也從回調地獄中剝離了,代碼瞬間清爽有木有!
深入Promise調用鏈前面我們只是將一層處理操作分離到then方法中(其中catch方法只是then方法的一個語法糖,后面會再作講解);但在實際應用中多個異步操作往往會以串行或并行的方式連續出現,比如下面這個預定房間的流程:
其中數據校驗、向API發送請求、往數據庫插入數據都是異步操作,一種用回調的寫法大概長這樣:
validate(data, (err)=> { if (err) return errorHandler(err); request(apiUrl, (err, apiResponse)=> { if (err) return errorHandler(err); if (apiResponse.isSuccessful) insertToDB(data, (err)=> { if (err) return errorHandler(err); successHandler(); }); else errorHandler(new Error("API error")); }); });
根據前面我們了解的Promise用法,我們已經能將validate這個異步操作寫成Promise形式了:
let promiseValidate = new Promise((resolve, reject)=> { validate(data, (err)=> { if (err) return reject(err); resolve(); }); }); promiseValidate(data) .then(()=> { request(apiUrl, (err, apiResponse)=> { if (err) return errorHandler(err); if (apiResponse.isSuccessful) insertToDB(data, (err)=> { if (err) return errorHandler(err); successHandler(); }); else errorHandler(new Error("API error")); }); }) .catch((err)=> errorHandler(err));
但要改就改到底,上面這種Promise和回調寫法混合得就不倫不類,除了仍存在回調嵌套的問題,多次出現的錯誤判斷和處理也有點違反DRY。所以接下來我們會深入研究下Promise調用鏈的行為,重點探討then方法里注冊的回調對調用鏈上數據傳遞和Promise對象狀態變化的影響,以及如何在調用鏈上對錯誤進行統一的處理。
Promise.resolve和Promise.reject我們先來看下一種“快速”生成Promise對象的方法:直接調用Promise.resolve(value)或Promise.reject(err)。這種方法和new一個Promise對象的區別在于,Promise對象在生成的時候狀態就已經確定,要么是Fullfilled(使用Promise.resolve())、要么是Rejected(使用Promise.reject()),不會和new實例化一樣等要異步操作完了再發生變化。
此外,如果傳給Promise.resolve方法的是一個具有then方法的對象(即所謂的Thenable對象),比如jQuery的$.ajax(),那么返回的Promise對象,后續調用的then就是原對象then方法的同一形式(參見下面的代碼)。簡單來講,就是Promise.resolve會將Thenable對象轉為ES6的Promise對象,這一特性常被用來將Promise的不同實現轉換為ES6實現。
$.ajax("https://httpbin.org/ip").then((value)=> { /* 輸出223.65.191.59 */ console.log(value.origin) }); Promise.resolve($.ajax("https://httpbin.org/ip")) .then((value)=> { /* 輸出223.65.191.59 */ console.log(value.origin) });詳解Promise.prototype.then
有了前面知識的鋪墊,我們終于可以來詳細講一下Promise對象的then方法了。
參數如前面所提到的,catch方法只是then方法的一個語法糖,
原因就在于then方法的參數為實際上是“兩個”回調函數,分別用于處理調用它的Promise對象的Fullfilled和Rejected狀態,而catch方法就等價于then(undefined, Rejected狀態處理函數)。
關于這兩個回調函數,首先要注意它們是異步調用的:
var v = 1; /* 輸出result: 2 */ Promise.resolve().then(()=> {console.log("result: " + v)}); /* 輸出result: 2 */ Promise.reject().then(undefined, ()=> {console.log("result: " + v)}); v++;
而兩個回調函數的參數,則是通過調用then方法的Promise對象指定的:
new Promise()產生的Promise對象,會分別用內部resolve()、reject()函數的參數
Promise.resolve()或Promise.reject()產生的Promise對象,則分別用Promise.resolve()、Promise.reject()的參數
而兩個回調函數的返回值,會用Promise.resolve(第一個回調返回值)或Promise.reject(第二個回調返回值)的形式作包裝,用來“替換”then方法返回的Promise對象。結合上面提到的then回調函數參數指定方式,回調返回值會這樣影響下一個then的回調函數:
返回的是普通數據,會傳給下一級調用的then方法作為回調函數的參數
返回的是Promise對象或Thenable對象,會被拿來“替換”then方法返回的Promise對象,具體then的回調函數怎么調用和傳參就得看其內部實現了
返回值一個新的Promise對象,狀態看執行哪個回調函數決定。注意這是一個新對象,不是簡單把調用then的Promise對象拿來改裝后返回:
var aPromise = new Promise((resolve)=> resolve(100)); var thenPromise = aPromise.then((value)=> console.log(value)); var catchPromise = thenPromise.catch((error)=> console.error(error)); /* true */ console.log(aPromise !== thenPromise); /* true */ console.log(thenPromise !== catchPromise);鏈式調用
知道了then方法的具體細節后,我們就能明白Promise調用鏈上:
傳遞數據的方法:利用上面提到的then回調的參數傳遞形式——不論是在Promise對象產生過程中直接傳遞、還是在then回調返回值中間接傳遞——就能實現將每一級異步操作的結果傳遞給后續then中注冊的處理函數處理。
Promise對象狀態傳遞和改變的方法:利用then回調的返回值,可以控制某個操作后then方法返回的Promise對象及其狀態。
現在我們把所有異步操作改為Promise語法,再利用在Promise調用鏈傳遞數據和控制狀態的方法,就能把本節開始提到的預定房間操作中的回調嵌套都展開來了:
let promiseValidate = new Promise((resolve, reject)=> { validate(data, (err)=> { if (err) return reject(err); resolve(); }); }); let promiseRequest = new Promise((resolve, reject)=> { request(data, (err, apiResponse)=> { if (err) return reject(err); // 在Promise對象產生過程中直接傳遞異步操作的結果 resolve(apiResponse); }); } ); let promiseInsertToDB = new Promise((resolve, reject)=> { insertToDB(data, (err)=> { if (err) return reject(err); resolve(); }); } ); promiseValidate(data) .then(()=> promiseRequest(apiUrl)) .then((apiResponse)=> { // 控制then回調的返回值,來改變then方法返回的新Promise對象的狀態 if (apiResponse.isSuccessful) return insertToDB(data); else errorHandler(new Error("API error")); }) .then(()=> successHandler()) .catch((err)=> return errorHandler(err));
上面的代碼不僅將嵌套的代碼展開,讓我們掙脫了“回調地獄”;而且可以對異步操作的錯誤直接利用統一的Promise錯誤處理方法,避免寫一堆重復的代碼。如果要進一步DRY,可以抽象出一個將典型的Node.js回調接口封裝為Promise接口的函數:
/* 處理形如 receiver.fn(...args, (err, res)=> {}) 的接口 */ let promisify = (fn, receiver) => { return (...args) => { // 返回重新封裝的Promise接口 return new Promise((resolve, reject) => { fn.apply(receiver, [...args, (err, res) => { // 重新綁定this return err ? reject(err) : resolve(res); }]); }); }; }; /* 用例 */ let promiseValidate = promisify(validate, global); let promiseRequest = promisify(request, global); let promiseInsertToDB = promisify(insertToDB, global);
Promise調用鏈上的錯誤處理注意,由于resolve和reject方法只能接收一個參數,所上面這個函數處理的回調里只能有err和一個數據參數。
在Promise調用鏈上的處理錯誤的思路,就是去觸發Promise對象的Rejected狀態,利用狀態的傳遞特性實現對錯誤的捕獲,再在catch或then回調里處理這些錯誤。下面我們就來進行相關的探討:
錯誤的捕獲首先我們有必要詳細了解下Promise對象的Rejected狀態的產生和傳遞過程。
Rejected狀態的產生有兩種情況:
調用了reject函數:Promise對象實例化的回調調用了reject(),或者直接調用了Promise.reject()
通過throw拋出錯誤
而只要產生了Rejected狀態,就會在調用鏈上持續傳遞,直到遇見Rejected狀態的處理回調(catch的回調或then的第二個回調)。再結合之前提到的Promise調用鏈上的數據傳遞方法,錯誤就能在調用鏈上作為參數被相應的回調“捕獲”了。這個過程可以參見下圖:
這里要注意,通過throw拋出錯時,如果錯誤是在setTimeout等的回調中拋出,是不會讓Promise對象產生Rejected狀態的,這也以為著Promise調用鏈上捕獲不了這個錯誤。舉個例子,下面這段代碼就不會有任何輸出:
Promise.resolve() .then(()=> setTimeout(100, ()=> {throw new Error("hi")})) .catch((err)=> console.log(err));
究其原因,是因為setTimeout的異步操作和Promise的異步操作不屬于同一種任務隊列,setTimeout回調里的錯誤會直接拋到全局變成Uncaught Error,而不會作用到Promise對象及其調用鏈上。這就也意味著,想要保證在調用鏈上產生的錯誤能被捕獲,就必須始終使用調用reject函數的方式來產生和傳遞錯誤。
錯誤處理錯誤處理可以在catch的回調或then的第二個回調里進行。雖然前面提到catch方法等價于then(undefined, Rejected狀態處理函數),但推薦始終使用catch來處理錯誤,原因有兩個:
代碼的可讀性
對于then(Fullfilled狀處理函數, Rejected狀態的處理函數)這種寫法,如果Fullfilled狀態的處理函數里出錯了,那錯誤只會繼續向下傳遞,同級的Rejected狀態處理函數沒辦法捕獲該錯誤
優化房間預訂例子的錯誤處理了解完了Promise調用鏈上的錯誤處理,我們再來回顧一開始提到的房間預訂例子。之前我們的代碼里只是對異步操作中的可能出現錯誤進行了統一的處理,但是其中的API error等別的執行錯誤并未使用在Promise調用鏈上捕獲和處理錯誤的方式。為了進一步DRY,我們可以通過調用Promise.reject,強制將返回的Promise對象變為Rejected狀態,共用統一的Promise錯誤處理:
(apiResponse)=> { if (apiResponse.isSuccessful) return insertToDB(data); // 返回的Promise對象為Rejected狀態,共用統一的Promise錯誤處理 else return Promise.reject(new Error("API error")); }Promise.all和Promise.race
前面研究的多個異步操作間往往具有前后依賴關系,或者說它們是“串行”進行的,只有前一個完成了才能進行后一個。但有時我們處理的異步操作間可能并不具有依賴關系,比如處理多張圖片,這時再使用上面的調用鏈寫法,就只能等處理完一張圖片、對應的Promise對象狀態變化了,才能再去處理下一張,就顯得很低效了。所以,我們需要一種能在調用鏈中同時處理多個Promise對象的方法,Promise.all和Promise.race就是這樣應運而生的。
這兩個方法的相同點是會接受一個Promise對象組成的數組作為參數,包裝返回成一個新的Promise實例。而它們的區別就在于返回的這個Promise實例狀態如何變化:
Promise.all:
所有傳入的Promise對象狀態都變成Fullfilled,最終狀態才會變成Fullfilled;此時便會調用Promise.resolve(各Promise對象resolve參數組成的數組),生成新狀態的Promise對象返回
各個Promise對象若有一個被reject,最終狀態就變成Rejected;此時便會調用Promise.reject(第一個被reject的實例的reject參數),生成新狀態的Promise對象返回
Promise.race:只要傳入的各個Promise對象中有一個率先改變狀態(Fullfilled或Rejected),返回的Promise對象狀態就會改變為相應狀態
有了這兩個方法,我們就能在Promise調用鏈上“并行”等待某些異步操作了,還是用前面提到的客房例子來舉例,如果我們在預定房間時需要請求的API不止一個,調用鏈可以這么寫:
promiseValidate(data) /* 請求多個API */ .then(()=> Promise.all([promiseRequest(apiUrl1), promiseRequest(apiUrl2)])) .then((apiResponse)=> { /* 傳給下個then回調的是一個resolve參數組成的數組 */ if (apiResponse[0].isSuccessful && apiResponse[1].isSuccessful) return insertToDB(data); else return Promise.reject(new Error("API error")); }) .then(()=> successHandler()) .catch((err)=> return errorHandler(err));Promise的應用
Promise是一種異步調用的寫法,自然是用來寫出清晰的異步代碼、讓我們擺脫回調寫法帶來的種種弊端,本文一直使用的預定房間例子就是一個佐證。不過考慮實際的應用場景,還是有一些需要注意的地方:
前端異步處理前端的瀏覽器兼容性是阻礙新技術運用的一大難題,雖然使目前瀏覽器對于ES6的支持越來越完善了,但除非你不考慮IE(兼容性表),否則在前端代碼里直接使用的原生的Promise實現并不太現實。對于這種情況,我們可以用一些Polyfill或拓展類庫來讓我們能寫Promise代碼。
Node的異步處理:Node.js環境下對ES6的Promise支持,在零點幾版開始就有了,所以我們在編寫服務器代碼、或者寫一些跑在Node上的模塊時可以直接上Promise語法。不過要注意的是,Node上的大部分模塊開放的API,還是默認使用回調風格,這是為了方便用戶在不了解Promise語法時快速上手;所以一般自己寫的模塊API也會遵循這個慣例,至于模塊內部實現那就隨你的意愿使用了。
還有一個要值得注意的是,最近Node實現了更優雅的異步寫法--async函數,不過新的寫法是基于Promise實現的,所以雖然async函數的出現讓Promise有種高不成低不就的感覺,但了解Promise的用法還是很有必要的,希望本文能幫你做到這點:D。
參考JavaScript Promise迷你書
Promise 的鏈式調用與中止
如何把 Callback 接口包裝成 Promise 接口
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82835.html
摘要:學習開發,無論是前端開發還是都避免不了要接觸異步編程這個問題就和其它大多數以多線程同步為主的編程語言不同的主要設計是單線程異步模型。由于異步編程可以實現非阻塞的調用效果,引入異步編程自然就是順理成章的事情了。 學習js開發,無論是前端開發還是node.js,都避免不了要接觸異步編程這個問題,就和其它大多數以多線程同步為主的編程語言不同,js的主要設計是單線程異步模型。正因為js天生的與...
摘要:即使耗時一秒的執行完畢,再的,仍然先于執行了,這很好地解釋了微任務優先的原理。把整個代碼分割成了個宏觀任務,這里不論是秒還是秒,都是一樣的。 js實現異步的幾種形式 回調函數 事件監聽 - 事件驅動模式 發布/訂閱 - 觀察者模式 Promises對象 js異步歷史 一個 JavaScript 引擎會常駐于內存中,它等待著我們把JavaScript 代碼或者函數傳遞給它執行 在 ...
摘要:版本以及之前,本身還沒有異步執行代碼的能力,宿主環境傳遞給引擎,然后按順序執行,由宿主發起任務。采納引擎術語,把宿主發起的任務稱為宏觀任務,把引擎發起的任務稱為微觀任務。基本用法示例的回調是一個異步的執行過程。 筆記說明 重學前端是程劭非(winter)【前手機淘寶前端負責人】在極客時間開的一個專欄,每天10分鐘,重構你的前端知識體系,筆者主要整理學習過程的一些要點筆記以及感悟,完整的...
摘要:版本以及之前,本身還沒有異步執行代碼的能力,宿主環境傳遞給引擎,然后按順序執行,由宿主發起任務。采納引擎術語,把宿主發起的任務稱為宏觀任務,把引擎發起的任務稱為微觀任務。基本用法示例的回調是一個異步的執行過程。 筆記說明 重學前端是程劭非(winter)【前手機淘寶前端負責人】在極客時間開的一個專欄,每天10分鐘,重構你的前端知識體系,筆者主要整理學習過程的一些要點筆記以及感悟,完整的...
閱讀 3056·2021-09-22 15:59
閱讀 1310·2021-08-30 09:46
閱讀 2272·2019-08-30 15:54
閱讀 2003·2019-08-26 12:15
閱讀 2530·2019-08-26 12:09
閱讀 1328·2019-08-26 11:57
閱讀 3333·2019-08-23 17:11
閱讀 1879·2019-08-23 15:59