??【異步】4. 異步方案之Promise

Promise

Promise對象是一個代理對象吼鳞。它接受你傳入的 executor (執(zhí)行器)作為入?yún)ⅲ试S你把異步任務的成功和失敗分別綁定到對應的處理方法上兄渺。一個 Promise 實例有三種狀態(tài)

  • pending 狀態(tài)择浊,表示進行中。這是 Promise 實例創(chuàng)建后的一個初始態(tài)刹碾;
  • fulfilled 狀態(tài),表示成功完成座柱。這是我們在執(zhí)行器中調(diào)用 resolve 后迷帜,達成的狀態(tài);
  • rejected 狀態(tài)色洞,表示操作失敗戏锹、被拒絕。這是我們在執(zhí)行器中調(diào)用 reject后火诸,達成的狀態(tài)锦针;
    【狀態(tài)切換機制】Promise實例的狀態(tài)是可以改變的,但它只允許被改變一次。 當我們的實例狀態(tài)從 pending 切換為 rejected 后奈搜,就無法再扭轉(zhuǎn)為 fulfilled悉盆,反之同理。當 Promise 的狀態(tài)為 resolved 時馋吗,會觸發(fā)其對應的 then 方法入?yún)⒗锏?onfulfilled 函數(shù)舀瓢;當 Promise 的狀態(tài)為 rejected 時,會觸發(fā)其對應的 then 方法入?yún)⒗锏?onrejected 函數(shù)耗美。

Promise解決的痛點

對于回調(diào)地獄的引發(fā)的問題,我們需要一種更加友好的代碼組織方式航缀,解決異步嵌套的問題商架。
于是 Promise 規(guī)范誕生了,并且在業(yè)界有了很多實現(xiàn)來解決回調(diào)地獄的痛點芥玉。比如業(yè)界著名的 Qbluebird 蛇摸,bluebird 甚至號稱運行最快的類庫。
Promise對象現(xiàn)已在ECMAScript 2015中作為JavaScript的標準內(nèi)置對象提供灿巧,這個對象根據(jù) Promise A+ 規(guī)范實現(xiàn)赶袄。(Promise規(guī)范有很多,如Promise/A抠藕,Promise/B饿肺,Promise/D 以及 Promise/A的升級版 Promise/A+,最終ES6采用了Promise/A+規(guī)范)

  1. 回調(diào)嵌套 -> 理解問題盾似,缺乏順序性
new Promise(請求1)
    .then(請求2(請求結(jié)果1))
    .then(請求3(請求結(jié)果2))
    .then(請求4(請求結(jié)果3))
    .then(請求5(請求結(jié)果4))
    .catch(處理異常(異常信息))

對比Promise寫法和嵌套回調(diào)寫法敬辣,Promise鏈以順序的方式表達異步流,有助于我們的大腦更好的計劃和維護異步JavaScript代碼零院,并且能夠在外層捕獲異步函數(shù)的異常信息溉跃。

  1. 控制反轉(zhuǎn) -> 信任問題

如果我們能夠把控制反轉(zhuǎn)再反轉(zhuǎn)回來,會怎樣呢告抄?如果我們不把自己程序的continuation傳給第三方撰茎,而是希望第三方給我們提供了解其任務何時結(jié)束的能力,然后由我們自己的代碼來決定下一步做什么打洼,那將會怎樣呢龄糊?這種范式就稱為Promise。

Promise封裝了依賴于時間的狀態(tài)——等待底層值的完成或拒絕募疮,所以Promise本身是與時間無關(guān)的绎签。因此,Promise可以按照可預測的方式組合酝锅,而不用關(guān)心時序或者底層的結(jié)果诡必。
Promise是一種封裝和組合未來值的易于復用的機制。

Promise的決議也可以看做是一種在異步任務中作為兩個或更多步驟的流程控制機制。

?? 場景:要調(diào)用一個函數(shù)foo()執(zhí)行某個任務爸舒,期望通過某種方式在foo()執(zhí)行完成時得到通知
??? 思考:在典型的JavaScript場景中蟋字,如果需要偵聽某個通知,就會使用事件扭勉。需實現(xiàn)對foo()發(fā)出的一個完成事件的偵聽鹊奖。

  • 使用回調(diào),通知就是任務(foo(...))調(diào)用的回調(diào)涂炎。
  • 使用Promise忠聚,這個關(guān)系就反轉(zhuǎn)過來了,偵聽來自foo(..)的事件唱捣,然后在得到通知的時候做相關(guān)處理两蟀。
  • 顯式地創(chuàng)建并返回一個事件訂閱對象:
function foo(x){
  // ...do something
  // 構(gòu)造一個listener處理
  return listener;
}
var evt = foo(42);
evt.on("completion", function(){
  // 可以進行下一步
})
evt.on("failure", function(){
  // foo(..)中出錯了
})

Promise模式構(gòu)建的最重要的特性,就是解決了部分信任問題:

  • 調(diào)用過早:即使這個Promise已經(jīng)決議震缭,提供給then(..)的回調(diào)也總會被異步調(diào)用赂毯。不需要再插入setTimeout(.., 0) hack,Promise會自動防止Zalgo出現(xiàn)拣宰。
  • 調(diào)用過晚:Promise創(chuàng)建對象調(diào)用resolve()或reject()時党涕,這個Promise的then(..)注冊的觀察回調(diào)就會被自動調(diào)度。也就是說巡社,一個Promise決議后膛堤,這個Promise上所有通過then(..)注冊的回調(diào)都會在下一個異步時機點上依次被立即調(diào)用。
  • 回調(diào)未調(diào)用 & 調(diào)用次數(shù)過少(0次):如果你對一個Promise注冊了一個完成回調(diào)和一個拒絕回調(diào)晌该,那么Promise在決議時總是會調(diào)用其中的一個骑祟。即使回調(diào)本身包含JavaScript錯誤,也不會被吞掉气笙。
    如果Promise本身永遠不被決議次企,Promise提供超時模式來解決。
  • 調(diào)用次數(shù)過多(>1次):Promise的定義方式使得其只能被決議一次潜圃,所有通過then(..)注冊的回調(diào)都只會被調(diào)一次缸棵。Promise只接受第一次決議,并默默地忽略任何后續(xù)調(diào)用谭期。
  • 未能傳遞參數(shù)/環(huán)境值:如果沒有用任何值顯式?jīng)Q議堵第,那這個值就是undefined,會被傳給所有注冊的回調(diào)隧出。
  • 吞掉錯誤或異常:在Promise創(chuàng)建過程或查看其決議結(jié)果過程中的任何時間點上出現(xiàn)JavaScript異常踏志,這個異常都會被捕獲,并且會使這個Promise被拒絕胀瞪。
var p = new Promise(function(resolve, reject){
  resolve(42);
});
p.then(function fulfilled(msg){
  foo.bar();
  console.log(msg); // 永遠不會到達這里
}, function rejected(err){
  console.log(err); // 永遠不會到達這里
}).then(function fulfilled(msg){
  console.log('....'+msg); // 永遠不會到達這里
}, function rejected(err){
  console.log('....')
  console.log(err); // 到達這里
})

Promise并沒有完成擺脫回調(diào)针余,只是改變了傳遞回調(diào)的位置饲鄙。并沒有把回調(diào)傳給foo(..),而是從foo(..)獲得某個東西(Promise)圆雁,然后把回調(diào)傳給他忍级。

??? Q:為什么這就比單純的使用回調(diào)更值得信任呢?如何確定返回的這個東西實際上就是一個可信任的Promise伪朽?
?? A:Promise對這個問題已經(jīng)有一個解決方案:原生ES6 Promise實現(xiàn)中的解決方案就是 Promise.resolve() 轴咱。 可接受任何thenable,得到一個真正的Promise烈涮。如果傳入的已經(jīng)是真正的Promise朴肺,將得到其本身。

Promise常見方法及其作用

類方法

JavaScript中的類(對象)方法可以認為是靜態(tài)方法(即:不需要實例化就可以使用的方法)

  1. Promise.all(iterable):這個方法返回一個新的 promise 對象坚洽,該 promise 對象在 iterable 參數(shù)對象里所有的 promise 對象都成功的時候才會觸發(fā)成功戈稿,一旦有任何一個 iterable 里面的 promise 對象失敗則立即觸發(fā)該 promise 對象的失敗。
  2. Promise.race(iterable):當 iterable 參數(shù)里的任意一個子 promise 被成功或失敗后酪术,父 promise 馬上也會用子 promise 的成功返回值或失敗詳情作為參數(shù)調(diào)用父 promise 綁定的相應處理函數(shù),并返回該 promise 對象翠储。
  3. Promise.reject(reason): 返回一個狀態(tài)為失敗的Promise對象绘雁,并將給定的失敗信息傳遞給對應的處理方法。
  4. Promise.resolve(value):它返回一個 Promise 對象援所,但是這個對象的狀態(tài)由你傳入的value決定庐舟,情形分以下三種:
  • 如果傳入的是一個帶有 then 方法的對象(我們稱為 thenable 對象),返回的Promise對象會跟隨這個thenable對象住拭,采用它的最終狀態(tài)( resolved/rejected/pending/settled)挪略;
  • 如果傳入的value本身就是Promise對象,則該對象作為Promise.resolve方法的返回值返回滔岳;
  • 其他情況以該值為成功狀態(tài)返回一個Promise對象杠娱;
// 如果傳入的 value 本身就是 Promise 對象,則該對象作為 Promise.resolve 方法的返回值返回谱煤。  
function fn(resolve){
    setTimeout(function(){
        resolve(123);
    },3000);
}
let p0 = new Promise(fn);
let p1 = Promise.resolve(p0);

console.log(p0 === p1); // 返回為true摊求,返回的 Promise 即是 入?yún)⒌?Promise 對象。

實例方法

實例方法刘离,是指創(chuàng)建Promise實例后才能使用的方法室叉,即:被添加到原型鏈 Promise.prototype 上的方法。

  1. Promise.prototype.then 實例方法硫惕,為Promise注冊回調(diào)茧痕,fn(value){}其中value是上一個任務的返回結(jié)果。如果我們的后續(xù)任務是異步任務的話恼除,必須return一個新的promise對象踪旷;如果后續(xù)任務是同步任務,只需return一個結(jié)果即可。
    then 中的函數(shù)一定要 return 一個結(jié)果或者一個新的 Promise 對象埃脏,才可以讓之后的then 回調(diào)接收搪锣。
  2. Promise.prototype.catch 捕獲異常,可以捕獲到前面回調(diào)中可能拋出的異常彩掐。

Promise A+規(guī)范規(guī)定:每個Promise實例中返回的都應該是一個Promise實例或thenable對象构舟。基于這個特性堵幽,能夠?qū)崿F(xiàn)類似于同步的鏈式調(diào)用狗超。

new Promise((resolve, reject) => {
  // a()
  resolve(2) 
}).then(value => {
  a();
  console.log(value);
}).catch(err => {
  console.log('..................'); // 可以捕獲到
  console.log('err:', err);
})

異常捕獲

對于多數(shù)開發(fā)者來說,錯誤處理最自然的形式就是同步的try..catch結(jié)構(gòu)朴下。遺憾的是努咐,它只能是同步的,無法用于異步代碼模塊殴胧。

  • error-firset回調(diào)設計風格錯誤處理:多級error-first回調(diào)交織在一起渗稍,再加上if檢查語句,很容易引發(fā)回調(diào)地獄的風險
  • 分離回調(diào)風格錯誤處理:接收兩個參數(shù)团滥,一個回調(diào)用于完成情況竿屹,一個回調(diào)用于拒絕情況(非必填)。
    分離回調(diào)風格的錯誤錯誤易于出錯灸姊,如果沒有傳入第二個拒絕回調(diào)拱燃,非常容易造成錯誤被吞掉。
    為了避免丟失被忽略和拋棄的Promise錯誤力惯,一些開發(fā)者表示Promise鏈的一個最佳實踐就是最后總以一個catch(...)結(jié)束碗誉,比如:
var p = Promise.resolve(42);
p.then(function fulfilled(msg){
    console.log(msg.toLowerCase); // 數(shù)字沒有string函數(shù),會拋錯
})
.catch( handleErrors )

因為我們沒有為then(..)傳入拒絕處理函數(shù)父晶,所以默認的處理函數(shù)被替換掉了哮缺,而這僅僅是把錯誤傳遞給了鏈中的下一個Promise。因此甲喝,進入p的錯誤以及p之后進入其決議的錯誤都會傳遞到最后的handleErrors(...)

??? Q:如果handleErrors本身內(nèi)部也有錯誤怎么辦呢蝴蜓?
?? A:瀏覽器有一個特定的功能是我們的代碼所沒有的,它們可以跟蹤并了解所有對象被丟棄以及垃圾回收的機制俺猿。所以茎匠,瀏覽器可以追蹤Promise對象。如果在它被垃圾回收的時候其中有拒絕押袍,瀏覽器就能夠確保這是一個真正的未捕獲錯誤诵冒,進而可以確定應該將其報告到開發(fā)者終端。

Promise的使用

  1. 例1:
const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
});
promise.then(() => {
    console.log(3);
});
console.log(4);
// 1 2 4 3
  1. 例2
const promise = new Promise((resolve, reject) => {
  resolve('第 1 次 resolve')
  console.log('resolve后的普通邏輯')
  reject('error')
  resolve('第 2 次 resolve')
})
promise
.then((res) => {
  console.log('then: ', res)
})
.catch((err) => {
  console.log('catch: ', err)
})
// resolve后的普通邏輯
// then:  第 1 次 resolve

Promise 對象的狀態(tài)只能被改變一次谊惭。 我們忽略的是第一次 resolve 后的 reject汽馋、resolve侮东,而不是忽略它身后的所有代碼。因此 console.log(‘resolve后的普通邏輯’) 這句豹芯,仍然可以正常被執(zhí)行悄雅。

  1. 例3 值穿透問題
Promise.resolve(1)
  .then(Promise.resolve(2))
  .then(3)
  .then()
  .then(console.log)
// 1

then 方法里允許我們傳入兩個參數(shù):onFulfilled(成功態(tài)的處理函數(shù))和 onRejected(失敗態(tài)的處理函數(shù))。
可以兩者都傳铁蹈,也可以只傳前者或者后者宽闲。但是無論如何,then 方法的入?yún)⒅荒苁呛瘮?shù)握牧,其他都會被忽略容诬。
在這個過程中,我們最初 resolve 出來那個值沿腰,穿越了一個又一個無效的 then 調(diào)用览徒,就好像是這些 then 調(diào)用都是透明的、不存在的一樣颂龙,因此這種情形我們也形象地稱它是 Promise 的“值穿透”习蓬。

手寫一個Promise的 polyfill

精簡版

function CutePromise(executor){
  this.value = null; //記錄異步任務成功的執(zhí)行結(jié)果
  this.reason = null; //記錄異步任務失敗的原因
  this.status = 'pending'; //記錄當前的狀態(tài) 初始化為pending

  //緩存兩個隊列,維護resolved和rejected各自對應的處理函數(shù)
  this.onResolvedQueue = [];
  this.onRejectedQueue = [];

  var self = this;

  function resolve(value){
    if(self.status !== 'pending'){
      return;
    }
    self.value = value;
    self.status = 'resolved';
    //用setTimeout延遲隊列任務的執(zhí)行
    setTimeout(function(){
      self.onResolvedQueue.forEach(resolved => resolved(self.value));
    })
  }
  function reject(reason){
    if(self.status !== 'pending'){
      return;
    }
    self.reason = reason;
    self.status = 'rejected';
    setTimeout(function(){
      self.onRejectedQueue.forEach(rejected => rejected(self.reason));
    })
  }
  
  //把 resolve 和 reject 能力賦予執(zhí)行器
  executor(resolve, reject);
}

CutePromise.prototype.then = function(onResolved, onRejected){
  if(typeof onResolved !== 'function'){
    onResolved = function(x){ return x };
  }
  if(typeof onRejected !== 'function'){
    onRejected = function(e){ throw e };
  }

  var self = this;
  if(self.status === 'resolved'){
    onResolved(self.value);
  }else if(self.status === 'rejected'){
    onRejected(self.reason);
  }else if(self.status === 'pending'){
    //如果是pending狀態(tài)措嵌,則只對任務做入隊列處理
    self.onResolvedQueue.push(onResolved);
    self.onRejectedQueue.push(onRejected);
  }
  return this; //鏈式調(diào)用  ?? 真實的場景是返回一個新的Promise實例
}

new CutePromise(function(resolve, reject){
  resolve('成了躲叼!');
}).then((value) => {
  console.log(value)
  console.log('我是第 1 個任務')
  return '第一個任務的結(jié)果'
}).then(value => {
  console.log(value);
  console.log('我是第 2 個任務')
});
// 依次輸出“成了!” “我是第 1 個任務” “我是第 2 個任務

其他

阮一峰ES6-Promise
Promise A+規(guī)范
Promise代碼題
ES6系列之聊聊Promise

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铅匹,一起剝皮案震驚了整個濱河市押赊,隨后出現(xiàn)的幾起案子饺藤,更是在濱河造成了極大的恐慌包斑,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涕俗,死亡現(xiàn)場離奇詭異罗丰,居然都是意外死亡,警方通過查閱死者的電腦和手機再姑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門萌抵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人元镀,你說我怎么就攤上這事绍填。” “怎么了栖疑?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵讨永,是天一觀的道長。 經(jīng)常有香客問我遇革,道長卿闹,這世上最難降的妖魔是什么揭糕? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮锻霎,結(jié)果婚禮上著角,老公的妹妹穿的比我還像新娘。我一直安慰自己旋恼,他們只是感情好吏口,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蚌铜,像睡著了一般锨侯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上冬殃,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天囚痴,我揣著相機與錄音,去河邊找鬼审葬。 笑死深滚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的涣觉。 我是一名探鬼主播痴荐,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼官册!你這毒婦竟也來了生兆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤膝宁,失蹤者是張志新(化名)和其女友劉穎鸦难,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體员淫,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡合蔽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年贱枣,在試婚紗的時候發(fā)現(xiàn)自己被綠了笙以。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡弓摘,死狀恐怖圣蝎,靈堂內(nèi)的尸體忽然破棺而出刃宵,到底是詐尸還是另有隱情,我是刑警寧澤徘公,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布牲证,位于F島的核電站,受9級特大地震影響步淹,放射性物質(zhì)發(fā)生泄漏从隆。R本人自食惡果不足惜诚撵,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望键闺。 院中可真熱鬧寿烟,春花似錦、人聲如沸辛燥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挎塌。三九已至徘六,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榴都,已是汗流浹背待锈。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嘴高,地道東北人竿音。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像拴驮,于是被迫代替她去往敵國和親春瞬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355