你能手寫一個Promise嗎?Yes I promise。

webfansplz發表於2019-03-03

前言

[實踐系列] 主要是讓我們通過實踐去加深對一些原理的理解。

[實踐系列]前端路由

[實踐系列]Babel原理

[實踐系列]瀏覽器快取

有興趣的同學可以關注 [實踐系列] 。 求star求follow~

什麼是Promise ?

Promise是JS非同步程式設計中的重要概念,非同步抽象處理物件,是目前比較流行Javascript非同步程式設計解決方案之一

Promises/A+ 規範

為實現者提供一個健全的、可互操作的 JavaScript promise 的開放標準。

術語

  • 解決 (fulfill) : 指一個 promise 成功時進行的一系列操作,如狀態的改變、回撥的執行。雖然規範中用 fulfill 來表示解決,但在後世的 promise 實現多以 resolve 來指代之。

  • 拒絕(reject) : 指一個 promise 失敗時進行的一系列操作。

  • 拒因 (reason) : 也就是拒絕原因,指在 promise 被拒絕時傳遞給拒絕回撥的值。

  • 終值(eventual value) : 所謂終值,指的是 promise 被解決時傳遞給解決回撥的值,由於 promise 有一次性的特徵,因此當這個值被傳遞時,標誌著 promise 等待態的結束,故稱之終值,有時也直接簡稱為值(value)。

  • Promise : promise 是一個擁有 then 方法的物件或函式,其行為符合本規範。

  • thenable : 是一個定義了 then 方法的物件或函式,文中譯作“擁有 then 方法”。

  • 異常(exception) : 是使用 throw 語句丟擲的一個值。

基本要求

下面我們先來講述Promise/A+ 規範的幾個基本要求。

1. Promise的狀態

一個Promise的當前狀態必須是以下三種狀態中的一種: 等待狀態(Pending) 執行狀態(Fulfilled)拒絕狀態(Rejected)。


const PENDING = `pending`;

const FULFILLED = `fulfilled`;

const REJECTED = `rejected`;

複製程式碼

等待狀態 (Pending)

處於等待態時,promise 需滿足以下條件:

  • 可以遷移至執行態或拒絕態
 if (this.state === PENDING) {
     this.state = FULFILLED || REJECTED ;
 }
複製程式碼

執行狀態 (Fulfilled)

處於執行態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態

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

 this.value = value;
複製程式碼

拒絕狀態 (Rejected)

處於拒絕態時,promise 需滿足以下條件:

  • 不能遷移至其他任何狀態

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

 this.reason = reason;
複製程式碼

這裡的不可變指的是恆等(即可用 === 判斷相等),而不是意味著更深層次的不可變(譯者注:蓋指當 value 或 reason 不是基本值時,只要求其引用地址相等,但屬性值可被更改)

2. Then 方法

一個 promise 必須提供一個 then 方法以訪問其當前值、終值和據因。

promise 的 then 方法接受兩個引數:

promise.then(onFulfilled, onRejected)
複製程式碼

引數可選

onFulfilled 和 onRejected 都是可選引數。

  • 如果 onFulfilled 不是函式,其必須被忽略

  • 如果 onRejected 不是函式,其必須被忽略

onFulfilled 特性

如果 onFulfilled 是函式:

  • 當 promise 執行結束後其必須被呼叫,其第一個引數為 promise 的終值

  • 在 promise 執行結束前其不可被呼叫

  • 其呼叫次數不可超過一次

onRejected 特性

如果 onRejected 是函式:

  • 當 promise 被拒絕執行後其必須被呼叫,其第一個引數為 promise 的據因

  • 在 promise 被拒絕執行前其不可被呼叫

  • 其呼叫次數不可超過一次

呼叫時機

onFulfilled 和 onRejected 只有在執行環境堆疊僅包含平臺程式碼時才可被呼叫 注1

注1 這裡的平臺程式碼指的是引擎、環境以及 promise 的實施程式碼。實踐中要確保 onFulfilled 和 onRejected 方法非同步執行,且應該在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。

這個事件佇列可以採用“巨集任務(macro – task)”機制或者“微任務(micro – task)”機制來實現。

由於 promise 的實施程式碼本身就是平臺程式碼(譯者注:即都是 JavaScript),故程式碼自身在處理在處理程式時可能已經包含一個任務排程佇列。

呼叫要求

onFulfilled 和 onRejected 必須被作為函式呼叫(即沒有 this 值)

多次呼叫

then 方法可以被同一個 promise 呼叫多次

  • 當 promise 成功執行時,所有 onFulfilled 需按照其註冊順序依次回撥

  • 當 promise 被拒絕執行時,所有的 onRejected 需按照其註冊順序依次回撥

簡易版實踐

我們先通過實踐一個簡易版的Promise來消化一下上面Promises/A+規範的基本要求。

首先

npm init 

// 測試實現是否符合 promises/A+ 規範

npm install promises-aplus-tests -D 

複製程式碼

package.json

{
  "name": "ajpromise",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "promises-aplus-tests ./simple.js"
  },
  "author": "webfansplz",
  "license": "MIT",
  "devDependencies": {
    "promises-aplus-tests": "^2.1.2"
  }
}
    
複製程式碼

simple.js

//Promise 的三種狀態  (滿足要求 -> Promise的狀態)
const PENDING = `pending`;
const FULFILLED = `fulfilled`;
const REJECTED = `rejected`;

class AjPromise {
  constructor(fn) {
    //當前狀態
    this.state = PENDING;
    //終值
    this.value = null;
    //拒因
    this.reason = null;
    //成功態回撥佇列
    this.onFulfilledCallbacks = [];
    //拒絕態回撥佇列
    this.onRejectedCallbacks = [];

    //成功態回撥
    const resolve = value => {
      // 使用macro-task機制(setTimeout),確保onFulfilled非同步執行,且在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。
      setTimeout(() => {
        if (this.state === PENDING) {
          // pending(等待態)遷移至 fulfilled(執行態),保證呼叫次數不超過一次。
          this.state = FULFILLED;
          // 終值
          this.value = value;
          this.onFulfilledCallbacks.map(cb => {
            this.value = cb(this.value);
          });
        }
      });
    };
    //拒絕態回撥
    const reject = reason => {
      // 使用macro-task機制(setTimeout),確保onRejected非同步執行,且在 then 方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。 (滿足要求 -> 呼叫時機)
      setTimeout(() => {
        if (this.state === PENDING) {
          // pending(等待態)遷移至 fulfilled(拒絕態),保證呼叫次數不超過一次。
          this.state = REJECTED;
          //拒因
          this.reason = reason;
          this.onRejectedCallbacks.map(cb => {
            this.reason = cb(this.reason);
          });
        }
      });
    };
    try {
      //執行promise
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    typeof onFulfilled === `function` && this.onFulfilledCallbacks.push(onFulfilled);
    typeof onRejected === `function` && this.onRejectedCallbacks.push(onRejected);
    // 返回this支援then 方法可以被同一個 promise 呼叫多次
    return this;
  }
}

複製程式碼

就這樣,一個簡單的promise就完成了.

new AjPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(2);
  }, 2000);
})
  .then(res => {
    console.log(res);
    return res + 1;
  })
  .then(res => {
    console.log(res);
  });

//output  

// delay 2s..
//  2 
//  3 
複製程式碼

接下來,我們來看看我們的實現是否完全符合promises/A+規範~

npm run test
複製程式碼

GG,測試用例只過了一小部分,大部分飄紅~

OK,接下來,我們來繼續瞭解promises/A+ 進一步的規範要求~

進一步要求

由於接下來的要求比較抽象和難理解,所以我們將一步一步實踐來加深理解。

1. 返回

  • 1.then方法必須返回一個promise物件

  • 2.如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程:[[Resolve]](promise2, x)

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

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

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

  • 6.不論 promise1 被 reject 還是被 resolve 時 promise2 都會被 resolve,只有出現異常時才會被 rejected。

我們通過以上要求來一步一步完善then方法
1.

// 1.首先,then方法必須返回一個promise物件
  then(onFulfilled, onRejected) {
    let newPromise;
    return (newPromise = new AjPromise((resolve, reject) => {}));
  }
複製程式碼
  then(onFulfilled, onRejected) {
    let newPromise;
    return (newPromise = new AjPromise((resolve, reject) => {
      // 2.如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程:[[Resolve]](promise2, x)
      this.onFulfilledCallbacks.push(value => {
        let x = onFulfilled(value);
        //解決過程 resolvePromise
        resolvePromise(newPromise, x);
      });
      this.onRejectedCallbacks.push(reason => {
        let x = onRejected(reason);
        //解決過程 resolvePromise
        resolvePromise(newPromise, x);
      });
    }));
  }
  // 解決過程
  function resolvePromise() {
  //...
  }

複製程式碼
  then(onFulfilled, onRejected) {
    let newPromise;
    return (newPromise = new AjPromise((resolve, reject) => {
      //  3.如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e。
      this.onFulfilledCallbacks.push(value => {
        try {
          let x = onFulfilled(value);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
      this.onRejectedCallbacks.push(reason => {
        try {
          let x = onRejected(reason);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
    }));
  }
複製程式碼

4,5.

  then(onFulfilled, onRejected) {  
    let newPromise;
    // 4.如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值。
    onFulfilled = typeof onFulfilled === `function` ? onFulfilled : value => value;
    // 5.如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因。
    onRejected =
      typeof onRejected === `function`
        ? onRejected
        : reason => {
            throw reason;
          };
    return (newPromise = new AjPromise((resolve, reject) => {
      this.onFulfilledCallbacks.push(value => {
        try {
          let x = onFulfilled(value);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
      this.onRejectedCallbacks.push(reason => {
        try {
          let x = onRejected(reason);
          resolvePromise(newPromise, x);
        } catch (e) {
          reject(e);
        }
      });
    }));
  }
複製程式碼
  then(onFulfilled, onRejected) {
    let newPromise;

    onFulfilled = typeof onFulfilled === `function` ? onFulfilled : value => value;
    onRejected =
      typeof onRejected === `function`
        ? onRejected
        : reason => {
            throw reason;
          };
    // 2.2.6規範 對於一個promise,它的then方法可以呼叫多次.
    // 當在其他程式中多次呼叫同一個promise的then時 由於之前狀態已經為FULFILLED / REJECTED狀態,則會走以下邏輯,
    // 所以要確保為FULFILLED / REJECTED狀態後 也要非同步執行onFulfilled / onRejected ,這裡使用setTimeout

    // 6.不論 promise1 被 reject 還是被 resolve 時 promise2 都會被 resolve,只有出現異常時才會被 rejected。
    // 由於在接下來的解決過程中需要呼叫resolve,reject進行處理,處理我們在呼叫處理過程時,傳入引數
    if (this.state == FULFILLED) {  
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state == REJECTED) {
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state === PENDING) {
      return (newPromise = new AjPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
  }
複製程式碼

ok,完整的then方法搞定了。相信通過以上實踐,你對返回要求已經有了更深的理解。

2. Promise 解決過程

Promise 解決過程是一個抽象的操作,其需輸入一個 promise 和一個值,我們表示為 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一個 Promise ,解決程式即嘗試使 promise 接受 x 的狀態;否則其用 x 的值來執行 promise 。

這種 thenable 的特性使得 Promise 的實現更具有通用性:只要其暴露出一個遵循 Promise/A+ 協議的 then 方法即可;這同時也使遵循 Promise/A+ 規範的實現可以與那些不太規範但可用的實現能良好共存。

執行 [[Resolve]](promise, x) 需遵循以下步驟:

1。x 與 promise 相等

如果 promise 和 x 指向同一物件,以 TypeError 為據因拒絕執行 promise。

2。x 為 Promise

  • 如果 x 為 Promise ,則使 promise 接受 x 的狀態。

  • 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕。

  • 如果 x 處於執行態,用相同的值執行 promise。

  • 如果 x 處於拒絕態,用相同的據因拒絕 promise。

3。x 為物件或函式

如果 x 為物件或者函式:

  • 把 x.then 賦值給 then。

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

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

    • 如果 resolvePromise 以值 y 為引數被呼叫,則執行 [[Resolve]](promise, y)
    • 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
    • 如果 resolvePromise 和 rejectPromise 均被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
    • 如果呼叫 then 方法丟擲了異常 e:
      • 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
      • 否則以 e 為據因拒絕 promise
    • 如果 then 不是函式,以 x 為引數執行 promise
  • 如果 x 不為物件或者函式,以 x 為引數執行 promise

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

1.x 與 promise 相等

function resolvePromise(promise2, x, resolve, reject) {
  //x 與 promise 相等 
  //如果從onFulfilled中返回的x 就是promise2 就會導致迴圈引用報錯
  
  //如果 promise 和 x 指向同一物件,以 TypeError 為據因拒絕執行 promise
  if (x === promise2) {
    reject(new TypeError(`迴圈引用`));
  }
}
複製程式碼

2.x 為 Promise。

function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError(`迴圈引用`));
  }
  // x 為 Promise
  else if (x instanceof AjPromise) {
    // 如果 x 為 Promise ,則使 promise 接受 x 的狀態
    // 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject);
        },
        reason => {
          reject(reason);
        }
      );
    } else {
      // 如果 x 處於執行態,用相同的值執行 promise
      // 如果 x 處於拒絕態,用相同的據因拒絕 promise
      x.then(resolve, reject);
    }
  }
}
複製程式碼

3.x 為物件或函式

function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError(`迴圈引用`));
  }
  if (x instanceof AjPromise) {
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject);
        },
        reason => {
          reject(reason);
        }
      );
    } else {
      x.then(resolve, reject);
    }
  } else if (x && (typeof x === `function` || typeof x === `object`)) {
    // 避免多次呼叫
    let called = false;
    try {
      //把 x.then 賦值給 then
      let then = x.then;
      if (typeof then === `function`) {
        // 如果 then 是函式,將 x 作為函式的作用域 this 呼叫之。
        // 傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise ,第二個引數叫做 rejectPromise
        // 如果 resolvePromise 和 rejectPromise 均被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
        then.call(
          x,
          // 如果 resolvePromise 以值 y 為引數被呼叫,則執行[[Resolve]](promise, y)
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          // 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      }else {
        // 如果 then 不是函式,以 x 為引數執行 promise
        resolve(x);
      }  
    } catch (e) {
      // 如果取 x.then 的值時丟擲錯誤 e ,則以 e 為據因拒絕 promise
      // 如果呼叫 then 方法丟擲了異常 e:
      // 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
      // 否則以 e 為據因拒絕 promise
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 如果 x 不為物件或者函式,以 x 為引數執行 promise
    resolve(x);
  }
}

複製程式碼

Ok~比較複雜的解決過程也讓我們搞定了.接下來我們整合下程式碼

Promises/A+ 規範 完整程式碼


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

class AjPromise {
  constructor(fn) {
    this.state = PENDING;
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    const resolve = value => {
      if (value instanceof Promise) {
        return value.then(resolve, reject);
      }
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = FULFILLED;
          this.value = value;
          this.onFulfilledCallbacks.map(cb => {
            cb = cb(this.value);
          });
        }
      });
    };
    const reject = reason => {
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = REJECTED;
          this.reason = reason;
          this.onRejectedCallbacks.map(cb => {
            cb = cb(this.reason);
          });
        }
      });
    };
    try {
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  then(onFulfilled, onRejected) {
    let newPromise;

    onFulfilled = typeof onFulfilled === `function` ? onFulfilled : value => value;
    onRejected =
      typeof onRejected === `function`
        ? onRejected
        : reason => {
            throw reason;
          };
    if (this.state === FULFILLED) {
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state === REJECTED) {
      return (newPromise = new AjPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
    if (this.state === PENDING) {
      return (newPromise = new AjPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason);
            resolvePromise(newPromise, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        });
      }));
    }
  }
}
function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(new TypeError(`迴圈引用`));
  }
  if (x instanceof AjPromise) {
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject);
        },
        reason => {
          reject(reason);
        }
      );
    } else {
      x.then(resolve, reject);
    }
  } else if (x && (typeof x === `function` || typeof x === `object`)) {
    let called = false;
    try {
      let then = x.then;
      if (typeof then === `function`) {
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

AjPromise.deferred = function() {
  let defer = {};
  defer.promise = new AjPromise((resolve, reject) => {
    defer.resolve = resolve;
    defer.reject = reject;
  });
  return defer;
};

module.exports = AjPromise;

複製程式碼

再來看看我們的實現是否符合Promises/A+規範

npm run test
複製程式碼

nice,測試用例全部通過!

原始碼地址

傳送門

如果覺得有幫助到你,請給個star支援下作者~

參考文獻

Promises/A+規範譯文

Promise詳解與實現

相關文章