遵循Promises/A+規範,深入分析Promise實現細節 | 通過872測試樣例

語言: CN / TW / HK

前言

本週寫文的核心為 PromisePromise 大家應該都特別熟悉了,Promise 是非同步程式設計的一種解決方案,廣泛用在日常程式設計中。本週小包將圍繞 Promise 原始碼手寫進行寫文,原始碼手寫初步計劃使用三篇文章實現—— 手寫 Promise 之基礎篇,手寫 PromiseresolvePromise 篇,手寫 Promise 之靜態方法篇。

Promises/A+ 規範是 Promise 的實現準則,因此 Promise 手寫系列將遵循 Promises/A+ 規範的思路,以案例和提問方式層層深入,一步一步實現 Promise 封裝。

學習本文,你能收穫:

  • 🌟 理解 Promise A+規範
  • 🌟 理解什麼是 Promise 的值穿透、Promise 鏈式呼叫機制、Promise 註冊多個 then 方法等。
  • 🌟 掌握 Promise 原始碼編寫全過程
  • 🌟 掌握髮布訂閱模式在 Promise 原始碼編寫中的使用

基礎鋪墊

Promise 必定處於下列三種狀態之一:

  • Pending 等待態: 初始狀態,不是成功或失敗狀態。
  • Fulfilled 完成態: 意味著操作成功完成。
  • Rejected 失敗態: 意味著操作成功失敗。
  • promise 處於 Pending 狀態時,可以轉變為 Fulfilled 或者 Rejected

    promise 處於 FulfilledRejected 時,狀態不能再發生改變

那什麼會觸發 promise 中狀態的改變吶?我們來看幾個栗子:

// p1 什麼都不執行且傳入空函式
const p1 = new Promise(() => {});
console.log("p1: ", p1);

// p2 執行 resolve
const p2 = new Promise((resolve, reject) => {
  resolve("success");
});
console.log("p2: ", p2);

// p3 執行 reject
const p3 = new Promise((resolve, reject) => {
  reject("fail");
});
console.log("p3: ", p3);

// p4 丟擲錯誤
const p4 = new Promise((resolve, reject) => {
  throw Error("error");
});
console.log("p4: ", p4);

// p5 先執行 resolve 後執行 reject
const p5 = new Promise((resolve, reject) => {
  resolve("success");
  reject("fail");
});
console.log("p5: ", p5);

// p6 什麼都不執行且不傳參
const p6 = new Promise();
console.log("p6: ", p6);
複製程式碼

我們來看一下輸出結果:

從輸出結果我們可以發現:

  • 建立 promise 物件時,需傳入一個函式(否則會報錯,詳見 p6),並且該函式會立即執行
  • promise 的初始狀態為 Pending(見 p1)
  • 執行 resolve()reject() 可以將 promise 的狀態修改為 FulfilledRejected (見 p2,p3)
  • promise 中丟擲異常,相當於執行 reject (見 p4)
  • promise 狀態轉變只能由 Pending 開始(見 p5)

根據我們對輸出結果的分析,我們來編寫 promise 的第一版程式碼。

實現基礎 promise —— 第一版

promise 建構函式實現

  1. 首先定義 promise 的三種狀態
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";
複製程式碼
  1. 定義 Promise 建構函式,新增必備屬性

Promises/A+ 規範中指出:

  • value 是任意的 JavaScript 合法值(包括 undefined)
  • reason 是用來表示 promise 為什麼被拒絕的原因

我們使用 ES6 class 定義 Promise 類,value/reason 分別賦值為 undefined ,狀態 status 初始為 PENDING

class Promise {
  constructor() {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
  }
}
複製程式碼
  1. 定義 promise 時需要傳入函式 executor
  • executor 有兩個引數,分別為 resolve,reject,且兩個引數都是函式
  • executor 會立即執行
class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    // 定義resolve 和 reject 函式
    const resolve = () => {};
    const reject = () => {};
    // 構造器立即執行
    executor(resolve, reject);
  }
}
複製程式碼
  1. 實現 resolvereject 的功能

promise 狀態為 Pedding 時: resolve 函式可以將 promisePending 轉變為 Fulfilled,並且更新 promisevalue 值。reject 函式可以將 promisePending 轉變為 Rejected,並且更新 promisereason

注意: promise 狀態只能由 Pending -> FulfilledPending -> Rejected

因此在定義 resolvereject 函式時,內部需要先判斷 promise 的狀態,如果狀態為 pending ,才可以更新 value 值和 promise 狀態。

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;

    const resolve = (value) => {
      // 判斷當前的狀態是否為Pending
      // promise狀態轉變只能從 Pending 開始
      if (this.status === PENDING) {
        // 更新 value 值和 promise 狀態
        this.value = value;
        this.status = FULFILLED;
      }
    };
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.reason = reason;
        this.status = REJECTED;
      }
    };

    executor(resolve, reject);
  }
}
複製程式碼

原始碼寫到這裡,小包就產生疑惑了本文第一個疑問,來,上問題。

提問一: resolve/reject 函式為什麼使用箭頭函式定義?

問題答案小包這裡先不講,大家先思考思考,到文末小包一塊回答。

  1. Promise A+ 規範規定,Promise 執行丟擲異常時,執行失敗函式。因此我們需要捕獲 executor 的執行,如果存在異常,執行 reject 函式。

class Promise {
    // ...多餘程式碼先暫省略
    // 捕獲 executor 異常
    try {
      executor(resolve, reject);
    } catch (e) {
      // 當發生異常時,呼叫 reject 函式
      reject(e);
    }
  }
}
複製程式碼

我們實現完了 Promise 的主體部分,下面就來實現 Promise 的另一重要核心 then 方法。

實現 then 方法的基本功能

then 方法的注意事項比較多,咱們一起來閱讀規範順帶舉例說明一下。

  1. promise.then 接受兩個引數:
promise.then(onFulfilled, onRejected);
複製程式碼

定義 then 函式,接收兩個引數

class Promise {
  then (onFulfilled, onRejected) {}
}
複製程式碼
  1. onFulfilledonRejected 是可選引數,兩者如果不是函式,則會忽略掉(真的是簡單的忽略掉嗎?請看下文值穿透)
  2. 如果 onFulfilled 是一個函式,當 promise 狀態為 Fulfilled 時,呼叫 onFulfilled 函式,onRejected 類似,當 promise 狀態為 Rejeted 時呼叫。

我們繼續來看幾個栗子:

// 執行 resolve
const p1 = new Promise((resolve, reject) => {
  resolve(1);
});
p1.then(
  (v) => {
    console.log("onFulfilled: ", v);
  },
  (r) => {
    console.log("onRejected: ", r);
  }
);

// 執行 reject
const p2 = new Promise((resolve, reject) => {
  reject(2);
});
p2.then(
  (v) => {
    console.log("onFulfilled: ", v);
  },
  (r) => {
    console.log("onRejected: ", r);
  }
);

// 丟擲異常
const p3 = new Promise((resolve, reject) => {
  throw new Error("promise執行出現錯誤");
});
p3.then(
  (v) => {
    console.log("onFulfilled: ", v);
  },
  (r) => {
    console.log("onRejected: ", r);
  }
);
複製程式碼

我們來看一下輸出結果:

通過輸出結果,我們可以發現 then 的呼叫邏輯

  • 執行 resolve 後,promise 狀態改變為 FulfilledonFulfilled 函式呼叫,引數值為 value
  • 執行 reject 或 丟擲錯誤,promise 狀態改變為 RejectedonRejected 函式呼叫,引數值為 reason

接下來,我們來分析一下 then 的實現思路。

then 函式中判斷 promise 當前的狀態,如果為 Fulfilled 狀態,執行 onFulfilled 函式;Rejected 狀態,執行 onRejected 函式。實現思路很簡單,那下面咱們就來實現一下。

class Promise {
  then(onFulfilled, onRejected) {
    // 當狀態為 Fulfilled 時,呼叫 onFulfilled函式
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    // 當狀態為 Rejected 時,呼叫 onRejected 函式
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}
複製程式碼

提問二: then 方法執行時 promise 狀態會出現 Pending 狀態嗎

promise 註冊多個 then 方法

我們繼續往下讀規範:

如果一個 promise 呼叫多次 then: 當 promise 狀態為 Fulfilled 時,所有的 onFulfilled 函式按照註冊順序呼叫。當 promise 狀態為 Rejected 時,所有的 onRejected 函式按照註冊順序呼叫。

這個規範講的是什麼意思那?小包來舉個栗子:

const p = new Promise((resolve, reject) => {
  resolve("success");
});

p.then((v) => {
  console.log(v);
});
p.then((v) => {
  console.log(`${v}--111`);
});
複製程式碼

輸出結果:

success;
success---111;
複製程式碼

通過上面的案例,該規範通俗來講: 同一個 promise 可以註冊多個 then 方法,當 promise 完成或者失敗後,對應的 then 方法按照註冊順序依次執行

該規範咱們的程式碼已經可以相容。學到這裡,我們整合一下 Promise 第一版程式碼,並對目前所寫程式碼進行測試。

// promise 三種狀態
// 狀態只能由 PENDING -> FULFILLED/REJECTED
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    // 初始狀態為 Pending
    this.status = PENDING;
    // this指向問題
    const resolve = (value) => {
      // 判斷當前的狀態是否為Pending
      // promise狀態轉變只能從 Pending 開始
      if (this.status === PENDING) {
        // 更新 value 值和 promise 狀態
        this.value = value;
        this.status = FULFILLED;
      }
    };
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.reason = reason;
        this.status = REJECTED;
      }
    };
    try {
      // 捕獲 executor 異常
      executor(resolve, reject);
    } catch (e) {
      // 當發生異常時,呼叫 reject 函式
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    // 當狀態為 Fulfilled 時,呼叫 onFulfilled函式
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    // 當狀態為 Rejected 時,呼叫 onRejected 函式
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
  }
}
複製程式碼

先來測試基礎部分的案例,輸出結果如下:

再來測試同一 Promise 註冊多個 then 方法,輸出結果為

success;
success---111;
複製程式碼

第一版程式碼是可以滿足當前規範的,~~~,放鬆一下,我們來繼續實現。

處理非同步功能——第二版

文章剛開始我們就講過,promise 是非同步程式設計的一種解決方案,那我們來測試一下第一版 Promise 是否可以實現非同步。

const p = new Promise((resolve, reject) => {
  // 使用 setTimeout 模擬一下非同步
  setTimeout(() => {
    resolve("success");
  });
});

p.then((v) => {
  console.log(v);
});
p.then((v) => {
  console.log(`${v}--111`);
});
複製程式碼

沒有任何輸出,可見第一版程式碼到目前是無法實現非同步程式設計的,我們來分析一下原因。

如果 Promise 內部存在非同步呼叫,當執行到 then 函式時,此時由於 resolve/reject 處於非同步回撥之中,被阻塞未能呼叫,因此 promise 的狀態仍為 Pending,第一版 then 回撥中的 onFulfilledonRejected 無法執行。

釋出訂閱模式

為了更好的實現原生 promise 的編寫,在這裡我們插補一點知識。

非同步程式設計中有一個經常使用的思想,叫做釋出訂閱模式。釋出訂閱模式是指基於一個事件(主題)通道,希望接收通知的物件 Subscriber 通過自定義事件訂閱主題,被啟用事件的物件 Publisher 通過釋出主題事件的方式通知各個訂閱該主題的 Subscriber 物件。

釋出訂閱模式中有三個角色,釋出者 Publisher ,事件通道 Event Channel ,訂閱者 Subscriber

光憑藉定義有點難以理解,小包舉一個栗子: 以目前的熱播劇人世間為例,人世間實在太火了,工作時候也安不下心,每天就迫不及待的等人世間更新,想在人世間更新的第一刻就開始看劇,那你應該怎麼做吶?總不能時時刻刻重新整理頁面,監測人世間是否更新。平臺是人性化的,其提供了訊息訂閱功能,如果你選擇訂閱,平臺更新人世間後,會第一時間發訊息通知你,訂閱後,你就可以愉快的追劇了。

那我們要怎麼設計 Promise 的非同步功能吶? 我們把 Promise 的功能按照發布訂閱模式分解一下:

  • then 回撥 onFulfilled/onRejected 函式
  • resolve/reject 函式
  • resolve/reject 函式執行後,promise 狀態改變,then 回撥函式執行

只有當 resolve/reject 函式執行後,對應 onFulfilled/onRejected 才可以執行執行,但由於存在非同步呼叫,resolve/reject 執行晚於 then 函式。因此 onFulfilled/onRejected 就可以理解為訂閱者,訂閱 resolve/reject 函式執行;resolve/reject 是釋出者;Promise 提供事件通道作用,儲存訂閱的 onFulfilled/onRejected由於同一個 promise 物件可以註冊多個 then 回撥,因此 Event Channel 儲存回撥應為陣列格式

因此我們需要修改 resolve/reject 函式的實現,當兩者被呼叫時,同時通知對應訂閱者執行。

非同步實現

  1. Promise 中定義兩個陣列 onFulfilledCallbacksonRejectedCallbacks ,分別用來儲存 then 回撥 onFulfilledonRejected 函式
class Promise {
  // 儲存訂閱的onFulfilled函式和onRejected函式
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];
}
複製程式碼
  1. then 方法執行時,若 Promise 處於 Pending 狀態,將 onFulfilledonRejected 函式分別訂閱至 onFulfilledCallbacksonRejectedCallbacks——等待 resolve/reject 執行(事件釋出)
then(onFulfilled, onRejected) {
    if (this.status === PENDING) {
        // 當promise處於pending狀態時,回撥函式訂閱
        this.onFulfilledCallbacks.push(onFulfilled);
        this.onRejectedCallbacks.push(onRejected);
    }
}
複製程式碼
  1. 呼叫 resolve/reject 時,釋出事件,分別執行對應 onFulfilledCallbacksonRejectedCallbacks 陣列中的函式
// 執行釋出
const resolve = (value) => {
  if (this.status === PENDING) {
    this.value = value;
    this.status = FULFILLED;
    // 依次執行onFulfilled函式
    this.onFulfilledCallbacks.forEach((cb) => cb(this.value));
  }
};
const reject = (reason) => {
  if (this.status === PENDING) {
    this.reason = reason;
    this.status = REJECTED;
    // 依次執行onRejected函式
    this.onRejectedCallbacks.forEach((cb) => cb(this.reason));
  }
};
複製程式碼

我們將上述程式碼進行彙總,形成第二版程式碼,並進行案例測試。

// 非同步呼叫
const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    // 儲存訂閱的onFulfilled函式和onRejected函式
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.value = value;
        this.status = FULFILLED;
        // 當 resolve 函式呼叫時,通知訂閱者 onFulfilled 執行
        this.onFulfilledCallbacks.forEach((cb) => cb(this.value));
      }
    };
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.reason = reason;
        this.status = REJECTED;
        // 當 reject 函式呼叫時,通知訂閱者 onRejected 執行
        this.onRejectedCallbacks.forEach((cb) => cb(this.reason));
      }
    };
    try {
      executor(resolve, reject);
    } catch (e) {
      console.log(e);
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    if (this.status === PENDING) {
      // 當promise處於pending狀態時,回撥函式訂閱
      this.onFulfilledCallbacks.push(onFulfilled);
      this.onRejectedCallbacks.push(onRejected);
    }
  }
}
複製程式碼

使用剛才的案例進行測試,輸出結果為

success
success--111
複製程式碼

上面的案例有些簡單,我們再來測試一個複雜的案例:

console.log(1);
setTimeout(() => {
  console.log(2);
})
const p1 = new Promise((resolve) => {
  console.log(3);
  setTimeout(() => {
    resolve(4);
  })
})
p1.then(v => console.log(v));
console.log(5);
複製程式碼

瀏覽器輸出結果:

第二版程式碼輸出結果:

瀏覽器與第二版輸出的結果是相同的,因此可見目前第二版 Promise 是可以實現非同步功能的。

但真的沒問題嗎?我們把案例稍微修改,去掉 Promise 中的非同步呼叫,看瀏覽器輸出結果是否與第二版相同。

console.log(1);
setTimeout(() => {
  console.log(2);
})
const p1 = new Promise((resolve) => {
  console.log(3);
  resolve(4);
})
p1.then(v => console.log(v));
console.log(5);
複製程式碼

瀏覽器輸出結果:

第二版程式碼輸出結果:

我們可以明顯的發現第二版程式碼與瀏覽器的2 4 輸出是相反的?可見瀏覽器中先執行 then 方法,後執行 setTimeout?

提問三: 為什麼瀏覽器會先執行 then 方法回撥,後執行 setTimeout 那?

鏈式呼叫——第三版

非同步功能實現完畢,我們繼續去實現 then 方法的鏈式呼叫。首先我們繼續去讀規範:

  1. then 方法必須返回一個 promise
promise2 = promise1.then(onFulfilled, onRejected)
複製程式碼

promise2then 函式的返回值,同樣是一個 Promise 物件。

then(onFulfilled, onRejected) {
  // ... 多餘程式碼省略
  cosnt promise2 = new Promise((resolve, reject) => {})
  return promise2;
}
複製程式碼
  1. 如果 onFulfilledonRejected 返回值為 x ,則執行 Promise Resolution Procedure [[Resolve]](promise2, x)(這裡暫且將他理解為執行 promise2 的 resolve(x)函式)

我們來舉栗子理解一下此條規範:

// 案例1 resolve
console.log(new Promise((resolve) => {
    resolve(1)
}).then((x) => x))
// 案例2 reject
console.log(new Promise((resolve, reject) => {
    reject(1)
}).then(undefined,(r) => r))
複製程式碼

咦,怎麼兩者返回結果一樣,明明 promise 中分別執行 resolvereject 函式

我們再來詳讀一遍規範:

  • 如果 onFulfilledonRejected 返回值為 x ——上面兩個函式都 (v) => v,傳入引數值都是 1,因此返回值 x = 1
  • 則執行 promise2resolve(x)函式,然後 then 返回 promise2 物件——因此上面兩個函式都是呼叫 promise2resolve 函式,所以兩者返回值都是處於 fulfilled 狀態的 promise 物件,並且值都為 1

由於我們需要將 onFulfilled/onRejected 函式返回值作為 promise2 resolve 的引數值,因此我們需要將 then 函式整體移動至 promise2 內部。

then (onFulfilled, onRejected) {
  let promise2 = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        // 返回值作為 resolve 的引數值
        let x = onFulfilled(this.value);
        resolve(x);
      }
      if (this.status === REJECTED) {
        let x = onRejected(this.reason);
        resolve(x);
      }
    });
    return promise2;
}
複製程式碼

你以為這樣就能實現這條規範了嗎?NONONO!!!

難點: 同步程式碼上述思路的確可以實現,但設想這樣一個場景,若 Promise 中存在非同步程式碼,非同步邏輯設計是 then 執行時,若 Promise 處於 Pending 狀態,先將 onFulfilled/onRejected 函式訂閱到 onFulfilledCallbacks/onRejectedCallbacks 中,意味著在 then 中此時兩函式不會執行,那麼此我們應該如何獲取兩者的返回值那?

因此我們不能單純的使用 this.onFulfilledCallbacks.push(onFulfilled) 將回調函式壓入事件通道的儲存陣列中,我們對回撥函式做一層封裝,將 promise2resolve 函式和 onFulfilled 封裝在一起,這樣當 onFulfilled 執行時,可以獲取其返回值 x ,返回 fulfilled 狀態的 promise2,具體可以看下面程式碼:

// 使用匿名箭頭函式,保證內部 this 指向
() => {
  // 回撥函式執行,獲取其返回值
  let x = onFulfilled(this.value);
  // 執行 promise2 的 resolve 方法
  resolve(x);
}
複製程式碼

因此 Pending 狀態的程式碼如下:

if (this.status === PENDING) {
  // 使用匿名函式,將 resovle 與 onFulfilled 捆綁在一起
  this.onFulfilledCallbacks.push(() => {
    let x = onFulfilled(this.value);
    resolve(x);
  });
  this.onRejectedCallbacks.push(() => {
    let x = onRejected(this.reason);
    resolve(x);
  });
}
複製程式碼
  1. 如果 onFulfilledonRejected 執行過程中丟擲異常 e ,則呼叫 promise2reject(e),返回 promise2

我們還是舉栗子測試一下:

console.log(new Promise((resolve) => {
    resolve(1)
}).then(()=> {
    throw new Error('resolve err')
}))
console.log(new Promise((resolve, reject) => {
    reject(1)
}).then(undefined,()=> {
    throw new Error('reject err')
}))
複製程式碼

通過輸出結果,我們可以看出當 onFulfilled/onRejected 函式報錯時,promise2 會執行其 reject 函式。因此我們需要給目前的程式碼新增一層異常捕獲,將程式碼修改成如下情況:

then(onFulfilled, onRejected) {
  let p1 = new Promise((resolve, reject) => {
    if (this.status === FULFILLED) {
      // 新增異常捕獲
      try {
        // 返回值作為 resolve 的引數值
        let x = onFulfilled(this.value);
        resolve(x);
      } catch (e) {
        reject(e);
      }
    }
    //... 其餘部分類似
  return promise2;
}
複製程式碼
  1. 如果 onFulfilled 不是函式,且 promise 狀態為 Fulfilled ,那麼 promise2 應該接受同樣的值,同時狀態為 Fulfilled

這個規範是啥意思吶?我們來舉一個栗子:

// 輸出結果 1
const p1 = new Promise((resolve) => {
    resolve(1)
})
p1.then(x => x).then().then().then().then().then(x=> console.log(x))
複製程式碼

上述程式最終輸出結果為 1 ,初次 resolve 傳遞的 value 值為 1,可見當 onFulfilled 不是函式時, promise 值會沿 then 發生傳遞,直到 onFulfilled 為函式。

這也就是 Promise 的值傳遞,當 thenonFulfilled 為非函式時,值會一直傳遞下去,直至遇到函式 onFulfilled

  1. 如果 onRejected 不是函式,且 promise 狀態為Rejected,那麼 promise2 應該接受同樣的原因,同時狀態為 Rejected
// 輸出結果 Error: error at <anonymous>:4:33
const p1 = new Promise((resolve) => {
    reject(1)
})
p1.then(undefined, () => {throw Error('error')}).then().then().then().then().then(x=> console.log(x), (r)=> console.log(r))
複製程式碼

onFulfilled 類似,Promise 同樣提供了對onRejected 函式的相容,會發生錯誤傳遞。

通過第 4 條與第 5 條的案例,我們可以發現,當 onFulfilled/onRejected 為非函式型別,Promise 會分別發生值傳遞和異常傳遞。

我們如何才能連續傳遞值或者異常那?(見下面程式碼)

  • 值傳遞: 值傳遞非常簡單,我們只需要定義一個函式,引數值為 x ,返回值為 x
  • 異常: 定義函式引數值為異常,之後不斷丟擲此異常。
x => x;
e => throw e;
複製程式碼
then(onFulfilled, onRejected) {
  // 判斷引數是否為函式,如果不是函式,使用預設函式替代
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : (e) => {
          throw e;
        };
  let promise2 = new Promise((resolve, reject) => {
  });
  return promise2;
}
複製程式碼

寫到這裡,鏈式呼叫的部分就暫時實現了,我們整合一下第三版 Promise 程式碼。

const PENDING = "PENDING";
const FULFILLED = "FULFILLED";
const REJECTED = "REJECTED";

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.value = value;
        this.status = FULFILLED;
        this.onFulfilledCallbacks.forEach((cb) => cb(this.value));
      }
    };
    const reject = (reason) => {
      if (this.status === PENDING) {
        this.reason = reason;
        this.status = REJECTED;
        this.onRejectedCallbacks.forEach((cb) => cb(this.reason));
      }
    };
    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) => v;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (e) => {
            throw e;
          };
    let promise2 = new Promise((resolve, reject) => {
      if (this.status === FULFILLED) {
        // 新增異常捕獲
        try {
          // 返回值作為 resolve 的引數值
          let x = onFulfilled(this.value);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      }
      if (this.status === REJECTED) {
        try {
          let x = onRejected(this.reason);
          resolve(x);
        } catch (e) {
          reject(e);
        }
      }
      if (this.status === PENDING) {
        // 使用匿名函式,將 resovle 與 onFulfilled 捆綁在一起
        this.onFulfilledCallbacks.push(() => {
          try {
            let x = onFulfilled(this.value);
            resolve(x);
          } catch (e) {
            reject(e);
          }
        });
        this.onRejectedCallbacks.push(() => {
          try {
            let x = onRejected(this.reason);
            resolve(x);
          } catch (e) {
            reject(e);
          }
        });
      }
    });
    return promise2;
  }
}
複製程式碼

我們測試一下是否可以實現鏈式呼叫:

// 輸出結果為 4,可以說明resolve狀態的鏈式呼叫是可行的,並且實現了值傳遞
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  });
});
p1.then((v) => v + 1)
  .then((v) => v * 2)
  .then()
  .then((v) => console.log(v));

// 輸出 Error1,說明鏈式呼叫仍然是成功的。
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(1);
  });
});
p2.then(
  () => {},
  (r) => new Error(r)
).then(
  (v) => console.log("v", v),
  (r) => console.log("r", r)
);
複製程式碼

寫到這裡,第三版程式碼就實現成功了,後面還有最核心的 resolvePromise 部分,該部分比較複雜,因此小包決定專門開一篇文章詳細講述。

問題回答

resolve/reject 函式為什麼使用箭頭函式定義?

一句話解釋: this 指向問題。

我們將其修改為普通函式形式:

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    const resovle = function (value) {
      console.log(this);
      this.value = value;
    }
    const reject = (reason) => {
      console.log(this);
      this.reason = reason;
    }
    executor(resovle, reject)
  }
}

複製程式碼

之後我們分別執行以下程式碼:

var value = 1;
new Promise((resolve, reject) => {
  resolve(100)
})
複製程式碼

    

從結果我們可以發現: this 的輸出結果為 undefined 。因為 resolve 是一個普通函式,在 Promise 中呼叫為預設呼叫,this 非嚴格模式指向 window ,嚴格模式指向 undefinedES6 class 預設為嚴格模式,因此指向 undefined。所以使用普通函式,我們獲取不到 Promise 中的 value 屬性

// 輸出結果 Promise {value: undefined, reason: 200}
var reason = 2;
new Promise((resolve, reject) => {
  reject(200)
})
複製程式碼

reject 使用箭頭函式,箭頭函式自身沒有 this ,因此會沿作用域鏈使用外層作用域的 this所以我們可以獲取到 reason 屬性

then 方法執行時 promise 狀態會出現 pending 狀態嗎

會出現,文章中已經提到了,當 Promise 中存在非同步程式碼時,例如

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  })
})
複製程式碼

為什麼瀏覽器會先執行 then 方法回撥,後執行 setTimeout 那?

這是 JavaScript 的事件機制(Event Loop)導致的,then 回撥為微任務,setTimeout 為巨集任務,當同步程式碼執行完畢後,主程式會先去微任務佇列尋找任務,微任務佇列全部執行完畢,才會執行巨集任務佇列。

最後

如果你覺得此文對你有一丁點幫助,點個贊。或者可以加入我的開發交流群:1025263163相互學習,我們會有專業的技術答疑解惑

如果你覺得這篇文章對你有點用的話,麻煩請給我們的開源專案點點star:http://github.crmeb.net/u/defu不勝感激 !

PHP學習手冊:http://doc.crmeb.com
技術交流論壇:http://q.crmeb.com