手撕遵循 Promise/A+ 規範的 Promise

YanceyOfficial發表於2019-04-18

相比於回撥函式,Promise 解決了 “回撥地獄” 和 “信任問題” 等痛點,並且大大提高了程式碼的可讀性。在現代前端開發中,Promise 幾乎成了處理非同步的首選(雖然還有更方便的 async/await,逃)。這篇文章從 Promise 的思想和執行機制入手,深入理解每個 API,最後手寫一個遵循 Promise/A+ 規範的 Promise 來。

非同步方式

JavaScript 非同步方式共有有下面六種。

  • 事件監聽

  • 回撥函式

  • 釋出/訂閱

  • Promise

  • 生成器

  • async/await

回撥函式

面試中被問到 回撥函式 有什麼缺點,相信你一定不假思索地回答 回撥地獄。的確如此,當我們需要傳送多個非同步請求,並且每個請求之間需要相互依賴時,就會產生回撥地獄。

前段時間寫了一個天氣微信小程式 Natsuha,它獲取天氣的邏輯大致如下(當然真實場景複雜的多)。

  • 首先要獲取使用者的經緯度 (介面 A)

  • 根據經緯度反查城市 (介面 B)

  • 根據城市拿到相應的天氣資訊 (介面 C)

按照回撥的方式去處理這個邏輯,大致會寫成下面的樣子:

ajax(A, () => {
  // 獲取經緯度
  ajax(B, () => {
    // 根據經緯度反查城市
    ajax(C, () => {
      // 根據城市獲取天氣資訊
    });
  });
});
複製程式碼

看起來很醜陋不是嗎?相信大家對回撥函式的缺點大致都瞭解,這裡就不展開,只做個總結。

  • 程式碼邏輯書寫順序與執行順序不一致,不利於閱讀與維護。

  • 非同步操作的順序變更時,需要大規模的程式碼重構。

  • 回撥函式基本都是匿名函式,bug 追蹤困難。

  • 回撥函式是被第三方庫程式碼(如上例中的 ajax )而非自己的業務程式碼所呼叫的,造成了控制反轉(IoC)。

簡單談一談 控制反轉,《你不知道的 JavaScript (中卷)》把回撥函式的最大缺點歸結為 信任問題。例子中 ajax 是一個三方的函式(你完全可以把它想象成 jQuery 的 $.ajax()),我們把自己的業務邏輯,也就是將回撥函式 交給了 ajax 去處理。但 ajax 對我們來說僅僅是一個黑盒,如果 ajax 本身有缺陷的話,我們的回撥函式就處於危險之中,這也就是所謂的“信任問題”。

不過 Promise 的出現解決了這些缺點,它能夠把控制反轉再反轉回來。這樣的話,我們可以不把自己程式的傳給第三方,而是讓第三方給我們提供瞭解其任務何時結束的能力,進而由我們自己的程式碼來決定下一步做什麼。

何為 Promise

《你不知道的 JavaScript (中卷)》舉了一個例子:

我在快餐店點了一個漢堡,並支付了 1.07 美金。這意味著我對某個值(漢堡)發出了請求。

接著收銀員給我一張 取餐單據,它保證了我最終會得到漢堡,因此 取餐單據 就是一個 承諾

在等待取餐的過程中,我可以做點其他的事情,比如刷刷推特,看看 996.icu 今天又漲了多少 star。之所以我可做點兒其他的事情,是因為 取餐單據 代表了我 未來的 漢堡。它在某種意義上已經成了漢堡的 佔位符。從本質上來講,這個 佔位符 使得這個值不再依賴時間,這是一個 未來值

終於,我聽到服務員在喊 250號前來取餐,我就可以拿著 取餐單據 換我的漢堡了。

但是可能還有另一種結果,在我去取餐時,服務員充滿抱歉的告訴我漢堡已經售罄了,除了憤怒,我們還可以看到 未來值 可能成功,也可能失敗。

Promise 基礎知識

Promise 的生命週期

每個 Promise 都會經歷一個短暫的生命週期:先是處於 進行中 (pending),此時操作尚未完成,因此它也是 未處理 (unsettled) 的;一旦非同步操作執行結束,Promise 變成 已處理 (settled) 狀態,此時它會進入到以下兩個狀態中的其中一個:

  • Fulfilled:Promise 非同步操作成功完成

  • Rejected:由於程式錯誤或其他原因,非同步操作未能成功完成

Promise 建構函式

Promise 本身是一個建構函式,它接收一個叫做 executor 的函式,該函式會被傳遞兩個名為 resolve()reject() 的函式作為引數。resolve() 函式在執行器成功時被呼叫,而 reject() 在執行器操作失敗後被呼叫。看下面這個例子。

const fs = require('fs');

const promise = path =>
  // 執行器接收 resolve() 和 reject() 作為引數
  new Promise((resolve, reject) => {
    fs.readFile(__dirname + '/' + path, 'utf-8', (err, data) => {
      if (err) {
        // 失敗時呼叫 reject()
        reject(err);
        return;
      }
      // 成功時時呼叫 resolve()
      resolve(data);
    });
  });
複製程式碼

Promise 的 then 方法

then() 方法接收兩個函式作為引數,第一個作為 完成 時的回撥,第二個作為 拒絕 時的回撥。兩個引數均為可選,因此你可以只監聽 完成,或者只監聽 拒絕。其中當第一個引數為 null,第二個引數為回撥函式時,它意味著監聽 拒絕。在實際應用中,完成拒絕 都應當被監聽。

const promise = new Promise((resolve, reject) => {
  resolve('success');
});

// 監聽完成和拒絕
promise.then(
  res => {
    // 完成
    console.log(res);
  },
  e => {
    // 拒絕
    console.log(e);
  },
);

// 只監聽完成
promise.then(res => {
  console.log(res);
});

// 第一個引數為 null 時意味著拒絕
promise.then(null, res => {
  // 完成
  console.log(res);
});
複製程式碼

Promise 還有兩個方法分別是 catch()finally(),前者用於監聽 拒絕,後者無論成功失敗都會被執行到。鏈式呼叫顯然可讀性更高,所以我們推薦下面這種寫法。

promise
  .then(res => {
    console.log(res);
  })
  .catch(e => {
    console.log(e);
  })
  .finally(() => {
    console.log('無論成功失敗都會執行這句');
  });
複製程式碼

Promise 鏈式呼叫

每次呼叫 then() 或 catch() 方法時都會 建立並返回一個新的 Promise,只有當前一個 Promise 完成或被拒絕後,下一個才會被解決。

看下面這個例子,p.then() 完成後返回第二個 Promise,接著又呼叫了它的 then() 方法,也就是說只有當第一個 Promise 被解決之後才會呼叫第二個 then() 方法的 then()

let p = new Promise((resolve, reject) => {
  resolve(42);
});

p.then(value => {
  console.log(value); // 42
}).then(() => {
  console.log('可以執行到'); // '可以執行到'
});
複製程式碼

將上述示例拆開,看起來是這樣的。呼叫 p1.then() 的結果被儲存到 p2 中,p2.then() 被呼叫來新增最終的 then()

let p1 = new Promise((resolve, reject) => {
  resolve(42);
});

let p2 = p1.then(value => {
  console.log(value);
});

p2.then(() => {
  console.log('可以執行到');
});
複製程式碼

我們通過一個例項來看一下鏈式呼叫。下面是獲取城市天氣的場景:我們首先需要呼叫 getCity 介面來獲取 城市id,接著呼叫 getWeatherById/城市id 來獲取城市的天氣資訊。首先用 Promise 封裝一個原生 Ajax。(敲黑板,面試可能要求手寫)

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject) {
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open('GET', url);
    client.onreadystatechange = handler;
    client.responseType = 'json';
    client.setRequestHeader('Accept', 'application/json');
    client.send();
  });

  return promise;
};

const baseUrl = 'https://5cb322936ce9ce00145bf070.mockapi.io/api/v1';
複製程式碼

通過鏈式呼叫來請求資料,最後別忘了捕獲錯誤。

getJSON(`${baseUrl}/getCity`)
  .then(value => getJSON(`${baseUrl}/getWeatherById/${value.cityId}`))
  .then(value => console.log(value))
  .catch(e => {
    console.log(e);
  });
複製程式碼

捕獲錯誤

當 then() 方法或者 catch() 方法丟擲錯誤時,鏈式呼叫的下一個 Promise 中的 catch() 方法可以通過 catch() 接收這個錯誤。側面來講,異常不一定只發生在 Promise 中,還有可能發生在 then() 或者 catch() 中。

let p1 = new Promise((resolve, reject) => {
  resolve(42);
});

p1.then(value => {
  throw new Error(' `then()` 錯誤');
}).catch(e => {
  console.log(e.message); // ' `then()` 錯誤'
});
複製程式碼

不僅 then() 可以丟擲異常,catch() 也可以丟擲的異常,且可以被下一個 catch() 捕獲。因此,無論如何都應該在 Promise 鏈的末尾留一個 catch() ,以保證能夠正確處理所有可能發生的錯誤。看下面這個例子。

let p1 = new Promise((resolve, reject) => {
  throw new Error('執行器錯誤');
});

p1.catch(e => {
  console.log(e.message); // '執行器錯誤'
  throw new Error(' `catch()` 錯誤');
}).catch(e => {
  console.log(e.message); // ' `catch()` 錯誤'
});
複製程式碼

Promise 鏈的返回值

Promise 鏈的一個重要特性是能從一個 Promise 傳遞資料給下一個 Promise,通過完成處理函式的返回值,來將資料沿著一個鏈傳遞下去。我們看下面這個例子。

function task() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('task');
    }, 1000);
  });
}

task()
  .then(res => {
    console.log(res);
    return 'taskB';
  })
  .then(res => {
    console.log(res);
    return 'taskC';
  })
  .then(res => {
    console.log(res);
    throw new Error();
  })
  .catch(e => {
    console.log(e);
    return 'taskD';
  })
  .then(res => {
    console.log(res);
  });
複製程式碼

Jietu20190415-172853.jpg

執行結果如上圖所示。我們知道,每次呼叫 then() 或者 catch() 都會返回一個新的 Promise 例項,通過指定處理函式的返回值,可以沿著一個鏈繼續傳遞資料。

因此第一個 then() 將 'taskB' 作為下一個 then() 的引數傳遞下去,同樣第二個 then() 將 'taskC' 作為第三個 then() 的引數傳遞下去。

而第三個 then() 裡面丟擲一個異常,上面說到處理函式中的丟擲異常一定會被後面的拒絕處理函式捕獲,所以 catch() 裡能夠列印出上一個 then() 的錯誤。

別忘了 catch() 返回 'taskD' 也可以被最後一個 then() 捕獲。

其他構造方法

Promise.resolve() 和 Promise.reject()

Promise.resolve() 和 Promise.reject() 類似於快捷方式,用來建立一個 已完成已被拒絕 的 promise。此外,Promise.resolve() 還能接受非 Promise 的 thenable 的作為引數,也就是所謂 擁有 then 方法的物件

// p1 和 p2 等價
const p1 = new Promise((resolve, reject) => {
  reject('Oops');
});

const p2 = Promise.reject('Oops');

// p3 和 p4 等價
const p3 = new Promise((resolve, reject) => {
  resolve('Oops');
});

const p4 = Promise.resolve('Oops');
複製程式碼

而對於 Promise.resolve(),它還能接收一個非 Promise 的 thenable 作為引數。它可以建立一個已完成的 Promise,也可以建立一個以拒絕的 Promise。

let thenable1 = {
  then(resolve, reject) {
    resolve(1);
  },
};

let p1 = Promise.resolve(thenable1);

p1.then(value => console.log(value)); // 1

let thenable2 = {
  then(resolve, reject) {
    reject(1);
  },
};

let p2 = Promise.resolve(thenable2);

p2.catch(reason => console.log(reason)); // 1
複製程式碼

Promise.all()

該方法接收單個迭代物件(最常見的就是陣列)作為引數,並返回一個 Promise。這個可迭代物件的元素都是 Promise,只有在它們都完成後,所返回的 Promise 才會被完成。

  • 當所有的 Promise 均為完成態,將會返回一個包含所有結果的陣列。

  • 只要有一個被拒絕,就不會返回陣列,只會返回最先被拒絕的那個 Promise 的原因

let p1 = new Promise((resolve, reject) => {
  resolve(42);
});
let p2 = new Promise((resolve, reject) => {
  reject(43);
});
let p3 = new Promise((resolve, reject) => {
  reject(44);
});

let p4 = new Promise((resolve, reject) => {
  resolve(45);
});

// 全部完成,返回陣列
let p5 = Promise.all([p1, p4]);
p5.then(value => console.log(value)); // [42, 45]

// 只要有一個出錯,就不會返回陣列,且只會返回最先被拒絕的那個 Promise 的原因
let p6 = Promise.all([p1, p2, p3, p4]);
p6.catch(value => console.log(value)); // 43
複製程式碼

Promise.race()

該方法同樣接收單個迭代物件(最常見的就是陣列)作為引數,不同的是,該方法只要檢測到任意一個被解決,該方法就會做出響應。因此一個有趣的例子是把 請求介面 和一個 setTimeout 進行競逐,如果 setTimeout 先做出響應,就證明這個介面請求超時。

const p = Promise.race([
  fetch('/some-api'),
  new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('請求超時')), 3000);
  }),
]);

p.then(value => {
  console.log(value);
}).catch(reason => {
  console.log(reason);
});
複製程式碼

Promise 的侷限性

看起來 Promise 很美好,解決了回撥函式的種種問題,但它也有自己的侷限性。

  • 一旦建立一個 Promise 併為其註冊完成/拒絕處理函式,Promise 將無法被取消。

  • 當處於 pending 狀態時,你無法得知當前進展到哪一塊

  • 因為 Promise 只能被決議一次(完成或拒絕),如果某些事件不斷髮生,stream 模式會更合適。

  • 如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反應到外部。

手撕程式碼

手撕程式碼的之前可以參照一下後面的 Promise A+ 規範翻譯,最好還是自己去官網翻譯一遍,這樣寫起來才會得心應手。下面的程式碼幾乎每句都加了註釋,並且連結到每一條規範。

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class Promise {
  constructor(executor) {
    // state 的初始狀態為等待態
    this.state = PENDING;

    // 成功的值 (1.3)
    this.value = undefined;

    // 失敗的原因 (1.5)
    this.reason = undefined;

    // 因為 then 在相同的 promise 可以被呼叫多次,所以需要將所有的 onFulfilled 存到陣列 (2.2.6)
    this.onResolvedCallbacks = [];

    // 因為 then 在相同的 promise 可以被呼叫多次,所以需要將所有的 onRejected 存到陣列 (2.2.6)
    this.onRejectedCallbacks = [];

    const resolve = value => {
      // 只有當前是 pending,才可能轉換為 fulfilled
      // 並且不能再轉換成其他任何狀態,且必須擁有一個不可變的值
      if (this.state === PENDING) {
        this.state = FULFILLED;
        this.value = value;
        // onFulfilled 回撥按原始呼叫順序依次執行 (2.2.6.1)
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };

    const reject = reason => {
      // 只有當前是 pending,才可能轉換為 rejected
      // 並且不能再轉換成其他任何狀態,且必須擁有一個不可變的原因
      if (this.state === PENDING) {
        this.state = REJECTED;
        this.reason = reason;
        // onRejectec 回撥按原始呼叫順序依次執行 (2.2.6.1)
        this.onRejectedCallbacks.forEach(fn => fn()); // (2.2.6.2)
      }
    };

    // 若 executor 報錯,直接執行 reject()
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    // onFulfilled 和 onRejected 都是可選引數 (2.2.1)

    // 如果 onFulfilled 不是函式,則必須將它忽略 (2.2.1.1)
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value => value;

    // 如果 onRejected 不是函式,則必須將它忽略 (2.2.1.2)
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : err => {
            throw err;
          };

    // 為了做到鏈式呼叫,規定每個 then 方法必須返回一個 promise,稱為 promise2
    const promise2 = new Promise((resolve, reject) => {
      // 在 promise 完成後方可呼叫 onFulfilled (2.2.2)
      if (this.state === FULFILLED) {
        // onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
        setTimeout(() => {
          try {
            // value 作為完成函式的第一個引數 (2.2.2.1)
            // onFulfilled 函式被記做 x (2.2.7.1)
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            // 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
            reject(e);
          }
        }, 0);
      }

      // 在 promise 被拒絕後方可呼叫 onRejected (2.2.3)
      if (this.state === REJECTED) {
        // onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
        setTimeout(() => {
          try {
            // reason 作為拒絕函式的第一個引數 (2.2.3.1)
            // onRejected 函式被記做 x (2.2.7.1)
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            // 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
            reject(e);
          }
        }, 0);
      }

      if (this.state === PENDING) {
        this.onResolvedCallbacks.push(() => {
          // onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              // 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          // onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              // 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
              reject(e);
            }
          }, 0);
        });
      }
    });

    // 返回 promise2 (2.2.7)
    return promise2;
  }

  // catch 實際是 then 的語法糖
  catch(fn) {
    return this.then(null, fn);
  }

  finally(fn) {
    return this.then(
      value => Promise.resolve(fn()).then(() => value),
      reason =>
        Promise.resolve(fn()).then(() => {
          throw reason;
        }),
    );
  }
}

const resolvePromise = (promise2, x, resolve, reject) => {
  // 如果 promise 和 x 指向同一個物件,將以 TypeError 作為拒因拒絕執行 promise (2.3.1)
  if (x === promise2) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  // onFulfilled 和 onRejected 只能被呼叫一次,因此這裡加一個 flag 作為判斷 (2.2.2.3 & 2.2.3.3)
  let isCalled = false;

  // 如果 x 是一個物件或者是一個函式 (2.3.3)
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      // (2.3.3.1)
      const then = x.then;

      // 如果 then 是函式,就以 x 作為 this 呼叫它 (2.3.3.2 & 2.3.3.3)
      if (typeof then === 'function') {
        // 後面接收兩個回撥,第一個是成功的回撥,第二個是失敗的回撥 (2.3.3.3)
        then.call(
          x,
          y => {
            if (isCalled) return;
            isCalled = true;
            // 如果 resolvePromise 以 y 為引數被呼叫,執行 [[Resolve]](promise, y) (2.3.3.3.1)
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (isCalled) return;
            isCalled = true;
            // 如果 rejectPromise 以 r 為原因被呼叫,則以拒因 r 拒絕 promise (2.3.3.3.2)
            reject(r);
          },
        );
      } else {
        // 如果 then 不是個函式,則以 x 為引數執行 promise (2.3.3.4)
        resolve(x);
      }
    } catch (e) {
      if (isCalled) return;
      isCalled = true;
      // 如果取 x.then 報錯,則以 e 為拒因拒絕 `promise` (2.3.3.2)
      reject(e);
    }
  }
  // 如果 then 不是個函式或者物件,則以 x 為引數執行 promise (2.3.4)
  else {
    resolve(x);
  }
};

// Promise.resolve
Promise.resolve = function(promises) {
  if (promises instanceof Promise) {
    return promises;
  }
  return new Promise((resolve, reject) => {
    if (promises && promises.then && typeof promises.then === 'function') {
      setTimeout(() => {
        promises.then(resolve, reject);
      });
    } else {
      resolve(promises);
    }
  });
};

// Promise.reject
Promise.reject = reason => new Promise((resolve, reject) => reject(reason));

// Promise.all
Promise.all = promises => {
  return new Promise((resolve, reject) => {
    let resolvedCounter = 0;
    let promiseNum = promises.length;
    let resolvedValues = new Array(promiseNum);
    for (let i = 0; i < promiseNum; i += 1) {
      (i => {
        Promise.resolve(promises[i]).then(
          value => {
            resolvedCounter++;
            resolvedValues[i] = value;
            if (resolvedCounter === promiseNum) {
              return resolve(resolvedValues);
            }
          },
          reason => {
            return reject(reason);
          },
        );
      })(i);
    }
  });
};

//race方法
Promise.race = promises => {
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      return;
    } else {
      for (let i = 0, l = promises.length; i < l; i += 1) {
        Promise.resolve(promises[i]).then(
          data => {
            resolve(data);
            return;
          },
          err => {
            reject(err);
            return;
          },
        );
      }
    }
  });
};
複製程式碼

最後全域性安裝 yarn global add promises-aplus-tests,插入下面這段程式碼,然後使用 promises-aplus-tests 該檔案的檔名 來驗證你手寫的 Promise 是否符合 Promises A+ 規範。

Promise.defer = Promise.deferred = function() {
  let dfd = {};
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};
module.exports = Promise;
複製程式碼

附錄:[全文翻譯] Promises/A+ 規範

一個開放、可靠且通用的 JavaScript Promise 標準。由開發者制定,供開發者參考。

promise 代表著一個非同步操作的最終結果,與之互動的主要方式是它的 then 方法,該方法註冊了兩個回撥函式,用於接收 promise 最終的值或者失敗的原因。

該規範詳細描述了 then 方法的行為,所有遵循 Promises/A+ 規範實現的 promise 均可以本標準作為參照基礎來實施。因此,這份規範是很穩定的。雖然 Promises/A+ 組織偶爾會修訂這份規範,但大多是為了處理一些特殊的邊界情況。這些改動都是微小且向下相容的。如果我們要進行大規模不相容的更新,我們一定會在事先進行謹慎地考慮、詳盡的探討和嚴格的測試。

最後,核心的 Promises/A+ 規範不會提供如何建立、解決和拒絕 promise,而是專注於提供一個通用的 then 方法。上述對於 promises 的操作方法將來在其他規範中可能會提及。

1. 術語

1.1. 'promise' 是一個擁有 then 方法的物件或者函式,且其行為符合此規範。

1.2. 'thenable' 是一個用來定義 then 方法的物件或者函式。

1.3. 'value' 是任何一個合法的 JavaScript 值 (包括 undefined,thenable 或者 promise)

1.4. 'exception' 是一個使用 throw 語句丟擲的值

1.5. 'reason' 表明了一個 promise 為什麼會被拒絕

2. 要求

2.1. Promise 狀態

promise 必須是三個狀態之一:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。

  • 2.1.1. 當前狀態為 pending 時,一個 promise:

    • 2.1.1.1 可以轉換成 fulfilled 或者 rejected 狀態
  • 2.1.2. 當前狀態為 fulfilled 時,一個 promise:

    • 2.1.2.1 不能再轉換成其他任何狀態

    • 2.1.2.2 必須擁有一個不可變的值

  • 2.1.3. 當前狀態為 rejected 時,一個 promise:

    • 2.1.3.1 不能再轉換成其他任何狀態

    • 2.1.3.2 必須擁有一個不可變的原因

這裡的不可變指的是恆等(即可用 === 判斷相等),而不是意味著更深層次的不可變。(即當 value 或者 reason 為引用型別時,只要求引用地址相等即可,但屬性值可以被修改)

2.2. then 方法

promise 必須提供一個 then 方法以訪問它當前或最終的值或被拒絕的原因。

一個 promise 的 then 方法接收兩個引數:

promise.then(onFulfilled, onRejected);
複製程式碼
  • 2.2.1 onFulfilledonRejected 都是可選引數。

    • 2.2.1.1 如果 onFulfilled 不是個函式,它將被忽略

    • 2.2.1.2 如果 onRejected 不是個函式,它將被忽略

  • 2.2.2 如果 onFulfilled 是一個函式:

    • 2.2.2.1 它必須在 promise 完成式後被呼叫,並且以 promise 的值作為它的第一個引數。

    • 2.2.2.2 在 promise 未完成前不可呼叫

    • 2.2.2.3 此函式僅可呼叫一次

  • 2.2.3 如果 onRejected 是一個函式:

    • 2.2.3.1 它必須在 promise 被拒絕後被呼叫,並且以 promise 的原因作為它的第一個引數。

    • 2.2.3.2 在 promise 未被拒絕前不可呼叫

    • 2.2.3.3 此函式僅可呼叫一次

  • 2.2.4 onFulfilledonRejected 只有在 執行上下文 堆疊僅包含平臺程式碼時才可被呼叫。[1]

  • 2.2.5 onFulfilledonRejected 必須被作為函式呼叫 (即沒有 this 值)。[2]

  • 2.2.6 then 在相同的 promise 可以被呼叫多次

    • 2.2.6.1 當 promise 是完成態, 所有相應的 onFulfilled 回撥必須按其原始呼叫的順序執行。

    • 2.2.6.2 當 promise 是拒絕態,所有相應的 onRejected 回撥必須按其原始呼叫的順序執行。

  • 2.2.7 每個 then 方法必須返回一個 promise [3]

    promise2 = promise1.then(onFulfilled, onRejected);
    複製程式碼
    • 2.2.7.1 如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程:[[Resolve]](promise2, x)

    • 2.2.7.2 如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e

    • 2.2.7.3 如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值

    • 2.2.7.4 如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的拒因

2.3. Promise 解決過程

Promise 解決過程是一個抽象的操作,它接收一個 promise 和一個值,我們可以表示為 [[Resolve]](promise, x),如果 x 是一個 thenable 的物件,解決程式將試圖接受 x 的狀態,否則用 x 的值來執行 promise

這種對 thenales 的處理使得 promise 的實現更加有普適性,只要它暴露出一個相容 Promises/A+ 規範的 then 方法。它還允許讓遵循 Promise/A+ 規範的實現和不太規範但可用的實現良好共存。

為了執行 [[Resolve]](promise, x),要執行下面的步驟:

  • 2.3.1 如果 promisex 指向同一個物件,將以 TypeError 作為拒因拒絕執行 promise

  • 2.3.2 如果 x 是一個 promise,那麼將 promise 將接受它的狀態 [4]

    • 2.3.2.1 如果 x 是等待態,promise 必須保留等待狀態直到 x 被完成或者被拒絕。

    • 2.3.2.2 如果 x 是完成態,用相同的值執行 promise

    • 2.3.2.3 如果 x 是拒態,用相同的原因拒絕 promise

  • 2.3.3 如果 x 是一個物件或者是一個函式,

    • 2.3.3.1 把 x.then 賦值給 then[5]

    • 2.3.3.2 如果取 x.then 的值時丟擲錯誤 e,則以 e 為拒因拒絕 promise

    • 2.3.3.3 如果 then 是函式,將 x 作為函式的作用域 this 來呼叫它。傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise,第二個引數叫做 rejectPromise:

      • 2.3.3.3.1 如果 resolvePromisey 為引數被呼叫,執行 [[Resolve]](promise, y)

      • 2.3.3.3.2 如果 rejectPromiser 為原因被呼叫,則以拒因 r 拒絕 promise

      • 2.3.3.3.3 如果 resolvePromiserejectPromise 都被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫。

      • 2.3.3.3.4 如果呼叫 then 丟擲一個異常 e

        • 2.3.3.3.4.1 如果 resolvePromiserejectPromise 都被呼叫,則忽略掉它

        • 2.3.3.3.4.2 否則,以 e 為拒因拒絕這個 promise

    • 2.3.3.4 如果 then 不是個函式,則以 x 為引數執行 promise

  • 2.3.4 如果 then 不是個函式或者物件,則以 x 為引數執行 promise

如果一個 promise 被一個迴圈的 thenable 鏈中的物件解決,而 [[Resolve]](promise, thenable) 的遞迴性質又使得其被再次呼叫,根據上述的演算法將會陷入無限遞迴之中。演算法雖不強制要求,但也鼓勵施者檢測這樣的遞迴是否存在,若檢測到存在則以一個可識別的 TypeError 為拒因來拒絕 promise [6]

3. 註釋


  1. 這裡的“平臺程式碼”意味著引擎,環境和 promise 實施程式碼,在實踐中要確保 onFulfilledonRejected 非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。這個事件佇列可以採用“巨集任務(macro-task)”機制,類似於 setTimeOut 或者 setImmediate,也可以使用“微任務(micro-task)”機制來實現,類似於 MutationObserverprocess.nextTick。因為 promise 實現被認為是平臺程式碼,所以它本身可能包含一個任務排程佇列或跳板,在其中呼叫處理程式。 ↩︎

  2. 在嚴格模式下 thisundefined,而在非嚴格模式中,this 為全域性物件。 ↩︎

  3. 程式碼實現在滿足所有要求的情況下可以允許 promise2 === promise1 。每個實現都要文件說明其是否允許以及在何種條件下允許 promise2 === promise1↩︎

  4. 總體來說,如果 x 符合當前實現,我們才認為它是真正的 promise 。這一規則允許那些特例實現接受符合已知要求的 Promises 狀態。 ↩︎

  5. 這步我們先是儲存了一個指向 x.then 的引用,然後測試並呼叫該引用,以避免多次訪問 x.then 屬性。這種預防措施確保了該屬性的一致性,因為其值可能在檢索呼叫時被改變。 ↩︎

  6. 實現不應該對 thenable 鏈的深度設限,並假定超出本限制的遞迴就是無限迴圈。只有真正的迴圈遞迴才應能導致 TypeError 異常;如果一條無限長的鏈上 thenable 均不相同,那麼遞迴下去永遠是正確的行為。 ↩︎

相關文章