一步一步實現一個符合Promises/A+規範的Promise

mlxiao93發表於2019-03-31

準備工作

開始

首先,拋開測試用例,實現一個我們印象中的Promise

  1. Promise是一個建構函式,其入參接收一個函式 (fn)

  2. Promise執行會呼叫fn, 並將resolve和reject作為入參傳遞給fn (resolve和reject均是函式)

  3. Promise執行後,返回其例項promise

  4. promise有三種狀態:pending、fullfilled、rejected

  5. promise存在then方法,then方法可以註冊回撥函式onFullfilled、onRejeted,then方法返回Promise例項 (nextPromise) ,供鏈式呼叫

    • then方法內部:當promise為非pending狀態時,執行回撥。 回撥的執行結果會對nextPromise產生影響,所以每次註冊的回撥和對應的nextPromise需要一起儲存
  6. promise預設pending狀態

    • 執行resolve置為fullfilled,儲存resolve入參 (data),觸發onFullfilled
    • 執行reject置為rejected,儲存reject入參 (data),觸發onRejeted
  7. 執行callback時 (onFullfilled、onRejeted) ,傳入儲存的data

    • callback正常返回:觸發nextPromise的resolve,傳入返回值
    • callback執行捕獲到錯誤:觸發nextPromise的reject,傳入捕獲到的錯誤資訊
  8. 若callback的返回值是Promise例項,保證nextPromise的狀態與返回的promise的狀態同步

function execCallback(promise) {
  const defers = promise.defers;
  while (defers.length > 0) {
    const defer = defers.shift();
    const nextPromise = defer.promise;
    const callback = promise.state === 'fullfilled' ?  defer.onFullfilled : defer.onRejected;
    let cbRes;
    try {
      cbRes = callback(promise.data);
    } catch (err) {
      reject(nextPromise, err);
      continue;
    }
    if (cbRes instanceof Promise) {
      cbRes.then(data => {          // 當cbRes也是Promise時,保證nextPromise的狀態與cbRes一致
        resolve(nextPromise, data);
      }, err => {
        reject(nextPromise, err);
      });
    } else {
      resolve(nextPromise, cbRes);
    }
  }
}

function resolve(promise, data) {
  promise.data = data;
  promise.state = 'fullfilled';
  execCallback(promise);
}

function reject(promise, err) {
  promise.data = err;
  promise.state = 'rejected';
  execCallback(promise);
}

function Promise(fn) {
  this.state = 'pending';    // pending|fullfilled|rejected
  this.data = undefined;
  this.defers = [];     // 儲存 callback 和 nextPromise

  const promise = this;
  fn(data => {
    resolve(promise, data);
  }, err => {
    reject(promise, err);
  });
  return this;
};

Promise.prototype.then = function(onFullfilled, onRejected) {
  const nextPromise = new Promise(function() {});
  let defer = {
    promise: nextPromise,
    onFullfilled,
    onRejected
  };   // 回撥的執行會對nextPromise產生影響,故一起儲存
  this.defers.push(defer)
  if (this.state !== 'pending') {
    execCallback(this);    // 非pending狀態,觸發callback
  }
  return nextPromise;
}

module.exports =  Promise;
複製程式碼

github上檢視上述程式碼

用幾個常用的promise demo驗證了下上述程式碼,沒發現問題

跑測試用例,結果是:

171 passing (2m)
672 failing

  1) 2.2.1: Both `onFulfilled` and `onRejected` are optional arguments. 2.2.1.2: If `onRejected` is not a function, it must be ignored. applied to a promise fulfilled and then chained off of `onFulfilled` is `undefined`:
     Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
  ......
複製程式碼

檢視全部log

分析log,按順序先實現規範2.2.1:處理callback不為函式的情況

2.2.1 Both onFulfilled and onRejected are optional arguments:
       2.2.1.1. If onFulfilled is not a function, it must be ignored.
       2.2.1.2. If onRejected is not a function, it must be ignored.

意思是若onFulfilled、onRejected不是函式,則忽略。

考慮如下case:

new Promise((resolve, reject) => {
  resolve(123);
})
.then(null, null)
.then(data => {
  console.log('data: ', data)
}, err => {
  console.log('error: ', err)
})
複製程式碼

程式碼修改如下:

一步一步實現一個符合Promises/A+規範的Promise
(^_^本文寫到後面發現這兒有處錯誤: 第8行不應該使用return,應該使用continue,但是測試用例還是過了。¶¶¶)

檢視diff

實現規範2.2.2、2.2.3、2.2.4中未通過的部分:非同步執行callback

2.2.2. If onFulfilled is a function:
       2.2.2.2. it must not be called before promise is fulfilled.
2.2.3. If onRejected is a function,
       2.2.3.2. it must not be called before promise is rejected.
2.2.4. onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

這幾條規範總結起來就是: callback需要非同步執行

所以為execCallback的方法體加上setTimeout就能解決問題

檢視diff

實現規範2.1.2與2.1.3:狀態轉換的限制

2.1.2. When fulfilled, a promise:
       2.1.2.1. must not transition to any other state.
       2.1.2.2. must have a value, which must not change.
2.1.3. When rejected, a promise:
       2.1.3.1. must not transition to any other state.
       2.1.3.2. must have a reason, which must not change.

狀態只能從pending轉掉fullfilled或者rejected, 且只能轉換一次

改動如下:

一步一步實現一個符合Promises/A+規範的Promise
檢視diff

至此,規範1、2.1、2.2的要求全部滿足

理解規範2.3:callback返回promise & resolve傳入promise的處理

promise的callback函式是可以返回promise的,

對於返回promise的情況,then方法返回的newPromise的狀態和攜帶的資料,要求與callback返回的promise一致,看demo:

const promise1 = new Promise((resolve, reject) => {
  resolve(123);
});
const promise2 = new Promise((resolve, reject) => {
  resolve(456);
});
new Promise((resolve, reject) => {
  resolve(promise1);
}).then(data => {
  console.log(data)    // expect "123"
  return promise2;
}).then(data => {
  console.log(data);   // expect "456"
})
複製程式碼

規範2.3:

  • 保障了promise狀態和資料的鏈式傳遞,實現了非同步程式碼的繼發呼叫,避免callback hell
  • 規定了只要物件是thenable,就可以當成promise處理

關於thenable, 簡單來講,就是包含then方法的物件或函式

new Promise((resolve, reject) => {
  resolve('ok');
}).then(data => {
  return {         // 此處返回的就是thenable
    then(resolve, reject) {
      resolve('aaa');
    }
  };
}).then(data => {
  console.log(data)   // log: aaa
})
複製程式碼

為什麼會有thenable這個定義?

主要是為了使不同的promise實現可以協同使用

例如,張三實現了個Promise1, 李四實現了個庫Promise2,那麼可以這麼使用

new Promise1(resolve => {
  resolve(new Promise2())
})
複製程式碼

具體到實現程式碼,不可通過跑一遍測試用例判斷是否Promise, 所以規定通過thenable判斷是否可以協同使用

實現規範 2.3

前文的測試log,報錯指向 2.3.1

| 2.3.1. promise and x refer to the same object, reject promise with a TypeError as the reason.

case如下

let promise = new Promise((resolve, reject) => {
  resolve(123);
});
let nextPromise = promise.then(data => {
  return nextPromise;    // 這種情況需要throw TypeError
})
複製程式碼

程式碼改動:

一步一步實現一個符合Promises/A+規範的Promise
檢視diff

2.3.3 & 2.3.4

一步一步實現一個符合Promises/A+規範的Promise
其實就是圍繞thanable的一堆處理邏輯,先大致按照文件寫一下

注意:

  • execCallback和resolve函式都要處理thenable
  • promise也可以當成thenable處理

這裡先貼一下用於處理thenable的函式:

/**
 * 處理thenanble, 如果data不是thenable,返回false
 * @param promise: thanable執行後會對該promis產生影響
 * @param data: callback的返回,或者resolve的傳入值
 */
function doResolveThenable (promise, data) {    
  if (data && /object|function/.test(typeof data)) {
    let then;
    try {
      then = data.then;
      if (typeof then !== 'function') return false;    // 非thenanble
      then.call(data, data => { 
        resolve(promise, data)
      }, err => {
        reject(promise, err)
      })
    } catch(err) {
      reject(promise, err)
    }
  } else {
    return false;   // 非thenanble
  }
  return true;
}
複製程式碼

其它修改

一步一步實現一個符合Promises/A+規範的Promise
檢視diff

再看log,只剩下60個失敗的case了

剩下的case如下:

thenable的then方法裡,如果先後執行了onFullfilled、onRejected、throw,應當以第一次執行的為準,忽略後續的執行

let promise = new Promise((resolve, reject) => {
  resolve('ok');
}).then(data => {
  return {
    then(onFullfilled, onRejected) { 
      onFullfilled(111);   // 只允許這一句執行
      onFullfilled(222);
      onRejected(333);
      onRejected(444);
      throw (666);
    }
  }
})
複製程式碼

程式碼修改:

一步一步實現一個符合Promises/A+規範的Promise
檢視diff

終於,測試用例全過了,大功告成~~~

一步一步實現一個符合Promises/A+規範的Promise


github.com/mlxiao93/pr…

相關文章