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

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

為什么使用Promise

這篇關(guān)于promise的blog其實(shí)已經(jīng)是3年前寫(xiě)的了,但是一直在草稿狀態(tài)。因?yàn)楫?dāng)時(shí)的項(xiàng)目開(kāi)始使用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)通過(guò)才允許用戶(hù)提交。

再來(lái)看一下:

// 以下三個(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);
      }));
    }));
  }));
})();

這里和上面的例子,都使用了嵌套的寫(xiě)法,如果邏輯再?gòu)?fù)雜一點(diǎn),嵌套層數(shù)會(huì)更多,容易陷入回調(diào)地獄(callback hell)。

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

promise是如何處理的呢?

如果可以寫(xiě)成 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);})
})();

那么,一開(kāi)始的例子也可以改寫(xiě)成

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

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

什么是Promise

定義

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

狀態(tài)

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

狀態(tài)的變化只有兩種方法:pending變成fulfilledpending變成rejected,狀態(tài)變化時(shí),有以下的方法來(lái)處理:

方法

then(onFulfilled, onRejected) 添加解決(fulfillment)和拒絕(rejection)回調(diào)到當(dāng)前 promise, 返回一個(gè)新的 promise, 將以回調(diào)的返回值來(lái)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)用,無(wú)論當(dāng)前promise的狀態(tài)是完成(fulfilled)還是失敗(rejected)

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

或者使用以下寫(xiě)法

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

上面的例子,就可以寫(xiě)成:

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.

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

再來(lái)看看另一種寫(xiě)法:

(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省略了,沒(méi)有本質(zhì)上的區(qū)別。

再來(lái)做一點(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(),先來(lái)看代碼

(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類(lèi)型(注:Array,Map,Set都屬于ES6的iterable類(lèi)型)的輸入,并且只返回一個(gè)Promise實(shí)例, 那個(gè)輸入的所有promise的resolve回調(diào)的結(jié)果是一個(gè)數(shù)組。

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

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

(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èn)題。先來(lái)看看概念:

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

為了更好了看出執(zhí)行順序,我們先來(lái)修改一下上面的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í)器線(xiàn)程,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í)器線(xiàn)程,500ms后放入宏任務(wù)隊(duì)列
  7. 遇到setTimeout,移交給定時(shí)器線(xiàn)程,0ms后放入宏任務(wù)隊(duì)列(即使是0,但是仍然要按規(guī)矩)
  8. 遇到setTimeout,移交給定時(shí)器線(xiàn)程,250ms后放入宏任務(wù)隊(duì)列
  9. 執(zhí)行console.log("***** END ******"),到這里主線(xiàn)程執(zhí)行完畢
  10. 開(kāi)始執(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í)為同步

所以最開(kāi)始的時(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ì)比較麻煩,可以改寫(xiě)成

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上的說(shuō)明
Promise+
講JS運(yùn)行機(jī)制,事件循環(huán)講得很清晰

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容