標籤:

深入 Promise(一)——Promise 實現詳解

目錄:

  1. 深入 Promise(一)——Promise 實現詳解

  2. 深入 Promise(二)——進擊的 Promise

  3. 深入 Promise(三)——命名 Promise

封面來源——ivanjov.com

if (typeof Promise === undefined) {n returnn} n

實現 Promise/A+ 規範的庫有很多,lie 是一個精簡的實現 Promise/A+ 的庫,並且通過了 Promise/A+ 專門的測試集,但 lie 的代碼寫的有點繞,我在 lie 的代碼基礎上進行了修改,使之更容易閱讀和理解,並發布了 appoint 模塊供大家參考。

Promise/A+ 規範

Promise 規範有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升級版 Promise/A+,有興趣的可以去了解下,最終 ES6 中採用了 Promise/A+ 規範。在講解 Promise 實現之前,當然要先了解 Promise/A+ 規範。Promise/A+ 規範參考:

  • 英文版:promisesaplus.com/

  • 中文版:閱讀 : 【翻譯】Promises/A+規範

注意:沒有特殊說明以下 promise 均指代 Promise 實例。

規範雖然不長,但細節也比較多,我挑出幾個要點簡單說明下:

  1. Promise 本質是一個狀態機。每個 promise 只能是 3 種狀態中的一種:pending、fulfilled 或 rejected。狀態轉變只能是 pending -> fulfilled 或者 pending -> rejected。狀態轉變不可逆。
  2. then 方法可以被同一個 promise 調用多次。

  3. then 方法必須返回一個 promise。規範里沒有明確說明返回一個新的 promise 還是復用老的 promise(即 return this),大多數實現都是返回一個新的 promise,而且復用老的 promise 可能改變內部狀態,這與規範也是相違背的。

  4. 值穿透。下面會細講。

從頭實現 Promise

我們知道 Promise 是一個構造函數,需要用 new 調用,並有以下幾個 api:

function Promise(resolver) {}nnPromise.prototype.then = function() {}nPromise.prototype.catch = function() {}nnPromise.resolve = function() {}nPromise.reject = function() {}nPromise.all = function() {}nPromise.race = function() {}n

下面我們以 appoint 為最終目標,開始一步一步構建完整的 Promise 實現。

use strict;nnvar immediate = require(immediate);nnfunction INTERNAL() {}nfunction isFunction(func) {n return typeof func === function;n}nfunction isObject(obj) {n return typeof obj === object;n}nfunction isArray(arr) {n return Object.prototype.toString.call(arr) === [object Array];n}nnvar PENDING = 0;nvar FULFILLED = 1;nvar REJECTED = 2;nnmodule.exports = Promise;nnfunction Promise(resolver) {n if (!isFunction(resolver)) {n throw new TypeError(resolver must be a function);n }n this.state = PENDING;n this.value = void 0;n this.queue = [];n if (resolver !== INTERNAL) {n safelyResolveThen(this, resolver);n }n}n

immediate 是一個將同步轉非同步執行的庫。INTERNAL 就是一個空函數,類似於一些代碼庫中的 noop。定義了 3 個輔助函數:isFunction、isObject 和 isArray。定義了 3 種狀態:PENDING、FULFILLED 和 REJECTED。safelyResolveThen 後面講。promise 內部有三個變數:

  1. state: 當前 promise 的狀態,初始值為 PENDING。狀態改變只能是 PENDING -> FULFILLED 或 PENDING -> REJECTED。
  2. value: 當 state 是 FULFILLED 時存儲返回值,當 state 是 REJECTED 時存儲錯誤。
  3. queue: promise 內部的回調隊列,這是個什麼玩意兒?為什麼是一個數組?

Promise 實現基本原理

先看一段代碼:

var Promise = require(appoint)nvar promise = new Promise((resolve) => {n setTimeout(() => {n resolve(haha)n }, 1000)n})nvar a = promise.then(function onSuccess() {})nvar b = promise.catch(function onError() {})nconsole.dir(promise, { depth: 10 })nconsole.log(promise.queue[0].promise === a)nconsole.log(promise.queue[1].promise === b)n

列印出:

Promise {n state: 0,n value: undefined,n queue:n [ QueueItem {n promise: Promise { state: 0, value: undefined, queue: [] },n callFulfilled: [Function],n callRejected: [Function] },n QueueItem {n promise: Promise { state: 0, value: undefined, queue: [] },n callFulfilled: [Function],n callRejected: [Function] } ] }ntruentruen

可以看出,queue 數組中有兩個對象。因為規範中規定:then 方法可以被同一個 promise 調用多次。上例中在調用 .then 和 .catch 時 promise 並沒有被 resolve,所以將 .then 和 .catch 生成的新 promise(a 和 b) 和正確時的回調(onSuccess 包裝成 callFulfilled)和錯誤時的回調(onError 包裝成 callRejected)生成一個 QueueItem 實例並 push 到 queue 數組裡,所以上面兩個 console.log 列印 true。當 promise 狀態改變時遍歷內部 queue 數組,統一執行成功(FULFILLED -> callFulfilled)或失敗(REJECTED -> callRejected)的回調(傳入 promise 的 value 值),生成的結果分別設置 a 和 b 的 state 和 value,這就是 Promise 實現的基本原理。

再來看另一個例子:

var Promise = require(appoint)nvar promise = new Promise((resolve) => {n setTimeout(() => {n resolve(haha)n }, 1000)n})npromisen .then(() => {})n .then(() => {})n .then(() => {})nconsole.dir(promise, { depth: 10 })n

列印出:

Promise {n state: 0,n value: undefined,n queue:n [ QueueItem {n promise:n Promise {n state: 0,n value: undefined,n queue:n [ QueueItem {n promise:n Promise {n state: 0,n value: undefined,n queue:n [ QueueItem {n promise: Promise { state: 0, value: undefined, queue: [] },n callFulfilled: [Function],n callRejected: [Function] } ] },n callFulfilled: [Function],n callRejected: [Function] } ] },n callFulfilled: [Function],n callRejected: [Function] } ] }n

調用了 3 次 then,每個 then 將它生成的 promise 放到了調用它的 promise 隊列里,形成了 3 層調用關係。當最外層的 promise 狀態改變時,遍歷它的 queue 數組調用對應的回調,設置子 promise 的 state 和 value 並遍歷它的 queue 數組調用對應的回調,然後設置孫 promise 的 state 和 value 並遍歷它的 queue 數組調用對應的回調......依次類推。

safelyResolveThen

function safelyResolveThen(self, then) {n var called = false;n try {n then(function (value) {n if (called) {n return;n }n called = true;n doResolve(self, value);n }, function (error) {n if (called) {n return;n }n called = true;n doReject(self, error);n });n } catch (error) {n if (called) {n return;n }n called = true;n doReject(self, error);n }n}n

safelyResolveThen 顧名思義用來『安全的執行 then 函數』,這裡的 then 函數指『第一個參數是 resolve 函數第二個參數是 reject 函數的函數』,如下兩種情況:

  1. 構造函數的參數,即這裡的 resolver:

    new Promise(function resolver(resolve, reject) {n setTimeout(() => {n resolve(haha)n }, 1000)n})n

  2. promise 的 then:

    promise.then(resolve, reject)n

safelyResolveThen 有 3 個作用:

  1. try...catch 捕獲拋出的異常,如:

    new Promise(function resolver(resolve, reject) {n throw new Error(Oops)n})n

  2. called 控制 resolve 或 reject 只執行一次,多次調用沒有任何作用。即:

    var Promise = require(appoint)nvar promise = new Promise(function resolver(resolve, reject) {n setTimeout(() => {n resolve(haha)n }, 1000)n reject(error)n})npromise.then(console.log)npromise.catch(console.error)n

    列印 error,不會再列印 haha。

  3. 沒有錯誤則執行 doResolve,有錯誤則執行 doReject。

doResolve 和 doReject

function doResolve(self, value) {n try {n var then = getThen(value);n if (then) {n safelyResolveThen(self, then);n } else {n self.state = FULFILLED;n self.value = value;n self.queue.forEach(function (queueItem) {n queueItem.callFulfilled(value);n });n }n return self;n } catch (error) {n return doReject(self, error);n }n}nnfunction doReject(self, error) {n self.state = REJECTED;n self.value = error;n self.queue.forEach(function (queueItem) {n queueItem.callRejected(error);n });n return self;n}n

doReject 就是設置 promise 的 state 為 REJECTED,value 為 error,callRejected 如前面提到的通知子 promise:『我這裡出了點問題呀』然後子 promise 根據傳入的錯誤設置自己的狀態和值。doResolve 結合 safelyResolveThen 使用不斷地解包 promise,直至返回值是非 promise 對象後,設置 promise 的狀態和值,然後通知子 promise:『我這裡有值了喲』然後子 promise 根據傳入的值設置自己的狀態和值。

這裡有個輔助函數 getThen:

function getThen(obj) {n var then = obj && obj.then;n if (obj && (isObject(obj) || isFunction(obj)) && isFunction(then)) {n return function appyThen() {n then.apply(obj, arguments);n };n }n}n

規範中規定:如果 then 是函數,將 x(這裡是 obj) 作為函數的 this 調用。

Promise.prototype.then 和 Promise.prototype.catch

Promise.prototype.then = function (onFulfilled, onRejected) {n if (!isFunction(onFulfilled) && this.state === FULFILLED ||n !isFunction(onRejected) && this.state === REJECTED) {n return this;n }n var promise = new this.constructor(INTERNAL);n if (this.state !== PENDING) {n var resolver = this.state === FULFILLED ? onFulfilled : onRejected;n unwrap(promise, resolver, this.value);n } else {n this.queue.push(new QueueItem(promise, onFulfilled, onRejected));n }n return promise;n};nnPromise.prototype.catch = function (onRejected) {n return this.then(null, onRejected);n};n

上述代碼中的 return this 實現了值穿透,後面會講。可以看出,then 方法中生成了一個新的 promise 然後返回,符合規範要求。如果 promise 的狀態改變了,則調用 unwrap,否則將生成的 promise 加入到當前 promise 的回調隊列 queue 里,之前講解了如何消費 queue。有 3 點需要講解:

  1. Promise 構造函數傳入了一個 INTERNAL 即空函數,因為這個新產生的 promise 可以認為是內部的 promise,需要根據外部的 promise 的狀態和值產生自身的狀態和值,不需要傳入回調函數,而外部 Promise 需要傳入回調函數決定它的狀態和值。所以之前 Promise 的構造函數里做了判斷區分外部調用還是內部調用:

    if (resolver !== INTERNAL) {n safelyResolveThen(this, resolver);n}n

  2. unwrap 代碼如下:

    function unwrap(promise, func, value) {n immediate(function () {n var returnValue;n try {n returnValue = func(value);n } catch (error) {n return doReject(promise, error);n }n if (returnValue === promise) {n doReject(promise, new TypeError(Cannot resolve promise with itself));n } else {n doResolve(promise, returnValue);n }n });n}n

    從名字也可以理解是用來解包(即執行函數)的,第一個參數是子 promise,第二個參數是父 promise 的 then 的回調(onFulfilled/onRejected),第三個參數是父 promise 的值(正常值/錯誤)。有 3 點需要說明:

    1. 使用 immediate 將同步代碼變非同步。如:

      var Promise = require(appoint)nvar promise = new Promise((resolve, reject) => {n setTimeout(() => {n resolve(haha)n }, 1000)n})npromise.then(() => {n promise.then(() => {n console.log(1)n })n console.log(2)n})n

      列印 2 1,去掉 immediate 則列印 1 2。

    2. try...catch 用來捕獲 then/catch 內拋出的異常,並調用 doReject,如:

      promise.then(() => {n throw new Error(haha)n})npromise.catch(() => {n throw new Error(haha)n})n

    3. 返回的值不能是 promise 本身,否則會造成死循環,如 [email protected] 下運行:

      var promise = new Promise((resolve, reject) => {n setTimeout(() => {n resolve(haha)n }, 1000)n})nvar a = promise.then(() => {n return an})nna.catch(console.log)// [TypeError: Chaining cycle detected for promise #<Promise>]n

  3. QueueItem 代碼如下:

    function QueueItem(promise, onFulfilled, onRejected) {n this.promise = promise;n this.callFulfilled = function (value) {n doResolve(this.promise, value);n };n this.callRejected = function (error) {n doReject(this.promise, error);n };n if (isFunction(onFulfilled)) {n this.callFulfilled = function (value) {n unwrap(this.promise, onFulfilled, value);n };n }n if (isFunction(onRejected)) {n this.callRejected = function (error) {n unwrap(this.promise, onRejected, error);n };n }n}n

    promise 為 then 生成的新 promise(以下稱為『子promise』),onFulfilled 和 onRejected 即是 then 參數中的 onFulfilled 和 onRejected。從上面代碼可以看出:比如當 promise 狀態變為 FULFILLED 時,之前註冊的 then 函數,用 callFulfilled 調用 unwrap 進行解包最終得出子 promise 的狀態和值,之前註冊的 catch 函數,用 callFulfilled 直接調用 doResolve,設置隊列里子 promise 的狀態和值。當 promise 狀態變為 REJECTED 類似。

注意:promise.catch(onRejected) 就是 promise.then(null, onRejected) 的語法糖。

至此,Promise 的核心實現都完成了。

值穿透

Promise.prototype.then = function (onFulfilled, onRejected) {n if (!isFunction(onFulfilled) && this.state === FULFILLED ||n !isFunction(onRejected) && this.state === REJECTED) {n return this;n }n ...n};n

上面提到了值穿透問題,值穿透即:

var Promise = require(appoint)nvar promise = new Promise((resolve, reject) => {n setTimeout(() => {n resolve(haha)n }, 1000)n})npromisen .then(hehe)n .then(console.log)n

最終列印 haha 而不是 hehe。

通過 return this 只實現了值穿透的一種情況,其實值穿透有兩種情況:

  1. promise 已經是 FULFILLED/REJECTED 時,通過 return this 實現的值穿透:

    var Promise = require(appoint)nvar promise = new Promise(function (resolve) {n setTimeout(() => {n resolve(haha)n }, 1000)n})npromise.then(() => {n promise.then().then((res) => {// ①n console.log(res)// hahan })n promise.catch().then((res) => {// ②n console.log(res)// hahan })n console.log(promise.then() === promise.catch())// truen console.log(promise.then(1) === promise.catch({ name: nswbmw }))// truen})n

    上述代碼①②處 promise 已經是 FULFILLED 了符合條件所以執行了 return this。注意:原生的 Promise 實現里並不是這樣實現的,所以會列印兩個 false。

  2. promise 是 PENDING 時,通過生成新的 promise 加入到父 promise 的 queue,父 promise 有值時調用 callFulfilled->doResolve 或 callRejected->doReject(因為 then/catch 傳入的參數不是函數)設置子 promise 的狀態和值為父 promise 的狀態和值。如:

    var Promise = require(appoint)nvar promise = new Promise((resolve) => {n setTimeout(() => {n resolve(haha)n }, 1000)n})nvar a = promise.then()na.then((res) => {n console.log(res)// hahan})nvar b = promise.catch()nb.then((res) => {n console.log(res)// hahan})nconsole.log(a === b)// falsen

Promise.resolve 和 Promise.reject

Promise.resolve = resolve;nfunction resolve(value) {n if (value instanceof this) {n return value;n }n return doResolve(new this(INTERNAL), value);n}nnPromise.reject = reject;nfunction reject(reason) {n var promise = new this(INTERNAL);n return doReject(promise, reason);n}n

當 Promise.resolve 參數是一個 promise 時,直接返回該值。

Promise.all

Promise.all = all;nfunction all(iterable) {n var self = this;n if (!isArray(iterable)) {n return this.reject(new TypeError(must be an array));n }nn var len = iterable.length;n var called = false;n if (!len) {n return this.resolve([]);n }nn var values = new Array(len);n var resolved = 0;n var i = -1;n var promise = new this(INTERNAL);nn while (++i < len) {n allResolver(iterable[i], i);n }n return promise;n function allResolver(value, i) {n self.resolve(value).then(resolveFromAll, function (error) {n if (!called) {n called = true;n doReject(promise, error);n }n });n function resolveFromAll(outValue) {n values[i] = outValue;n if (++resolved === len && !called) {n called = true;n doResolve(promise, values);n }n }n }n}n

Promise.all 用來並行執行多個 promise/值,當所有 promise/值執行完畢後或有一個發生錯誤時返回。可以看出:

  1. Promise.all 內部生成了一個新的 promise 返回。
  2. called 用來控制即使有多個 promise reject 也只有第一個生效。
  3. values 用來存儲結果。
  4. 當最後一個 promise 得出結果後,使用 doResolve(promise, values) 設置 promise 的 state 為 FULFILLED,value 為結果數組 values。

Promise.race

Promise.race = race;nfunction race(iterable) {n var self = this;n if (!isArray(iterable)) {n return this.reject(new TypeError(must be an array));n }nn var len = iterable.length;n var called = false;n if (!len) {n return this.resolve([]);n }nn var i = -1;n var promise = new this(INTERNAL);nn while (++i < len) {n resolver(iterable[i]);n }n return promise;n function resolver(value) {n self.resolve(value).then(function (response) {n if (!called) {n called = true;n doResolve(promise, response);n }n }, function (error) {n if (!called) {n called = true;n doReject(promise, error);n }n });n }n}n

Promise.race 接受一個數組,當數組中有一個 resolve 或 reject 時返回。跟 Promise.all 代碼相近,只不過這裡用 called 控制只要有任何一個 promise onFulfilled/onRejected 立即去設置 promise 的狀態和值。

至此,Promise 的實現全部講解完畢。


推薦閱讀:

Koa2 源碼賞析
Node.js 性能調優之內存篇(一)——gcore+llnode
編寫 Node.js Rest API 的 10 個最佳實踐
express4.x Request對象獲得參數方法小談

TAG:Promise | Nodejs |