深入 Promise

創宇前端發表於2018-09-10
> new Promise((resolve, reject) => setTimeout(resolve, 1000, 'foo'))  
> .then(console.log)  
> // foo (1s後)
複製程式碼

在使用 Promise 的時候,我們最簡單的理解與用法就是像上面的程式碼那樣,把非同步結果提供給 resolve 作引數,然後通過給 then 方法傳遞一個自定義函式作為結果處理函式。但 resolve 和 reject 這兩個引數到底是什麼?在這背後,它的基本工作方式到底是怎樣的呢?讓我們從規範的角度來初步瞭解它吧。

參考: ES8 Promise

TL;DR

  • promise 的工作機制與 callback 類似,都採用內部的抽象操作 Job 來實現非同步
  • Promise 建構函式裡的 resolve/reject 函式是內部建立的,在呼叫它們時傳入的引數就是要解析的結果,把它和 promise 已經儲存的使用者傳入的處理函式一起插入到 Job 佇列中。傳入的引數也可以是一個 promise,在 Promise.all/race 的內部就有用到。
  • Promise.prototype.then 根據當前的 promise 的狀態來決定是立即將 promise 中儲存的結果取出並和引數中的處理函式一起直接插入到 Job 佇列中還是先與 promise 關聯起來作為結果處理函式。then 會隱式呼叫 Promise 構建函式構建新的 promise 並返回。
  • Promise.all 先建立一個新的 promise,然後先、初始化一個空的結果陣列和一個計數器來對已經 resolve 的 promise進行計數,之後會進行迭代,對於每個迭代值它都會為其創造一個promise,並設定這個promise的then為向結果陣列裡新增結果以及計數器--,當計數器減至0時就會resolve最終結果。
  • Promise.race 也是會建立一個新的主 promise,之後主要是根據 promise 只能 resolve 一次的限制,對於每個迭代值都會創造另一個promise,先resolve的也就會先被主 promise resolve 返回結果。

new Promise(executor)

首先從 Promise 這個建構函式說起,它是全域性物件的 Promise 屬性的值,這也就是為什麼瀏覽器環境下我們能直接呼叫它的原因,就像 String, Array 這些建構函式一樣。

new Promise(executor)的第一步就像其他建構函式一樣,按照 Promise 的 prototype 來構建一個新物件,並初始化了幾個內部插槽[[PromiseState]][[PromiseResult]][[PromiseFullfillReactions]][[PromiseRejectReactions]][[PromiseIsHandled]]來記錄一些相關的資訊,可以從名字來大致推斷出他們的作用,詳情我們下文再提。這裡它們的初始值除了[[PromiseResult]]依次為 "pending",空 list,空 list,false。

下一步,ES 會根據這個 promise 物件來生成用來resolve promise的 resolve function 和用來 reject promise 的 reject function。然後呼叫 executor,以 resolve functionreject function 為引數,如果在這個過程中出錯了,就直接 reject promise。最後返回 promise。

那什麼又是 resolve,什麼又是 reject 呢。我們知道 Promise 的狀態,也就是[[PromiseState]]有三種值: pending, fullfilled, rejected,用 reject function 就可以 reject promise,把它的狀態從 pending 變為rejected。不過 resolve function 既可以 fullfill promise 來把promise的狀態從 pending 變為 fullfilled,也可以用來 reject promise。

那麼 resolve functionreject function 到底做了些什麼呢?

先來看 reject function ,首先在生成它的時候,會給它初始化[[Promise]][[AlreadyResolved]]插槽,也就是把它和某個 promise 關聯起來。在執行時,會傳入一個引數 reason,並只有當[[AlreadyResolved]]是 false,也就是還沒 resolve 過、狀態為 pending 時,才會呼叫返回 RejectPromise、傳入 promise 和 reason 引數來 reject promise,否則返回 undefined。
RejectPromise(promise, reason),除了把[[PromiseState]]從 pending 變為 rejected 之外,還會把 promise 的結果[[PromiseResult]]的值設為 reason,並會取出 promise 的[[PromiseRejectReactions]]中已存的記錄(相信讀者們已經明白後面還會有一個操作來向這個內部插槽裡存記錄),並用 TriggerPromiseReactions 呼叫這些記錄做後續處理,並傳入 reject 的原因 reason。類似的,resolve function 中用到的 FullfillPromise(promise, value) 操作把 promise 的狀態變為 fulfilled,抽取[[PromiseFullfillReactions]]的值呼叫 TriggerPromiseReactions,並傳入 fulfilled 的結果 value。

TriggerPromiseReactions(reactions, argument) 會呼叫 EnqueueJob("PromiseJobs", PromiseReactionJob, <<reactions, argument>>),待會再詳細說明。

再來看 resolve function,與 reject function 一樣,在生成它時,會把它與某個 promise 關聯起來。在執行時,我們傳入的引數叫做 resolution。如果 promise 已經 resolve 過,就返回 undefined。之後的情況就相對複雜一些了。

  1. 如果使用者把這個 promise 本身傳給了 resolve function 作為引數 resolution,就會建立一個 TypeError,throw 它,並呼叫 RejectPromise,reason 引數為這個 TypeError。
  2. 如果 resolution 的型別不是 Object,就呼叫 FulfillPromise(promise, resolution)
  3. 其餘的情況就是 resolution 是除了自身以外的帶 then 的物件 (Promise) 的情況了。
    • 如果 resolution 是個不帶then的物件,就 RejectPromise
    • 如果有 then 屬性但不能呼叫,也 FulfillPromise, 。
    • 如果有 then 屬性並且可以呼叫,就 EnqueueJob("PromiseJobs", PromiseResolveThenableJob, <<promise, resolution, thenAction>>)

在說明 EnqueueJob 之前,先來看看 Job 是個什麼東西。簡單來說,它就像是回撥的內部實現機制:“當沒有其他 ES 在跑時,初始化並執行自己對應的 ES。“。我們有一個待執行的 FIFO 的 Job 佇列,以及當前的執行環境 running execution context 和 execution context stack,當後兩者均為空時,才會執行 Job 佇列的第一個。

ES 規定實現裡至少要有兩個 Job 佇列,ScriptJobsPromiseJobs。當我們呼叫 EnqueueJob("PromiseJobs", ...)時,也就將要完成的 Job 和它們的引數插入到了 PromiseJobs 這個佇列。可以看到,Promise 下有兩種 Job

  1. PromiseReactionJob(reaction, argument)
    reaction 有三個內部插槽 [[Capability]][[Type]][[Handler]],分別表示 [[關聯的 promise 及相關的resolve function 和 reject function]][[類別]][[handler]]。如果使用者沒有給 handler(undefined),就根據類別是 Fulfill 還是 Reject 來把 argument 當作結果。如果給了 handler,就用它來對 argument 進行進一步處理。最後根據這個結果來用 resolve function 和 reject function 進行處理並返回。
  2. PromiseResolveThenableJob(promiseToResolve, thenable, then)
    建立和 promiseToResolve 關聯的 resolve function 和 reject function。以 then 為呼叫函式,thenable 為this,resolve function和reject function 為引數呼叫返回。

Promise.prototype.then(onfulfilled, onrejected)

首先是建立一個 promiseCapability,它包含了一個新的 promise 和相關聯的 resolve functionreject function。promise 的產生就是像正常使用 Promise 建構函式那樣構建一個 promise,不過傳給建構函式 executor 是內部自動建立的,作用是把 resolve/reject function 記錄到PromiseCapability中。 根據 promiseCapability 和 onfulfilled/onrejected 建立兩個分別用於 fulfill 和 reject 的PromiseReaction,也就是 PromiseJobs 裡最終要執行的操作。 如果當前的 promise(this)是 pending 狀態,就把這兩個 reaction 分別插入到 promise的[[PromiseFulfillReactions]][[PromiseRejectReactions]]佇列中。但如果此時 promise 已經是 fulfilled 或是 rejected 狀態了,就從 promise 的[[PromiseResult]]取出值 result,作為 fulfilled 的結果/reject 的原因,插入到 Job 佇列裡,EnqueueJob("PromiseJobs", PromiseReactionJob, <<reaciton, result>>),最後返回 prjomiseCapability 裡儲存的新 promise。Promise.prototype.catch(onrejected) 就是 Promise.prototype.then(undefined, onrejected)

Promise.resolve(x)

像 then 那樣建立一個 promiseCapability,然後直接呼叫其中的 resolve function 並傳入要解析的值x,最後返回其中的新 promise.

Promise.all(iterable)

Promise.all也會像 then 那樣建立一個 promiseCapability,裡面包含著一個新的 promise 及其關聯的 resolve functionreject function,之後就結合迭代器迴圈:1.如果迭代完了並且計數器為0則呼叫 promiseCapabilityresolve function 來 resolve 結果陣列 2.否則計數器加1,然後取出下一個迭代的值,傳給 Promise.resolve 也構建一個新的 promise,然後內部建立一個 Promise.all Resolve Element Function,傳給這個新 promise 的 then 用來把結果新增到結果陣列並使計數器減一。

Promise.race(iterable)

同樣的,建立一個 promiseCapability,然後進行迭代,用 Promise.resolve 來構建一個新的 promise,之後呼叫這個新 promise 的 then 方法,傳入 promiseCapability 裡的 resolve/reject function,結合之前提到的 promise 只會 resolve 一次,可以看到確實很有 race 的意味。

結語

看到這裡,不知道大家是否對 Promise 有了更深的理解了呢。再往深一步,ES6裡新提出的 async/await 實際上也是應用了 Generator 的思想與 Promise,感興趣的話可以繼續瞭解一下。


文 / Kacxxia

並沒有作者介紹

本文已由作者授權釋出,版權屬於創宇前端。歡迎註明出處轉載本文。本文連結:knownsec-fed.com/2018-08-22-…

想要看到更多來自知道創宇開發一線的分享,請搜尋關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回覆。

深入 Promise

感謝您的閱讀。

相關文章