Promise總結

更好的閱度體驗

  • 前言
  • API
  • Promise特點
  • 狀態跟隨
  • V8中的async await和Promise
  • 實現一個Promise
  • 參考

前言

作爲一個前端開發,使用了Promise一年多了,一直以來都停留在API的調用階段,沒有很好的去深入。剛好最近閱讀了V8團隊的一篇如何實現更快的async await,藉着這個機會整理了Promise的相關理解。文中如有錯誤,請輕噴~

API

Promise是社區中對於異步的一種解決方案,相對於回調函數和事件機制更直觀和容易理解。ES6 將其寫進了語言標準,統一了用法,提供了原生的Promise對象。

這裏只對API的一些特點做記錄,如果需要詳細教程,推薦阮老師的Promise對象一文

new Promise
--創建一個promise實例

Promise.prototype.then(resolve, reject)
--then方法返回一個新的Promise實例

Promise.prototype.catch(error)
--.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數。
--錯誤會一直傳遞,直到被catch,如果沒有catch,則沒有任何反應
--catch返回一個新的Promise實例

Promise.prototype.finally()
--指定不管 Promise 對象最後狀態如何,都會執行的操作。
--實現如下:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

Promise.all([promise Array])
--將多個 Promise 實例,包裝成一個新的 Promise 實例
--所有子promise執行完成後,才執行all的resolve,參數爲所有子promise返回的數組
--某個子promise出錯時,執行all的reject,參數爲第一個被reject的實例的返回值
--某個子promise自己catch時,不會傳遞reject給all,因爲catch重新返回一個promise實例

Promise.race([promise Array])
--將多個 Promise 實例,包裝成一個新的 Promise 實例。
--子promise有一個實例率先改變狀態,race的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給race的回調函數。

Promise.resolve()
--將現有對象轉爲 Promise 對象
--參數是promise實例, 原封不動的返回
--參數是一個thenable對象 將這個對象轉爲 Promise 對象,狀態爲resolved
--參數是一個原始值 返回一個新的 Promise 對象,狀態爲resolved
--不帶有任何參數 返回一個resolved狀態的 Promise 對象。
--等價於如下代碼

Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))

Promise.reject()
--返回一個新的 Promise 實例,該實例的狀態爲rejected
--Promise.reject()方法的參數,會原封不動地作爲reject的理由,變成後續方法的參數。

Promise特點

很多文章都是把resolve當成fulfilled,本文也是,但本文還有另外一個resolved,指的是該Promise已經被處理,注意兩者的區別  

1. 對象具有三個狀態,分別是pending(進行中)、fulfilled(resolve)(已成功)、reject(已失敗),並且對象的狀態不受外界改變,只能從pending到fulfilled或者pending到reject。  

2. 一旦狀態被改變,就不會再變,任何時候都能得到這個結果,與事件回調不同,事件回調在事件過去後無法再調用函數。  

3. 一個promise一旦resolved,再次resolve/reject將失效。即只能resolved一次。  

4. 值穿透,傳給then或者catch的參數爲非函數時,會發生穿透(下面有示例代碼)  

5. 無法取消,Promise一旦運行,無法取消。  

6. 如果不設置回調函數,Promise內部拋出的錯誤,不會反應到外部  

7. 處於pending時,無法感知promise的狀態(剛剛開始還是即將完成)。

值穿透代碼:

new Promise(resolve=>resolve(8))
  .then()
  .then()
  .then(function foo(value) {
    console.log(value)  // 8
  })

狀態追隨

狀態追隨的概念和下面的v8處理asyac await相關聯

狀態跟隨就是指將一個promise(代指A)當成另外一個promise(代指B)的resolve參數,即B的狀態會追隨A。
如下代碼所示:

const promiseA = new Promise((resolve) => {
  setTimeout(() => {
    resolve('ccc')
  }, 3000)
})
const promiseB = new Promise(res => {
  res(promiseA)
})
promiseB.then((arg) => {
  console.log(arg) // print 'ccc' after 3000ms
})

按理說,promiseB應該是已經處於resolve的狀態, 但是依然要3000ms後纔打印出我們想要的值, 這難免讓人困惑

在ES的標準文檔中有這麼一句話可以幫助我們理解:

A resolved promise may be pending, fulfilled or rejected.

就是說一個已經處理的promise,他的狀態有可能是pending, fulfilled 或者 rejected。 這與我們前面學的不一樣啊, resolved了的promise不應該是處於結果狀態嗎?這確實有點反直覺,結合上面的例子看,當處於狀態跟隨時,即使promiseB立即被resolved了,但是因爲他追隨了promiseA的狀態,而A的狀態則是pending,所以才說處於resolved的promiseB的狀態是pending。

再看另外一個例子:

const promiseA = new Promise((resolve) => {
  setTimeout(() => {
    resolve('ccc')
  }, 3000)
})
const promiseB = new Promise(res => {
  setTimeout(() => {
    res(promiseA)
  }, 5000)
})
promiseB.then((arg) => {
  console.log(arg) // print 'ccc' after 5000ms
})

其實理解了上面的話,這一段的代碼也比較容易理解,只是因爲自己之前進了牛角尖,所以特意記錄下來:

  1. 3s後 promiseA狀態變成resolve
  2. 5s後 promiseB被resolved, 追隨promiseA的狀態
  3. 因爲promiseA的狀態爲resolve, 所以打印 ccc

V8中的async await和Promise

在進入正題之前,我們可以先看下面這段代碼:

const p = Promise.resolve();

(async () => {
  await p;
  console.log("after:await");
})();

p.then(() => {
  console.log("tick:a");
}).then(() => {
  console.log("tick:b");
});

V8團隊的博客中, 說到這段代碼的運行結果有兩種:

Node8(錯誤的):

tick a  
tick b  
after: await

Node10(正確的):

after await  
tick a  
tick b

ok, 問題來了, 爲啥是這個樣子?
先從V8對於await的處理說起, 這裏引用一張官方博客的圖來說明Node8 await的僞代碼:

Node8 await

image

對於上面的例子代碼翻譯過來就(該代碼引用自V8是怎麼實現更快的async await)是:

const p = Promise.resolve();

(() => {
  const implicit_promise = new Promise(resolve => {
    const promise = new Promise(res => res(p));
    promise.then(() => {
      console.log("after:await");
      resolve();
    });
  });

  return implicit_promise;
})();

p.then(() => {
  console.log("tick:a");
}).then(() => {
  console.log("tick:b");
});

很明顯,內部那一句 new Promise(res => res(p)); 代碼就是一個狀態跟隨,即promise追隨p的狀態,那這跟上面的結果又有什麼關係?

在繼續深入之前, 我們還需要了解一些概念:

task和microtask

JavaScript 中有 task 和 microtask 的概念。 Task 處理 I/O 和計時器等事件,一次執行一個。 Microtask 爲 async/await 和 promise 實現延遲執行,並在每個任務結束時執行。 總是等到 microtasks 隊列被清空,事件循環執行纔會返回。

如官方提供的一張圖:
image

EnqueueJob、PromiseResolveThenableJob和PromiseReactionJob

EnquequeJob: 存放兩種類型的任務, 即PromiseResolveThenableJob和PromiseReactionJob, 並且都是屬於microtask類型的任務

PromiseReactionJob: 可以通俗的理解爲promise中的回調函數

PromiseResolveThenableJob(promiseToResolve, thenable, then): 創建和 promiseToResolve 關聯的 resolve function 和 reject function。以 then 爲調用函數,thenable 爲this,resolve function和reject function 爲參數調用返回。(下面利用代碼講解)

狀態跟隨的內部

再以之前的代碼爲例子

const promiseA = new Promise((resolve) => {
  resolve('ccc')
})
const promiseB = new Promise(res => {
  res(promiseA)
})

當promiseB被resolved的時候, 也就是將一個promise(代指A)當成另外一個promise(代指B)的resolve參數,會向EnquequeJob插入一個PromiseResolveThenableJob任務,PromiseResolveThenableJob大概做了如下的事情:

() => { 
  promiseA.then(
    resolvePromiseB, 
    rejectPromiseB
  );
}

並且當resolvePromiseB執行後, promiseB的狀態才變成resolve,也就是B追隨A的狀態

Node 8中的流程

1. p處於resolve狀態,promise調用then被resolved,同時向microtask插入任務PromiseResolveThenableJob  
2. p.then被調用, 向microtask插入任務tickA  
3. 執行PromiseResolveThenableJob, 向microtask插入任務resolvePromise(如上面的promiseA.then(...))  
4. 執行tickA(即輸出tick: a),返回一個promise, 向microtask插入任務tickB  
5. 因爲microtask的任務還沒執行完, 繼續  
6. 執行resolvePromise, 此時promise終於變成resolve, 向microtask插入任務'after await'  
7. 執行tickB(即輸出tick: b)  
8. 執行'after await'(即輸出'after await')

Node 10 await

老規矩, 先補一張僞代碼圖:
image
翻譯過來就是醬紫:

const p = Promise.resolve();

(() => {
  const implicit_promise = new Promise(resolve => {
    const promise = Promise.resolve(p)
    promise.then(() => {
      console.log("after:await");
      resolve();
    });
  });

  return implicit_promise;
})();

p.then(() => {
  console.log("tick:a");
}).then(() => {
  console.log("tick:b");
});

因爲p是一個promise, 然後Promise.resolve會直接將P返回,也就是

p === promise // true

因爲直接返回了p,所以省去了中間兩個microtask任務,並且輸出的順序也變得正常,也就是V8所說的更快的async await

實現一個Promise

先實現一個基礎的函數

function Promise(cb) {
  const that = this
  that.value = undefined // Promise的值
  that.status = 'pending' // Promise的狀態
  that.resolveArray = [] // resolve函數集合
  that.rejectArray = []  // reject函數集合

  function resolve(value) {
    if (value instanceof Promise) {
      return value.then(resolve, reject)
    }
    setTimeout(function() {
      if (that.status === 'pending') { // 處於pending狀態 循環調用
        that.value = value
        that.status = 'resolve'
        for(let i = 0; i < that.resolveArray.length; i++) {
          that.resolveArray[i](value)
        }
      }
    })
  }
  function reject(reason) {
    if (reason instanceof Promise) {
      return reason.then(resolve, reject)
    }
    setTimeout(function() {
      if (that.status === 'pending') { // 處於pending狀態 循環調用
        that.value = reason
        that.status = 'reject'
        for(let i = 0; i < that.rejectArray.length; i++) {
          that.rejectArray[i](reason)
        }
      }
    })
  }

  try {
    cb(resolve, reject)
  } catch (e) {
    reject(e)
  }
}
Promise.prototype.then = function(onResolve, onReject) {
  var that = this
  var promise2 // 返回的Promise

  onResolve = typeof onResolve === 'function' ? onResolve : function(v) { return v }  //如果不是函數 則處理穿透值
  onReject = typeof onReject === 'function' ? onReject : function(v) { return v } //如果不是函數 則處理穿透值

  if (that.status === 'resolve') {
    return promise2 = new Promise(function(resolve, reject) {
      setTimeout(function() {
        try {
          const x = onResolve(that.value)
          if (x instanceof Promise) { // 如果onResolved的返回值是一個Promise對象,直接取它的結果做爲promise2的結果
            x.then(resolve, reject)
          } else {
            resolve(x)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }

  if (that.status === 'reject') {
    return promise2 = new Promise(function(resolve, reject) {
      setTimeout(function() {
        try {
          const x = onResolve(that.value)
          if (x instanceof Promise) { // 如果onResolved的返回值是一個Promise對象,直接取它的結果做爲promise2的結果
            x.then(resolve, reject)
          } else {
            reject(x)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }

  if (that.status === 'pending') {
    return promise2 = new Promise(function(resolve, reject) {
      that.resolveArray.push(function(value) {
        try {
          var x = onResolve(value)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })
      that.rejectArray.push(function(reason) {
        try {
          var x = onReject(reason)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }
}
Promise.prototype.catch = function(onReject) {
  return this.then(null, onReject)
}

參考

v8是怎麼實現更快的 await ?深入理解 await 的運行機制
V8中更快的異步函數和promise
剖析Promise內部結構,一步一步實現一個完整的、能通過所有Test case的Promise類
PromiseA+
ES6入門
深入Promise

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章