手寫一個Promise/A+,完美通過官方872個測試用例

蔣鵬飛發表於2020-04-07

前段時間我用兩篇文章深入講解了非同步的概念和Event Loop的底層原理,然後還講了一種自己實現非同步的釋出訂閱模式:

setTimeout和setImmediate到底誰先執行,本文讓你徹底理解Event Loop

從釋出訂閱模式入手讀懂Node.js的EventEmitter原始碼

本文會講解另一種更現代的非同步實現方案:Promise。Promise幾乎是面試必考點,所以我們不能僅僅會用,還得知道他的底層原理,學習他原理的最好方法就是自己也實現一個Promise。所以本文會自己實現一個遵循Promise/A+規範的Promise。實現之後,我們還要用Promise/A+官方的測試工具來測試下我們的實現是否正確,這個工具總共有872個測試用例,全部通過才算是符合Promise/A+規範,下面是他們的連結:

Promise/A+規範: github.com/promises-ap…

Promise/A+測試工具: github.com/promises-ap…

Promise用法

Promise的基本用法,網上有很多,我這裡簡單提一下,我還是用三個相互依賴的網路請求做例子,假如我們有三個網路請求,請求2必須依賴請求1的結果,請求3必須依賴請求2的結果,如果用回撥的話會有三層,會陷入“回撥地獄”,用Promise就清晰多了:

const request = require("request");

// 我們先用Promise包裝下三個網路請求
// 請求成功時resolve這個Promise
const request1 = function() {
  const promise = new Promise((resolve) => {
    request('https://www.baidu.com', function (error, response) {
      if (!error && response.statusCode == 200) {
        resolve('request1 success');
      }
    });
  });

  return promise;
}

const request2 = function() {
  const promise = new Promise((resolve) => {
    request('https://www.baidu.com', function (error, response) {
      if (!error && response.statusCode == 200) {
        resolve('request2 success');
      }
    });
  });

  return promise;
}

const request3 = function() {
  const promise = new Promise((resolve) => {
    request('https://www.baidu.com', function (error, response) {
      if (!error && response.statusCode == 200) {
        resolve('request3 success');
      }
    });
  });

  return promise;
}


// 先發起request1,等他resolve後再發起request2,
// 然後是request3
request1().then((data) => {
  console.log(data);
  return request2();
})
.then((data) => {
  console.log(data);
  return request3();
})
.then((data) => {
  console.log(data);
})
複製程式碼

上面的例子裡面,then是可以鏈式呼叫的,後面的then可以拿到前面resolve出來的資料,我們控制檯可以看到三個success依次打出來:

image-20200324164123892

Promises/A+規範

通過上面的例子,其實我們已經知道了一個promise長什麼樣子,Promises/A+規範其實就是對這個長相進一步進行了規範。下面我會對這個規範進行一些講解。

術語

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

  2. thenable:是一個定義了 then 方法的物件或函式。這個主要是用來相容一些老的Promise實現,只要一個Promise實現是thenable,也就是擁有then方法的,就可以跟Promises/A+相容。

  3. value:指reslove出來的值,可以是任何合法的JS值(包括 undefined , thenable 和 promise等)

  4. exception:異常,在Promise裡面用throw丟擲來的值

  5. reason:拒絕原因,是reject裡面傳的引數,表示reject的原因

Promise狀態

Promise總共有三個狀態:

  1. pending: 一個promise在resolve或者reject前就處於這個狀態。
  2. fulfilled: 一個promise被resolve後就處於fulfilled狀態,這個狀態不能再改變,而且必須擁有一個不可變的值(value)。
  3. rejected: 一個promise被reject後就處於rejected狀態,這個狀態也不能再改變,而且必須擁有一個不可變的拒絕原因(reason)。

注意這裡的不可變指的是===,也就是說,如果value或者reason是物件,只要保證引用不變就行,規範沒有強制要求裡面的屬性也不變。Promise狀態其實很簡單,畫張圖就是:

image-20200324173555225

then方法

一個promise必須擁有一個then方法來訪問他的值或者拒絕原因。then方法有兩個引數:

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

引數可選

onFulfilledonRejected 都是可選引數。

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

onFulfilled 特性

如果 onFulfilled 是函式:

  • promise 執行結束後其必須被呼叫,其第一個引數為 promise 的終值value
  • promise 執行結束前其不可被呼叫
  • 其呼叫次數不可超過一次

onRejected 特性

如果 onRejected 是函式:

  • promise 被拒絕執行後其必須被呼叫,其第一個引數為 promise 的據因reason
  • promise 被拒絕執行前其不可被呼叫
  • 其呼叫次數不可超過一次

多次呼叫

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

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

返回

then 方法必須返回一個 promise 物件。

promise2 = promise1.then(onFulfilled, onRejected); 
複製程式碼
  • 如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行 Promise 解決過程[[Resolve]](promise2, x)
  • 如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e
  • 如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值
  • 如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因

規範裡面還有很大一部分是講解Promise 解決過程的,光看規範,很空洞,前面這些規範已經可以指導我們開始寫一個自己的Promise了,Promise 解決過程會在我們後面寫到了再詳細講解。

自己寫一個Promise

我們自己要寫一個Promise,肯定需要知道有哪些工作需要做,我們先從Promise的使用來窺探下需要做啥:

  1. 新建Promise需要使用new關鍵字,那他肯定是作為物件導向的方式呼叫的,Promise是一個類。關於JS的物件導向更詳細的解釋可以看這篇文章。
  2. 我們new Promise(fn)的時候需要傳一個函式進去,說明Promise的引數是一個函式
  3. 建構函式傳進去的fn會收到resolvereject兩個函式,用來表示Promise成功和失敗,說明建構函式裡面還需要resolvereject這兩個函式,這兩個函式的作用是改變Promise的狀態。
  4. 根據規範,promise有pendingfulfilledrejected三個狀態,初始狀態為pending,呼叫resolve會將其改為fulfilled,呼叫reject會改為rejected
  5. promise例項物件建好後可以呼叫then方法,而且是可以鏈式呼叫then方法,說明then是一個例項方法。鏈式呼叫的實現這篇有詳細解釋,我這裡不再贅述。簡單的說就是then方法也必須返回一個帶then方法的物件,可以是this或者新的promise例項。

建構函式

為了更好的相容性,本文就不用ES6了。

// 先定義三個常量表示狀態
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

function MyPromise(fn) {
  this.status = PENDING;    // 初始狀態為pending
  this.value = null;        // 初始化value
  this.reason = null;       // 初始化reason
}
複製程式碼

resolvereject方法

根據規範,resolve方法是將狀態改為fulfilled,reject是將狀態改為rejected。

// 這兩個方法直接寫在建構函式裡面
function MyPromise(fn) {
  // ...省略前面程式碼...
  
  // 存一下this,以便resolve和reject裡面訪問
  var that = this;
  // resolve方法引數是value
  function resolve(value) {
    if(that.status === PENDING) {
      that.status = FULFILLED;
      that.value = value;
    }
  }
  
  // reject方法引數是reason
  function reject(reason) {
    if(that.status === PENDING) {
      that.status = REJECTED;
      that.reason = reason;
    }
  }
}
複製程式碼

呼叫建構函式引數

最後將resolvereject作為引數呼叫傳進來的引數,記得加上try,如果捕獲到錯誤就reject

function MyPromise(fn) {
  // ...省略前面程式碼...
  
  try {
    fn(resolve, reject);
  } catch (error) {
    reject(error);
  }
}
複製程式碼

then方法

根據我們前面的分析,then方法可以鏈式呼叫,所以他是例項方法,而且規範中的API是promise.then(onFulfilled, onRejected),我們先把架子搭出來:

MyPromise.prototype.then = function(onFulfilled, onRejected) {}
複製程式碼

then方法裡面應該幹什麼呢,其實規範也告訴我們了,先檢查onFulfilledonRejected是不是函式,如果不是函式就忽略他們,所謂“忽略”並不是什麼都不幹,對於onFulfilled來說“忽略”就是將value原封不動的返回,對於onRejected來說就是返回reasononRejected因為是錯誤分支,我們返回reason應該throw一個Error:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // 如果onFulfilled不是函式,給一個預設函式,返回value
  var realOnFulfilled = onFulfilled;
  if(typeof realOnFulfilled !== 'function') {
    realOnFulfilled = function (value) {
      return value;
    }
  }

  // 如果onRejected不是函式,給一個預設函式,返回reason的Error
  var realOnRejected = onRejected;
  if(typeof realOnRejected !== 'function') {
    realOnRejected = function (reason) {
      if(reason instanceof Error) {
        throw reason;
      } else {
        throw new Error(reason)
      }
    }
  }
}
複製程式碼

引數檢查完後就該乾點真正的事情了,想想我們使用Promise的時候,如果promise操作成功了就會呼叫then裡面的onFulfilled,如果他失敗了,就會呼叫onRejected。對應我們的程式碼就應該檢查下promise的status,如果是FULFILLED,就呼叫onFulfilled,如果是REJECTED,就呼叫onRejected:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // ...省略前面程式碼...

  if(this.status === FULFILLED) {
    onFulfilled(this.value)
  }

  if(this.status === REJECTED) {
    onRejected(this.reason);
  }
}
複製程式碼

再想一下,我們新建一個promise的時候可能是直接這樣用的:

new Promise(fn).then(onFulfilled, onRejected);
複製程式碼

上面程式碼then是在例項物件一建立好就呼叫了,這時候fn裡面的非同步操作可能還沒結束呢,也就是說他的status還是PENDING,這怎麼辦呢,這時候我們肯定不能立即調onFulfilled或者onRejected的,因為fn到底成功還是失敗還不知道呢。那什麼時候知道fn成功還是失敗呢?答案是fn裡面主動調resolve或者reject的時候。所以如果這時候status狀態還是PENDING,我們應該將onFulfilledonRejected兩個回撥存起來,等到fn有了結論,resolve或者reject的時候再來呼叫對應的程式碼。因為後面then還有鏈式呼叫,會有多個onFulfilledonRejected,我這裡用兩個陣列將他們存起來,等resolve或者reject的時候將陣列裡面的全部方法拿出來執行一遍

// 建構函式
function MyPromise(fn) {
  // ...省略其他程式碼...
  
  // 建構函式裡面新增兩個陣列儲存成功和失敗的回撥
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];
  
  function resolve(value) {
    if(that.status === PENDING) {
      // ...省略其他程式碼...
      // resolve裡面將所有成功的回撥拿出來執行
      that.onFulfilledCallbacks.forEach(callback => {
        callback(that.value);
      });
    }
  }
  
  function reject(reason) {
    if(that.status === PENDING) {
      // ...省略其他程式碼...
      // resolve裡面將所有失敗的回撥拿出來執行
      that.onRejectedCallbacks.forEach(callback => {
        callback(that.reason);
      });
    }
  }
}

// then方法
MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // ...省略其他程式碼...

  // 如果還是PENDING狀態,將回撥儲存下來
  if(this.status === PENDING) {
    this.onFulfilledCallbacks.push(realOnFulfilled);
    this.onRejectedCallbacks.push(realOnRejected);
  }
}
複製程式碼

上面這種暫時將回撥儲存下來,等條件滿足的時候再拿出來執行讓我想起了一種模式:訂閱釋出模式。我們往回撥陣列裡面push回撥函式,其實就相當於往事件中心註冊事件了,resolve就相當於釋出了一個成功事件,所有註冊了的事件,即onFulfilledCallbacks裡面的所有方法都會拿出來執行,同理reject就相當於釋出了一個失敗事件。更多訂閱釋出模式的原理可以看這裡

完成了一小步

到這裡為止,其實我們已經可以實現非同步呼叫了,只是then的返回值還沒實現,還不能實現鏈式呼叫,我們先來玩一下:

var request = require("request");
var MyPromise = require('./MyPromise');

var promise1 = new MyPromise((resolve) => {
  request('https://www.baidu.com', function (error, response) {
    if (!error && response.statusCode == 200) {
      resolve('request1 success');
    }
  });
});

promise1.then(function(value) {
  console.log(value);
});

var promise2 = new MyPromise((resolve, reject) => {
  request('https://www.baidu.com', function (error, response) {
    if (!error && response.statusCode == 200) {
      reject('request2 failed');
    }
  });
});

promise2.then(function(value) {
  console.log(value);
}, function(reason) {
  console.log(reason);
});
複製程式碼

上述程式碼輸出如下圖,符合我們的預期,說明到目前為止,我們的程式碼都沒問題:

image-20200325172257655

then的返回值

根據規範then的返回值必須是一個promise,規範還定義了不同情況應該怎麼處理,我們先來處理幾種比較簡單的情況:

  1. 如果 onFulfilled 或者 onRejected 丟擲一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e
MyPromise.prototype.then = function(onFulfilled, onRejected) {
	// ... 省略其他程式碼 ...
  
  // 有了這個要求,在RESOLVED和REJECTED的時候就不能簡單的執行onFulfilled和onRejected了。
  // 我們需要將他們用try...catch...包起來,如果有錯就reject。
  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        realOnFulfilled(that.value);
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }

  if(this.status === REJECTED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        realOnRejected(that.reason);
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
  
  // 如果還是PENDING狀態,也不能直接儲存回撥方法了,需要包一層來捕獲錯誤
  if(this.status === PENDING) {
    var promise2 = new MyPromise(function(resolve, reject) {
      that.onFulfilledCallbacks.push(function() {
        try {
          realOnFulfilled(that.value);
        } catch (error) {
          reject(error);
        }
      });
      that.onRejectedCallbacks.push(function() {
        try {
          realOnRejected(that.reason);
        } catch (error) {
          reject(error);
        }
      });
    });
  
    return promise2;
  }
}
複製程式碼
  1. 如果 onFulfilled 不是函式且 promise1 成功執行, promise2 必須成功執行並返回相同的值
// 我們就根據要求加個判斷,注意else裡面是正常執行流程,需要resolve
// 這是個例子,每個realOnFulfilled後面都要這樣寫
  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        if (typeof onFulfilled !== 'function') {
          resolve(that.value);
        } else {
          realOnFulfilled(that.value);
          resolve(that.value);
        }
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
複製程式碼
  1. 如果 onRejected 不是函式且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因。這個要求其實在我們檢測 onRejected 不是函式的時候已經做到了,因為我們預設給的onRejected裡面會throw一個Error,所以程式碼肯定會走到catch裡面去。但是我們為了更直觀,程式碼還是跟規範一一對應吧。需要注意的是,如果promise1onRejected執行成功了,promise2應該被resolve。改造程式碼如下:
  if(this.status === REJECTED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        if(typeof onRejected !== 'function') {
          reject(that.reason);
        } else {
          realOnRejected(that.reason);
          resolve();
        }
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
複製程式碼
  1. 如果 onFulfilled 或者 onRejected 返回一個值 x ,則執行下面的 Promise 解決過程[[Resolve]](promise2, x)。這條其實才是規範的第一條,因為他比較麻煩,所以我將它放到了最後。前面我們程式碼的實現,其實只要onRejected或者onFulfilled成功執行了,我們都要resolve promise2。多了這條,我們還需要對onRejected或者onFulfilled的返回值進行判斷,如果有返回值就要進行 Promise 解決過程。我們專門寫一個方法來進行Promise 解決過程。前面我們程式碼的實現,其實只要onRejected或者onFulfilled成功執行了,我們都要resolve promise2,這個過程我們也放到這個方法裡面去吧,所以程式碼變為下面這樣,其他地方類似:
  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      try {
        if (typeof onFulfilled !== 'function') {
          resolve(that.value);
        } else {
          var x = realOnFulfilled(that.value);
          resolvePromise(promise2, x, resolve, reject);   // 呼叫Promise 解決過程
        }
      } catch (error) {
        reject(error);
      }
    });
  
    return promise2;
  }
複製程式碼

Promise 解決過程

現在我們該來實現resolvePromise方法了,規範中這一部分較長,我就直接把規範作為註釋寫在程式碼裡面了。

function resolvePromise(promise, x, resolve, reject) {
  // 如果 promise 和 x 指向同一物件,以 TypeError 為據因拒絕執行 promise
  // 這是為了防止迴圈引用
  if(promise === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }

  // 如果 x 為 Promise ,則使 promise 接受 x 的狀態
  if(x instanceof MyPromise) {
    // 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕
    if(x.status === PENDING) {
      x.then(resolve, reject);
    } else if(x.status === FULFILLED) {
      // 如果 x 處於執行態,用相同的值執行 promise
      resolve(x.value);
    } else if(x.status === REJECTED) {
      // 如果 x 處於拒絕態,用相同的據因拒絕 promise
      reject(x.reason);
    }
  }
  // 如果 x 為物件或者函式
  else if(typeof x === 'object' || typeof x === 'function') {
    try {
      // 把 x.then 賦值給 then 
      var then = x.then;
    } catch (error) {
      // 如果取 x.then 的值時丟擲錯誤 e ,則以 e 為據因拒絕 promise
      reject(error);
    }

    // 如果 then 是函式
    if(typeof then === 'function') {
      var called = false;
      // 將 x 作為函式的作用域 this 呼叫之
      // 傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise ,第二個引數叫做 rejectPromise
      // 名字重名了,我直接用匿名函式了
      try {
        then.call(
          x, 
          // 如果 resolvePromise 以值 y 為引數被呼叫,則執行 [[Resolve]](promise, y)
          function(y){
            // 如果 resolvePromise 和 rejectPromise 均被呼叫,
            // 或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
            // 實現這條需要前面加一個變數called
            if(called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          }, 
          // 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
          function(r){
            if(called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果呼叫 then 方法丟擲了異常 e:
        // 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
        if(called) return;

        // 否則以 e 為據因拒絕 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函式,以 x 為引數執行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不為物件或者函式,以 x 為引數執行 promise
    resolve(x);
  }
}
複製程式碼

處理同步任務

到這裡我們的Promise/A+基本都實現了,只是還要注意一個點,如果使用者給建構函式傳的是一個同步函式,裡面的resolvereject會立即執行,比then還執行的早,那then裡面註冊的回撥就沒機會執行了,所以要給他們加個setTimeout

  function resolve(value) {
    // 這裡加setTimeout
    setTimeout(function() {
      if(that.status === PENDING) {
        that.status = FULFILLED;
        that.value = value;
  
        that.onFulfilledCallbacks.forEach(callback => {
          callback(that.value);
        });
      }
    }, 0);
  }
  
  function reject(reason) {
    // 這裡加setTimeout
    setTimeout(function() {
      if(that.status === PENDING) {
        that.status = REJECTED;
        that.reason = reason;
  
        that.onRejectedCallbacks.forEach(callback => {
          callback(that.reason);
        });
      }
    }, 0);
  }
複製程式碼

測試我們的Promise

我們使用Promise/A+官方的測試工具promises-aplus-tests來對我們的MyPromise進行測試,要使用這個工具我們必須實現一個靜態方法deferred,官方對這個方法的定義如下:

deferred: 返回一個包含{ promise, resolve, reject }的物件

promise 是一個處於pending狀態的promise

resolve(value)value解決上面那個promise

reject(reason)reason拒絕上面那個promise

我們實現程式碼如下:

MyPromise.deferred = function() {
  var result = {};
  result.promise = new MyPromise(function(resolve, reject){
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}
複製程式碼

然後用npm將promises-aplus-tests下載下來,再配置下package.json就可以跑測試了:

{
  "devDependencies": {
    "promises-aplus-tests": "^2.1.2"
  },
  "scripts": {
    "test": "promises-aplus-tests MyPromise"
  }
}
複製程式碼

在跑測試的時候發現一個坑,在resolvePromise的時候,如果x是null,他的型別也是object,是應該直接用x來resolve的,之前的程式碼會走到catch然後reject,所以需要檢測下null

// 這個坑是跑測試的時候發現的,如果x是null,應該直接resolve
if(x === null) {
  return resolve(x);
}
複製程式碼

這個測試總共872用例,我們寫的Promise完美通過了所有用例:

image-20200326214543894

其他Promise方法

在ES6的官方Promise還有很多API,比如:

Promise.resolve

Promise.reject

Promise.all

Promise.race

Promise.prototype.catch

Promise.prototype.finally

Promise.allSettled

雖然這些都不在Promise/A+裡面,但是我們也來實現一下吧,加深理解。其實我們前面實現了Promise/A+再來實現這些已經是小菜一碟了,因為這些API全部是前面的封裝而已。

Promise.resolve

將現有物件轉為Promise物件,如果 Promise.resolve 方法的引數,不是具有 then 方法的物件(又稱 thenable 物件),則返回一個新的 Promise 物件,且它的狀態為fulfilled。

MyPromise.resolve = function(parameter) {
  if(parameter instanceof MyPromise) {
    return parameter;
  }

  return new MyPromise(function(resolve) {
    resolve(parameter);
  });
}
複製程式碼

Promise.reject

返回一個新的Promise例項,該例項的狀態為rejected。Promise.reject方法的引數reason,會被傳遞給例項的回撥函式。

MyPromise.reject = function(reason) {
  return new MyPromise(function(resolve, reject) {
    reject(reason);
  });
}
複製程式碼

Promise.all

該方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。

const p = Promise.all([p1, p2, p3]);
複製程式碼

Promise.all()方法接受一個陣列作為引數,p1p2p3都是 Promise 例項,如果不是,就會先呼叫Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。當p1, p2, p3全部resolve,大的promise才resolve,有任何一個reject,大的promise都reject。

MyPromise.all = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var count = 0;
    var result = [];
    var length = promiseList.length;

    if(length === 0) {
      return resolve(result);
    }

    promiseList.forEach(function(promise, index) {
      MyPromise.resolve(promise).then(function(value){
        count++;
        result[index] = value;
        if(count === length) {
          resolve(result);
        }
      }, function(reason){
        reject(reason);
      });
    });
  });

  return resPromise;
}
複製程式碼

Promise.race

用法:

const p = Promise.race([p1, p2, p3]);
複製程式碼

該方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。上面程式碼中,只要p1p2p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。

MyPromise.race = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var length = promiseList.length;

    if(length === 0) {
      return resolve();
    } else {
      for(var i = 0; i < length; i++) {
        MyPromise.resolve(promiseList[i]).then(function(value) {
          return resolve(value);
        }, function(reason) {
          return reject(reason);
        });
      }
    }
  });

  return resPromise;
}
複製程式碼

Promise.prototype.catch

Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。

MyPromise.prototype.catch = function(onRejected) {
  this.then(null, onRejected);
}
複製程式碼

Promise.prototype.finally

finally方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。

MyPromise.prototype.finally = function(fn) {
  return this.then(function(value){
    return MyPromise.resolve(value).then(function(){
      return value;
    });
  }, function(error){
    return MyPromise.resolve(reason).then(function() {
      throw error
    });
  });
}
複製程式碼

Promise.allSettled

該方法接受一組 Promise 例項作為引數,包裝成一個新的 Promise 例項。只有等到所有這些引數例項都返回結果,不管是fulfilled還是rejected,包裝例項才會結束。該方法由 ES2020 引入。該方法返回的新的 Promise 例項,一旦結束,狀態總是fulfilled,不會變成rejected。狀態變成fulfilled後,Promise 的監聽函式接收到的引數是一個陣列,每個成員對應一個傳入Promise.allSettled()的 Promise 例項的執行結果。

MyPromise.allSettled = function(promiseList) {
  return new MyPromise(function(resolve){
    var length = promiseList.length;
    var result = [];
    var count = 0;

    if(length === 0) {
      return resolve(result);
    } else {
      for(var i = 0; i < length; i++) {

        (function(i){
          var currentPromise = MyPromise.resolve(promiseList[i]);

          currentPromise.then(function(value){
            count++;
            result[i] = {
              status: 'fulfilled',
              value: value
            }
            if(count === length) {
              return resolve(result);
            }
          }, function(reason){
            count++;
            result[i] = {
              status: 'rejected',
              reason: reason
            }
            if(count === length) {
              return resolve(result);
            }
          });
        })(i)
      }
    }
  });
}
複製程式碼

完整程式碼

完全版的程式碼較長,這裡如果看不清楚的可以去我的GitHub上看:

github.com/dennis-jian…

// 先定義三個常量表示狀態
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

function MyPromise(fn) {
  this.status = PENDING;    // 初始狀態為pending
  this.value = null;        // 初始化value
  this.reason = null;       // 初始化reason

  // 建構函式裡面新增兩個陣列儲存成功和失敗的回撥
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];

  // 存一下this,以便resolve和reject裡面訪問
  var that = this;
  // resolve方法引數是value
  function resolve(value) {
    setTimeout(function() {
      if(that.status === PENDING) {
        that.status = FULFILLED;
        that.value = value;
  
        // resolve裡面將所有成功的回撥拿出來執行
        that.onFulfilledCallbacks.forEach(callback => {
          callback(that.value);
        });
      }
    }, 0);
  }
  
  // reject方法引數是reason
  function reject(reason) {
    setTimeout(function() {
      if(that.status === PENDING) {
        that.status = REJECTED;
        that.reason = reason;
  
        // resolve裡面將所有失敗的回撥拿出來執行
        that.onRejectedCallbacks.forEach(callback => {
          callback(that.reason);
        });
      }
    }, 0);
  }

  try {
    fn(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

function resolvePromise(promise, x, resolve, reject) {
  // 如果 promise 和 x 指向同一物件,以 TypeError 為據因拒絕執行 promise
  // 這是為了防止死迴圈
  if(promise === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }

  // 如果 x 為 Promise ,則使 promise 接受 x 的狀態
  if(x instanceof MyPromise) {
    // 如果 x 處於等待態, promise 需保持為等待態直至 x 被執行或拒絕
    if(x.status === PENDING) {
      x.then(function(y) {
        resolvePromise(promise, y, resolve, reject);
      }, reject);
    } else if(x.status === FULFILLED) {
      // 如果 x 處於執行態,用相同的值執行 promise
      resolve(x.value);
    } else if(x.status === REJECTED) {
      // 如果 x 處於拒絕態,用相同的據因拒絕 promise
      reject(x.reason);
    }
  }
  // 如果 x 為物件或者函式
  else if(typeof x === 'object' || typeof x === 'function') {
    // 這個坑是跑測試的時候發現的,如果x是null,應該直接resolve
    if(x === null) {
      return resolve(x);
    }

    try {
      // 把 x.then 賦值給 then 
      var then = x.then;
    } catch (error) {
      // 如果取 x.then 的值時丟擲錯誤 e ,則以 e 為據因拒絕 promise
      return reject(error);
    }

    // 如果 then 是函式
    if(typeof then === 'function') {
      var called = false;
      // 將 x 作為函式的作用域 this 呼叫之
      // 傳遞兩個回撥函式作為引數,第一個引數叫做 resolvePromise ,第二個引數叫做 rejectPromise
      // 名字重名了,我直接用匿名函式了
      try {
        then.call(
          x, 
          // 如果 resolvePromise 以值 y 為引數被呼叫,則執行 [[Resolve]](promise, y)
          function(y){
            // 如果 resolvePromise 和 rejectPromise 均被呼叫,
            // 或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫
            // 實現這條需要前面加一個變數called
            if(called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          }, 
          // 如果 rejectPromise 以據因 r 為引數被呼叫,則以據因 r 拒絕 promise
          function(r){
            if(called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果呼叫 then 方法丟擲了異常 e:
        // 如果 resolvePromise 或 rejectPromise 已經被呼叫,則忽略之
        if(called) return;

        // 否則以 e 為據因拒絕 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函式,以 x 為引數執行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不為物件或者函式,以 x 為引數執行 promise
    resolve(x);
  }
}

MyPromise.prototype.then = function(onFulfilled, onRejected) {
  // 如果onFulfilled不是函式,給一個預設函式,返回value
  var realOnFulfilled = onFulfilled;
  if(typeof realOnFulfilled !== 'function') {
    realOnFulfilled = function (value) {
      return value;
    }
  }

  // 如果onRejected不是函式,給一個預設函式,返回reason的Error
  var realOnRejected = onRejected;
  if(typeof realOnRejected !== 'function') {
    realOnRejected = function (reason) {
      if(reason instanceof Error) {
        throw reason;
      } else {
        throw new Error(reason)
      }
    }
  }

  var that = this;   // 儲存一下this

  if(this.status === FULFILLED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      setTimeout(function() {
        try {
          if (typeof onFulfilled !== 'function') {
            resolve(that.value);
          } else {
            var x = realOnFulfilled(that.value);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      }, 0);
    });
  
    return promise2;
  }

  if(this.status === REJECTED) {
    var promise2 = new MyPromise(function(resolve, reject) {
      setTimeout(function() {
        try {
          if(typeof onRejected !== 'function') {
            reject(that.reason);
          } else {
            var x = realOnRejected(that.reason);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      }, 0);
    });
  
    return promise2;
  }

  // 如果還是PENDING狀態,將回撥儲存下來
  if(this.status === PENDING) {
    var promise2 = new MyPromise(function(resolve, reject) {
      that.onFulfilledCallbacks.push(function() {
        try {
          if(typeof onFulfilled !== 'function') {
            resolve(that.value);
          } else {
            var x = realOnFulfilled(that.value);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      });
      that.onRejectedCallbacks.push(function() {
        try {
          if(typeof onRejected !== 'function') {
            reject(that.reason);
          } else {
            var x = realOnRejected(that.reason);
            resolvePromise(promise2, x, resolve, reject);
          }
        } catch (error) {
          reject(error);
        }
      });
    });
  
    return promise2;
  }
}

MyPromise.deferred = function() {
  var result = {};
  result.promise = new MyPromise(function(resolve, reject){
    result.resolve = resolve;
    result.reject = reject;
  });

  return result;
}

MyPromise.resolve = function(parameter) {
  if(parameter instanceof MyPromise) {
    return parameter;
  }

  return new MyPromise(function(resolve) {
    resolve(parameter);
  });
}

MyPromise.reject = function(reason) {
  return new MyPromise(function(resolve, reject) {
    reject(reason);
  });
}

MyPromise.all = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var count = 0;
    var result = [];
    var length = promiseList.length;

    if(length === 0) {
      return resolve(result);
    }

    promiseList.forEach(function(promise, index) {
      MyPromise.resolve(promise).then(function(value){
        count++;
        result[index] = value;
        if(count === length) {
          resolve(result);
        }
      }, function(reason){
        reject(reason);
      });
    });
  });

  return resPromise;
}

MyPromise.race = function(promiseList) {
  var resPromise = new MyPromise(function(resolve, reject) {
    var length = promiseList.length;

    if(length === 0) {
      return resolve();
    } else {
      for(var i = 0; i < length; i++) {
        MyPromise.resolve(promiseList[i]).then(function(value) {
          return resolve(value);
        }, function(reason) {
          return reject(reason);
        });
      }
    }
  });

  return resPromise;
}

MyPromise.prototype.catch = function(onRejected) {
  this.then(null, onRejected);
}

MyPromise.prototype.finally = function(fn) {
  return this.then(function(value){
    return MyPromise.resolve(fn()).then(function(){
      return value;
    });
  }, function(error){
    return MyPromise.resolve(fn()).then(function() {
      throw error
    });
  });
}

MyPromise.allSettled = function(promiseList) {
  return new MyPromise(function(resolve){
    var length = promiseList.length;
    var result = [];
    var count = 0;

    if(length === 0) {
      return resolve(result);
    } else {
      for(var i = 0; i < length; i++) {

        (function(i){
          var currentPromise = MyPromise.resolve(promiseList[i]);

          currentPromise.then(function(value){
            count++;
            result[i] = {
              status: 'fulfilled',
              value: value
            }
            if(count === length) {
              return resolve(result);
            }
          }, function(reason){
            count++;
            result[i] = {
              status: 'rejected',
              reason: reason
            }
            if(count === length) {
              return resolve(result);
            }
          });
        })(i)
      }
    }
  });
}

module.exports = MyPromise;
複製程式碼

總結

至此,我們的Promise就簡單實現了,只是我們不是原生程式碼,不能做成微任務,如果一定要做成微任務的話,只能用其他微任務API模擬,比如MutaionObserver或者process.nextTick。下面再回顧下幾個要點:

  1. Promise其實是一個釋出訂閱模式
  2. then方法對於還在pending的任務,其實是將回撥函式onFilfilledonRejected塞入了兩個陣列
  3. Promise建構函式裡面的resolve方法會將陣列onFilfilledCallbacks裡面的方法全部拿出來執行,這裡面是之前then方法塞進去的成功回撥
  4. 同理,Promise建構函式裡面的reject方法會將陣列onRejectedCallbacks裡面的方法全部拿出來執行,這裡面是之前then方法塞進去的失敗回撥
  5. then方法會返回一個新的Promise以便執行鏈式呼叫
  6. catchfinally這些例項方法都必須返回一個新的Promise例項以便實現鏈式呼叫

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。

作者博文GitHub專案地址: github.com/dennis-jian…

作者掘金文章彙總:juejin.im/post/5e3ffc…

相關文章