Promise詳解

前言:


日常開發(fā)中,異步操作幾乎每天都能見到。傳統(tǒng)的意不解決方案是通過回調(diào)函數(shù),隨著程序邏輯越來越復(fù)雜,回調(diào)函數(shù)的方式變得越來越繁瑣,很容易出現(xiàn)回調(diào)地獄,于是一種更合理更強(qiáng)大的代替方案出現(xiàn)--Promise,接下來就深入學(xué)習(xí)Promise是如何解決異步操作的。

一.基礎(chǔ)

定義: Promise 對象用于表示一個異步操作的最終完成 (或失敗), 及其結(jié)果值.

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

promise1.then((value) => {
  console.log(value);
  // expected output: "foo"
});

console.log(promise1);
// expected output: [object Promise]


運(yùn)行上面這段代碼,先是打印 [object Promise] ,300ms后打印foo

語法

new Promise( function(resolve, reject) {...} /* executor */  )


參數(shù) executor executor是帶有 resolve 和 reject 兩個參數(shù)的函數(shù) 。Promise構(gòu)造函數(shù)執(zhí)行時立即調(diào)用executor 函數(shù), resolve 和 reject 兩個函數(shù)作為參數(shù)傳遞給executor(executor 函數(shù)在Promise構(gòu)造函數(shù)返回promise實(shí)例對象前被調(diào)用)。resolve 和 reject 函數(shù)被調(diào)用時,分別將promise的狀態(tài)改為fulfilled(完成)或rejected(失敗)。executor 內(nèi)部通常會執(zhí)行一些異步操作,一旦異步操作執(zhí)行完畢(可能成功/失敗),要么調(diào)用resolve函數(shù)來將promise狀態(tài)改成fulfilled,要么調(diào)用reject 函數(shù)將promise的狀態(tài)改為rejected。如果在executor函數(shù)中拋出一個錯誤,那么該promise 狀態(tài)為rejected。executor函數(shù)的返回值被忽略。

這段描述分解下就是:

  1. 實(shí)例化Promise對象時需要傳入一個executor函數(shù),所有業(yè)務(wù)代碼都需要寫在這個函數(shù)中;
  2. executor函數(shù)在構(gòu)造函數(shù)執(zhí)行時就會調(diào)用,此時實(shí)例化對象還并沒有被創(chuàng)建, resolve 和 reject 兩個函數(shù)作為參數(shù)傳遞給executor,resolve 和 reject 函數(shù)被調(diào)用時,分別將promise的狀態(tài)改為fulfilled(完成)或rejected(失敗)。一旦狀態(tài)改變,就不會再變,任何時候都可以得到這個結(jié)果。
  3. 如果executor中代碼拋出了錯誤,promise 狀態(tài)為rejected;
  4. executor函數(shù)的返回值被忽略。

狀態(tài) 一個 Promise有以下幾種狀態(tài):

  • pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
  • fulfilled: 意味著操作成功完成。
  • rejected: 意味著操作失敗。

pending 狀態(tài)的 Promise 對象可能會變?yōu)閒ulfilled 狀態(tài)并傳遞一個值給相應(yīng)的狀態(tài)處理方法,也可能變?yōu)槭顟B(tài)(rejected)并傳遞失敗信息。當(dāng)其中任一種情況出現(xiàn)時,Promise 對象的 then 方法綁定的處理方法(handlers )就會被調(diào)用(then方法包含兩個參數(shù):onfulfilled 和 onrejected,它們都是 Function 類型。當(dāng)Promise狀態(tài)為fulfilled時,調(diào)用 then 的 onfulfilled 方法,當(dāng)Promise狀態(tài)為rejected時,調(diào)用 then 的 onrejected 方法, 所以在異步操作的完成和綁定處理方法之間不存在競爭)。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('fulfilled');
  }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('rejected');
  }, 3000);
});

promise1.then((fulfilled) => {
  console.log(fulfilled);
},(rejected)=>{
    console.log(rejected);
});
promise2.then((fulfilled) => {
  console.log(fulfilled);
},(rejected)=>{
    console.log(rejected);
});


運(yùn)行上面這段代碼,1s后打印fulfilled,3s后打印rejected rejected狀態(tài)的 Promise也可以通過.catch進(jìn)行捕獲,因?yàn)?Promise.prototype.then 和 Promise.prototype.catch 方法返回promise 對象, 所以它們可以被鏈?zhǔn)秸{(diào)用。所以上述代碼可以改為:

promise1.then((fulfilled) => {
  console.log(fulfilled);
}).catch((rejected)=>{
    console.log(rejected);
});
promise2.then((fulfilled) => {
  console.log(fulfilled);
}).catch((rejected)=>{
    console.log(rejected);
});


二.深入理解

1.Promise 是用來管理異步編程的,它本身不是異步的,new Promise的時候會立即把executor函數(shù)執(zhí)行,只不過我們一般會在executor函數(shù)中處理一個異步操作。例如下面一段代碼:

let firstPromise = new Promise(()=>{
    setTimeout(()=>{
      console.log(1)
    },1000)
    console.log(2)
  })
console.log(3) // 2 3 1


2.Promise 采用了回調(diào)函數(shù)延遲綁定技術(shù),在執(zhí)行 resolve 函數(shù)的時候,回調(diào)函數(shù)還沒有綁定,那么只能推遲回調(diào)函數(shù)的執(zhí)行。這具體是啥意思呢?我們先來看下面的例子:

let p1 = new Promise((resolve,reject)=>{
  console.log(1);
  resolve('浪里行舟')
  console.log(2)
})
// then:設(shè)置成功或者失敗后處理的方法
p1.then(result=>{
 //p1延遲綁定回調(diào)函數(shù)
  console.log('成功 '+result)
},reason=>{
  console.log('失敗 '+reason)
})
console.log(3)
// 1
// 2
// 3
// 成功 浪里行舟


new Promise的時候先執(zhí)行executor函數(shù),打印出 1、2,Promise在執(zhí)行resolve時,觸發(fā)微任務(wù),還是繼續(xù)往下執(zhí)行同步任務(wù), 執(zhí)行p1.then時,存儲起來兩個函數(shù)(此時這兩個函數(shù)還沒有執(zhí)行),然后打印出3,此時同步任務(wù)執(zhí)行完成,最后執(zhí)行剛剛那個微任務(wù),從而執(zhí)行.then中成功的方法。

3.錯誤處理,多個Promise鏈?zhǔn)讲僮鞯腻e誤捕獲可以通過一個catch處理;例如下面一段代碼:

let executor = function(resolve,reject){
    let random = Math.random()
    if(random>0.5){
        resolve()
    }else{
        reject()
    }
}
let p1 = new Promise(executor)
p1.then(resualt=>{
    console.log(1)
    return new Promise(executor)
}).then(resualt=>{
    console.log(2)
    return new Promise(executor)
}).then(resualt=>{
    console.log(3)
    return new Promise(executor)
}).catch((error) => {
  console.log('error', error)
})


這段代碼有四個 Promise 對象,無論哪個對象里面拋出異常,都可以通過最后一個.catch 來捕獲異常,通過這種方式可以將所有 Promise 對象的錯誤合并到一個函數(shù)來處理,這樣就解決了每個任務(wù)都需要單獨(dú)處理異常的問題。

4.常用方法

  • Promise.resolve() Promise.resolve(value)方法返回一個以給定值解析后的Promise 對象。 Promise.resolve()等價于下面的寫法:
Promise.resolve('foo')
// 等價于
new Promise(resolve => resolve('foo'))


Promise.resolve方法的參數(shù)分成四種情況。

(1)參數(shù)是一個 Promise 實(shí)例

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})
p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail


上面代碼中,p1是一個 Promise,3 秒之后變?yōu)閞ejected。p2的狀態(tài)在 1 秒之后改變,resolve方法返回的是p1。由于p2返回的是另一個 Promise,導(dǎo)致p2自己的狀態(tài)無效了,由p1的狀態(tài)決定p2的狀態(tài)。所以,后面的then語句都變成針對后者(p1)。又過了 2 秒,p1變?yōu)閞ejected,導(dǎo)致觸發(fā)catch方法指定的回調(diào)函數(shù)。

(2)參數(shù)不是具有then方法的對象,或根本就不是對象

Promise.resolve("Success").then(function(value) {
 // Promise.resolve方法的參數(shù),會同時傳給回調(diào)函數(shù)。
  console.log(value); // "Success"
}, function(value) {
  // 不會被調(diào)用
});


(3)不帶有任何參數(shù) Promise.resolve()方法允許調(diào)用時不帶參數(shù),直接返回一個resolved狀態(tài)的 Promise 對象。如果希望得到一個 Promise 對象,比較方便的方法就是直接調(diào)用Promise.resolve()方法。

Promise.resolve().then(function () {
  console.log('two');
});
console.log('one');
// one two


(4)參數(shù)是一個thenable對象 thenable對象指的是具有then方法的對象,Promise.resolve方法會將這個對象轉(zhuǎn)為 Promise 對象,然后就立即執(zhí)行thenable對象的then方法。

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});


  • Promise.reject() Promise.reject()方法返回一個帶有拒絕原因的Promise對象。
new Promise((resolve,reject) => {
    reject(new Error("出錯了"));
});
// 等價于
 Promise.reject(new Error("出錯了"));  

// 使用方法
Promise.reject(new Error("BOOM!")).catch(error => {
    console.error(error);
});


值得注意的是,調(diào)用resolve或reject以后,Promise 的使命就完成了,后繼操作應(yīng)該放到then方法里面,而不應(yīng)該直接寫在resolve或reject的后面。所以,最好在它們前面加上return語句,這樣就不會有意外。

new Promise((resolve, reject) => {
  return reject(1);
  // 后面的語句不會執(zhí)行
  console.log(2);
})


  • Promise.all()
var p1 = Promise.resolve(1)
var p2 = Promise.resolve({a:2})
var p3 = new Promise(function(resolve,reject){
    setTimeout(function(){
        resolve(3)
    },3000)
})
Promise.all([p1,p2,p3]).then(result=>{
    // 返回的結(jié)果是按照Array中編寫實(shí)例的順序來
    console.log(result)
})


Promise.all 生成并返回一個新的 Promise 對象,所以它可以使用 Promise 實(shí)例的所有方法。參數(shù)傳遞promise數(shù)組中所有的 Promise 對象都變?yōu)閞esolve的時候,該方法才會返回, 新創(chuàng)建的 Promise 則會使用這些 promise 的值。 如果參數(shù)中的任何一個promise為reject的話,則整個Promise.all調(diào)用會立即終止,并返回一個reject的新的 Promise 對象。

  • Promise.allSettled()

有時候,我們不關(guān)心異步操作的結(jié)果,只關(guān)心這些操作有沒有結(jié)束。這時,ES2020 引入Promise.allSettled()方法就很有用。如果沒有這個方法,想要確保所有操作都結(jié)束,就很麻煩。Promise.all()方法無法做到這一點(diǎn)。

假如有這樣的場景:一個頁面有三個區(qū)域,分別對應(yīng)三個獨(dú)立的接口數(shù)據(jù),使用 Promise.all 來并發(fā)請求三個接口,如果其中任意一個接口出現(xiàn)異常,狀態(tài)是reject,這會導(dǎo)致頁面中該三個區(qū)域數(shù)據(jù)全都無法出來,顯然這種狀況我們是無法接受,Promise.allSettled的出現(xiàn)就可以解決這個痛點(diǎn):

Promise.allSettled([
  Promise.reject({ code: 500, msg: '服務(wù)異常' }),
  Promise.resolve({ code: 200, list: [] }),
  Promise.resolve({ code: 200, list: [] })
]).then(res => {
  console.log(res)
  /*
    0: {status: "rejected", reason: {…}}
    1: {status: "fulfilled", value: {…}}
    2: {status: "fulfilled", value: {…}}
  */
  // 過濾掉 rejected 狀態(tài),盡可能多的保證頁面區(qū)域數(shù)據(jù)渲染
  RenderContent(
    res.filter(el => {
      return el.status !== 'rejected'
    })
  )
})


Promise.allSettled跟Promise.all類似, 其參數(shù)接受一個Promise的數(shù)組, 返回一個新的Promise, 唯一的不同在于, 它不會進(jìn)行短路, 也就是說當(dāng)Promise全部處理完成后,我們可以拿到每個Promise的狀態(tài), 而不管是否處理成功。

  • Promise.race()

Promise.all()方法的效果是"誰跑的慢,以誰為準(zhǔn)執(zhí)行回調(diào)",那么相對的就有另一個方法"誰跑的快,以誰為準(zhǔn)執(zhí)行回調(diào)",這就是Promise.race()方法,這個詞本來就是賽跑的意思。race的用法與all一樣,接收一個promise對象數(shù)組為參數(shù)。

Promise.all在接收到的所有的對象promise都變?yōu)镕ulFilled或者Rejected狀態(tài)之后才會繼續(xù)進(jìn)行后面的處理,與之相對的是Promise.race只要有一個promise對象進(jìn)入FulFilled或者Rejected狀態(tài)的話,就會繼續(xù)進(jìn)行后面的處理。

// `delay`毫秒后執(zhí)行resolve
function timerPromisefy(delay) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(delay);
        }, delay);
    });
}
// 任何一個promise變?yōu)閞esolve或reject的話程序就停止運(yùn)行
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64)
]).then(function (value) {
    console.log(value);    // => 1
});


上面的代碼創(chuàng)建了3個promise對象,這些promise對象會分別在1ms、32ms 和 64ms后變?yōu)榇_定狀態(tài),即FulFilled,并且在第一個變?yōu)榇_定狀態(tài)的1ms后,.then注冊的回調(diào)函數(shù)就會被調(diào)用。

  • Promise.prototype.finally()

ES9 新增 finally() 方法返回一個Promise。在promise結(jié)束時,無論結(jié)果是fulfilled或者是rejected,都會執(zhí)行指定的回調(diào)函數(shù)。這為在Promise是否成功完成后都需要執(zhí)行的代碼提供了一種方式。這避免了同樣的語句需要在then()和catch()中各寫一次的情況。 比如我們發(fā)送請求之前會出現(xiàn)一個loading,當(dāng)我們請求發(fā)送完成之后,不管請求有沒有出錯,我們都希望關(guān)掉這個loading。

this.loading = true
request()
  .then((res) => {
    // do something
  })
  .catch(() => {
    // log err
  })
  .finally(() => {
    this.loading = false
  })


finally方法的回調(diào)函數(shù)不接受任何參數(shù),這表明,finally方法里面的操作,應(yīng)該是與狀態(tài)無關(guān)的,不依賴于 Promise 的執(zhí)行結(jié)果。

三.實(shí)際應(yīng)用

假設(shè)有這樣一個需求:紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重復(fù)亮燈? 三個亮燈函數(shù)已經(jīng)存在:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}


這道題復(fù)雜的地方在于需要“交替重復(fù)”亮燈,而不是亮完一遍就結(jié)束,我們可以通過遞歸來實(shí)現(xiàn):

// 用 promise 實(shí)現(xiàn)
let task = (timer, light) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (light === 'red') {
        red()
      }
      if (light === 'green') {
        green()
      }
      if (light === 'yellow') {
        yellow()
      }
      resolve()
    }, timer);
  })
}
let step = () => {
  task(3000, 'red')
    .then(() => task(1000, 'green'))
    .then(() => task(2000, 'yellow'))
    .then(step)
}
step()


同樣也可以通過async/await 的實(shí)現(xiàn):

//  async/await 實(shí)現(xiàn)
let step = async () => {
  await task(3000, 'red')
  await task(1000, 'green')
  await task(2000, 'yellow')
  step()
}
step()


參考資料 你真的懂Promise嗎

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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