JS基礎(chǔ)整理(3)—來說說Promise和事件循環(huán)吧

Promise是什么伯病?為什么要使用?

為什么使用Promise

這篇關(guān)于promise的blog其實(shí)已經(jīng)是3年前寫的了,但是一直在草稿狀態(tài)训桶。因?yàn)楫?dāng)時(shí)的項(xiàng)目開始使用ES6,我第一次接觸到promise這個(gè)概念酣倾,當(dāng)時(shí)還花了一點(diǎn)時(shí)間去理解舵揭。

現(xiàn)在每一個(gè)前端工作者肯定非常熟悉promise,它是用于處理異步的躁锡!那么午绳,為什么要用promise呢?

首先看一個(gè)項(xiàng)目上的例子:

let submit = function(params){
  validate(params, res=>{
    if(res.data === "TRUE"){
      submitData(params, res=>{
        if(res.data === "TRUE"){
          // other actions
        }
      })
    }
  })
}

以上例子映之,實(shí)現(xiàn)一個(gè)表單提交功能拦焚,在真正把數(shù)據(jù)提交到后臺(tái)之前,先要做一次校驗(yàn)惕医,校驗(yàn)通過才允許用戶提交耕漱。

再來看一下:

// 以下三個(gè)函數(shù)模擬異步方法
function job1(fn){
  setTimeout(() => { fn("job1 success"); }, 150);
}
function job2(fn){
  setTimeout(() => { fn("job2 success"); }, 200);
}
function job3(fn){
  setTimeout(() => { fn("job3 success!"); }, 100);
}
(function(){
  job1((res=>{ console.log(res); }));
  job2((res=>{ console.log(res); }));
  job3((res=>{ console.log(res); }));
})();

以上輸出:
job3 success
job1 success
job2 success

如果我們的需求是,job1, job2, job3必須按順序執(zhí)行抬伺,代碼得改成:

(function(){
  job1((res=>{
    console.log(res);
    job2((res=>{
      console.log(res);
      job3((res=>{
        console.log(res);
      }));
    }));
  }));
})();

這里和上面的例子螟够,都使用了嵌套的寫法,如果邏輯再復(fù)雜一點(diǎn)峡钓,嵌套層數(shù)會(huì)更多妓笙,容易陷入回調(diào)地獄(callback hell)

AjaxNode.js回調(diào)地獄例子就非常經(jīng)典了能岩。而promise就是為了解決這個(gè)問題寞宫。

promise是如何處理的呢?

如果可以寫成 job1.then(job2).then(job3)... 是不是好多了?

把異步方法修改為Promise

function job1(){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      console.log("job1 success");
      resolve("job1 success");
    }, 150);
  })
  
}
function job2(){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      console.log("job2 success");
      resolve("job2 success");
    }, 200);
  })
}
function job3(){
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      console.log("job3 success");
      resolve("job3 success");
    }, 100);
  })
}

這時(shí)候就可以使用鏈?zhǔn)椒椒ㄕ{(diào)用了

(function(){
  job1().then(job2).then(job3).then(res=>{console.log(res);})
})();

那么拉鹃,一開始的例子也可以改寫成

let submit = function(params){
  validate(params)
    .then(submitData(params))
    .then(res=>{ });
}

下面辈赋,我們一起來看看Promise是怎樣實(shí)現(xiàn)的

什么是Promise

定義

Promise 對(duì)象用于表示一個(gè)異步操作的最終完成 (或失敗)及其結(jié)果值鲫忍。

狀態(tài)

一個(gè) Promise 必然處于這幾種狀態(tài)之一:
pending(進(jìn)行中)
fulfilled(已成功)
rejected(已失敗)

狀態(tài)的變化只有兩種方法:pending變成fulfilled钥屈,pending變成rejected悟民,狀態(tài)變化時(shí),有以下的方法來處理:

方法

then(onFulfilled, onRejected) 添加解決(fulfillment)和拒絕(rejection)回調(diào)到當(dāng)前 promise, 返回一個(gè)新的 promise, 將以回調(diào)的返回值來resolve
catch(onRejected) 添加一個(gè)拒絕(rejection) 回調(diào)到當(dāng)前 promise, 返回一個(gè)新的promise
finally(onFinally) 添加一個(gè)事件處理回調(diào)于當(dāng)前promise對(duì)象篷就,并且在原promise對(duì)象解析完畢后射亏,返回一個(gè)新的promise對(duì)象〗咭担回調(diào)會(huì)在當(dāng)前promise運(yùn)行完畢后被調(diào)用智润,無論當(dāng)前promise的狀態(tài)是完成(fulfilled)還是失敗(rejected)

// MDN上的例子
const myPromise =
  (new Promise(myExecutorFunc))
  .then(onFulfilledA,onRejectedA)
  .then(onFulfilledB,onRejectedB)
  .then(onFulfilledC,onRejectedC);

或者使用以下寫法

const myPromise =
  (new Promise(myExecutorFunc))
  .then(onFulfilledA)
  .then(onFulfilledB)
  .then(onFulfilledC)
  .catch(onRejectedAny);

上面的例子,就可以寫成:

let onFulfilled = (data)=>{ console.log("Fulfilled: ", data); }
let onRejected = (error)=>{ console.log("Error: ", error); }
let onFinally = ()=>{ console.log("Finally."); }

(function(){
  job1().then(job2).then(job3).then(onFulfilled)
  .catch(onRejected)
  .finally(onFinally);
})();

輸出:
job1 success
job2 success
job3 success
Fulfilled: job3 success
Finally.

假如其中一個(gè)job有error未辆,那么輸出是
job1 success
job2 error
Error: job2 error
Finally.

可以看出窟绷,無論當(dāng)前promise的狀態(tài)是完成(fulfilled)還是失敗(rejected)finally()都會(huì)被調(diào)用鼎姐。

再來看看另一種寫法:

(function(){
  job1()
  .then(job2)
  .then(job3)
  .then(onFulfilled,onRejected)
  .finally(onFinally);
})();

使用then(onFulfilled,onRejected) 代替catch(onRejected)钾麸,輸出和以上例子一樣更振,所以炕桨,catch(onRejected) 其實(shí)是把then(onFulfilled,onRejected)的預(yù)留參數(shù)onFulfilled省略了,沒有本質(zhì)上的區(qū)別肯腕。

再來做一點(diǎn)修改

(function(){
  job1()
  .then(job2,onRejected)
  .then(job3,onRejected)
  .then(onFulfilled,onRejected)
  .finally(onFinally);
})();

輸出:
job1 success (第二行 job1 的輸出)
job2 error (第三行 job2的輸出)
Error: job2 error (第四行 onRejected 的輸出)
Fulfilled: undefined (第五行 onFulfilled 的輸出)
Finally. (第六行 onFinally 的輸出)

job2的promise調(diào)用了reject方法献宫,狀態(tài)變成rejected,所以在then()的時(shí)候調(diào)用了onRejected实撒,但是promise的方法都會(huì)返回一個(gè)新的promise姊途,所以在第五行的時(shí)候,then()對(duì)應(yīng)的promise是上一行onRejected()返回的promise, 會(huì)調(diào)用onFulfilled()

任何不是 throw 的終止都會(huì)創(chuàng)建一個(gè)"已決議(resolved)"狀態(tài)知态,而以 throw 終止則會(huì)創(chuàng)建一個(gè)"已拒絕"狀態(tài)捷兰。

如果我們把onRejected()修改一下

let onRejected = (error)=>{
  console.log("Error: ", error);
  throw new Error(error);
}

那么,上面的輸出就變成:
job1 success (第二行 job1 的輸出)
job2 error (第三行 job2的輸出)
Error: job2 error (第四行 onRejected 的輸出)
Error: Error: job2 error (第五行 onRejected 的輸出) *
at onRejected (.../test.js:34:9)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
Finally. 
(第六行 onFinally 的輸出)*

靜態(tài)方法

有一個(gè)使用得比較多的方法是Promise.all()负敏,先來看代碼

(function() {
  let p1 = job1();
  let p2 = job2();
  let p3 = job3();
  Promise.all([p1, p2, p3]).then(values=>{
    console.log(values); // 
  })
})();

輸出:
job3 success
job1 success
job2 success
[ 'job1 success', 'job2 success', 'job3 success' ]

Promise.all()方法接收一個(gè)promiseiterable類型(注:Array贡茅,MapSet都屬于ES6的iterable類型)的輸入其做,并且只返回一個(gè)Promise實(shí)例顶考, 那個(gè)輸入的所有promise的resolve回調(diào)的結(jié)果是一個(gè)數(shù)組。

但是這里注意一下妖泄,和上面的對(duì)比驹沿,job1、job2蹈胡、job3不是按順序執(zhí)行的渊季。

我們是不是還可能用上面then(onFulfilled,onRejected)或者catch(onRejected)來使用呢朋蔫?

(function() {
  let p1 = job1();
  let p2 = job2();
  let p3 = job3();
  Promise.all([p1, p2, p3]).then(onFulfilled, onRejected).finally(onFinally);
})();
// 或者
(function() {
  let p1 = job1();
  let p2 = job2();
  let p3 = job3();
  Promise.all([p1, p2, p3]).then(onFulfilled).catch(onRejected).finally(onFinally);
})();

輸入都是:
Error: job2 error
Finally.

Promise.all 在任意一個(gè)傳入的 promise 失敗時(shí)返回失敗。

因?yàn)閖ob2的狀態(tài)是失敗了却汉,所以最后調(diào)用的是onRejected

Promise與事件循環(huán)

當(dāng)涉及異步事件的時(shí)候斑举,事件循環(huán)就成是了個(gè)很讓人頭大的問題。先來看看概念:

  • 宏任務(wù)
    • 主代碼塊
    • setTimeout
    • setInterval
    • setImmediate ()-Node
    • requestAnimationFrame ()-瀏覽器
  • 微任務(wù)
    • process.nextTick ()-Node
    • Promise.then()
    • catch
    • finally
    • Object.observe
    • MutationObserver

為了更好了看出執(zhí)行順序病涨,我們先來修改一下上面的job的定義

function job1(){
  return new Promise((resolve, reject)=>{
    console.log("job1 start...")
    setTimeout(() => {
      console.log("job1 success");
      resolve(1);
    }, 150); //定時(shí)器富玷,150ms后執(zhí)行
  })
}
function job2(){
  return new Promise((resolve, reject)=>{
    console.log("job2 start...")
    setTimeout(() => {
      console.log("job2 success");
      resolve(2);
    }, 100); //定時(shí)器,100ms后執(zhí)行
  })
}
function job3(){
  return new Promise((resolve, reject)=>{
    console.log("job3 start...")
    setTimeout(() => {
      console.log("job3 success");
      resolve(3);
    },0);
  })
}

調(diào)用方法如下

console.log("***** START ******");
let p1 = job1();
let p2 = p1.then(job2);
let p3 = p2.then(job3);
let p = p3.then(onFulfilled);
console.log(p1, p2, p3, p);


setTimeout(() => {
  console.log('500ms: the stack is now empty');
  console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
  console.log('0ms...');
},0);
setTimeout(() => {
  console.log('250ms...');
},250);
console.log("***** END ******");

輸入順序會(huì)是怎樣呢既穆?

分析:
根據(jù)事件循環(huán)赎懦,

  1. 先執(zhí)行同步方法console.log("***** START ******");
  2. 構(gòu)造函數(shù)new Promise()是同步任務(wù),所以執(zhí)行 job1的console.log("job1 start...")
  3. 遇到setTimeout幻工,移交給定時(shí)器線程励两,150ms后放入宏任務(wù)隊(duì)列,到此job1結(jié)束
  4. 接下都是 Promise.then()的方法囊颅,是異步微任務(wù)当悔,放入微任務(wù)隊(duì)列
  5. 執(zhí)行 console.log(p1, p2, p3, p);,這時(shí)踢代,promise的狀態(tài)都是pending
  6. 遇到setTimeout盲憎,移交給定時(shí)器線程,500ms后放入宏任務(wù)隊(duì)列
  7. 遇到setTimeout胳挎,移交給定時(shí)器線程饼疙,0ms后放入宏任務(wù)隊(duì)列(即使是0,但是仍然要按規(guī)矩)
  8. 遇到setTimeout慕爬,移交給定時(shí)器線程窑眯,250ms后放入宏任務(wù)隊(duì)列
  9. 執(zhí)行console.log("***** END ******"),到這里主線程執(zhí)行完畢
  10. 開始執(zhí)行任務(wù)隊(duì)列医窿,宏任務(wù)隊(duì)列中根據(jù)時(shí)間順序: [0ms, 200ms,250ms, 500ms]
    a. 執(zhí)行console.log('0ms...');
    b. 執(zhí)行console.log("job1 success");resolve(1);
    c. 執(zhí)行console.log('250ms...');
    d. 執(zhí)行console.log('500ms: the stack is now empty'');console.log(p1, p2, p3, p);
    但是這里注意一下磅甩,當(dāng)一個(gè)宏任務(wù)執(zhí)行完,會(huì)在渲染前姥卢,將執(zhí)行期間所產(chǎn)生的所有微任務(wù)都執(zhí)行完 卷要。b任務(wù)執(zhí)行完的時(shí)候,p1.then(job2)會(huì)執(zhí)行隔显,即會(huì)執(zhí)行console.log("job2 start...")却妨,但是由于job2中也有setTimeout,根據(jù)時(shí)間放入宏任務(wù)隊(duì)列

最后輸出:

***** START ******
job1 start...
Promise { <pending> } Promise { <pending> } Promise { <pending> } Promise { <pending> }
***** END ******
0ms...
job1 success
job2 start...
job2 success
job3 start...
250ms...
job3 success
Fulfilled:  3
500ms: the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { 'Completed!' }

最后所有promise都是fulfilled/rejected狀態(tài)

Promise.all()的同步和異步

如果使用Promise.all()呢括眠?

console.log("***** START ******");
let p1 = job1();
let p3 = job3();
let p2 = job2();
let p = Promise.all([p1, p2, p3]);
let ep = Promise.all([]);

console.log(p1, p2, p3);
console.log(ep, p);
setTimeout(() => {
  console.log('the stack is now empty');
  console.log(p1, p2, p3, p);
},500);
setTimeout(() => {
  console.log('0ms...');
},0);
console.log("***** END ******")

結(jié)果:

***** START ******
job1 start...
job3 start...
job2 start...
Promise { <pending> } Promise { <pending> } Promise { <pending> }
Promise { [] } Promise { <pending> }
***** END ******
job3 success
0ms...
job2 success
job1 success
the stack is now empty
Promise { 1 } Promise { 2 } Promise { 3 } Promise { [ 1, 2, 3 ] }

這里有一個(gè)注意點(diǎn):

Promise.all當(dāng)且僅當(dāng)傳入的可迭代對(duì)象為空時(shí)為同步

所以最開始的時(shí)候彪标,console.log(ep, p);的輸出一個(gè)是fulfilled,一個(gè)是pending

async/await

最后順便看看 ES2017新增的 async/await

await關(guān)鍵字接收一個(gè)promise并獎(jiǎng)其轉(zhuǎn)換為一個(gè)返回值或拋出一個(gè)異常
async關(guān)鍵字意味著函數(shù)返回一個(gè)promise

任何使用await的代碼都是異步的掷豺,只能在async關(guān)鍵字聲明的函數(shù)內(nèi)部使用await關(guān)鍵字

上面的例子捞烟,如果想要取出每一步的結(jié)果薄声,可能會(huì)比較麻煩,可以改寫成

async function run() {
  // 按順序執(zhí)行
  let r1 = await job1();
  let r2 = await job2();
  let r3 = await job3(); 
  console.log(r1,r2, r3);
}
// output: 1 2 3

或使用Promise.all

async function run() {
  // 不會(huì)按順序執(zhí)行
  let [r1,r2, r3] = await Promise.all([job1(), job2(), job3()]);
  console.log(r1,r2, r3);
}
// output: 1 2 3

參考文章:
HTML Standard
MDN上的說明
Promise+
講JS運(yùn)行機(jī)制题画,事件循環(huán)講得很清晰

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末默辨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子苍息,更是在濱河造成了極大的恐慌缩幸,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件竞思,死亡現(xiàn)場(chǎng)離奇詭異表谊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盖喷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門爆办,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人课梳,你說我怎么就攤上這事距辆。” “怎么了暮刃?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵跨算,是天一觀的道長。 經(jīng)常有香客問我沾歪,道長漂彤,這世上最難降的妖魔是什么雾消? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任灾搏,我火速辦了婚禮,結(jié)果婚禮上立润,老公的妹妹穿的比我還像新娘狂窑。我一直安慰自己,他們只是感情好桑腮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布泉哈。 她就那樣靜靜地躺著,像睡著了一般破讨。 火紅的嫁衣襯著肌膚如雪丛晦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天提陶,我揣著相機(jī)與錄音烫沙,去河邊找鬼。 笑死隙笆,一個(gè)胖子當(dāng)著我的面吹牛锌蓄,可吹牛的內(nèi)容都是我干的升筏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼瘸爽,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼您访!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起剪决,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤灵汪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后柑潦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體识虚,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年妒茬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了担锤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡乍钻,死狀恐怖肛循,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情银择,我是刑警寧澤多糠,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站浩考,受9級(jí)特大地震影響夹孔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜析孽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一搭伤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧袜瞬,春花似錦怜俐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袄秩。三九已至诗眨,卻和暖如春忧换,著一層夾襖步出監(jiān)牢的瞬間晚凿,已是汗流浹背词疼。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國打工厢蒜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留葛菇,地道東北人则披。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓一忱,卻偏偏與公主長得像莲蜘,于是被迫代替她去往敵國和親谭确。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容