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