Promise幾乎是面試必考點,所以我們不能僅僅會用,還得知道他的底層原理,學習他原理的最好方法就是自己也實現一個Promise。所以本文會自己實現一個遵循Promise/A+
規範的Promise。實現之後,我們還要用Promise/A+
官方的測試工具來測試下我們的實現是否正確,這個工具總共有872個測試用例,全部通過才算是符合Promise/A+
規範,下面是他們的連結:
Promise/A+
規範: https://github.com/promises-aplus/promises-spec
Promise/A+
測試工具: https://github.com/promises-aplus/promises-tests
本文的完整程式碼託管在GitHub上: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/JavaScript/Promise/MyPromise.js
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依次打出來:
Promises/A+規範
通過上面的例子,其實我們已經知道了一個promise長什麼樣子,Promises/A+規範其實就是對這個長相進一步進行了規範。下面我會對這個規範進行一些講解。
術語
promise
:是一個擁有then
方法的物件或函式,其行為符合本規範thenable
:是一個定義了then
方法的物件或函式。這個主要是用來相容一些老的Promise實現,只要一個Promise實現是thenable,也就是擁有then
方法的,就可以跟Promises/A+相容。value
:指reslove
出來的值,可以是任何合法的JS值(包括undefined
, thenable 和 promise等)exception
:異常,在Promise裡面用throw
丟擲來的值reason
:拒絕原因,是reject
裡面傳的引數,表示reject
的原因
Promise狀態
Promise總共有三個狀態:
pending
: 一個promise在resolve或者reject前就處於這個狀態。fulfilled
: 一個promise被resolve後就處於fulfilled
狀態,這個狀態不能再改變,而且必須擁有一個不可變的值(value
)。rejected
: 一個promise被reject後就處於rejected
狀態,這個狀態也不能再改變,而且必須擁有一個不可變的拒絕原因(reason
)。
注意這裡的不可變指的是===
,也就是說,如果value
或者reason
是物件,只要保證引用不變就行,規範沒有強制要求裡面的屬性也不變。Promise狀態其實很簡單,畫張圖就是:
then方法
一個promise必須擁有一個then
方法來訪問他的值或者拒絕原因。then
方法有兩個引數:
promise.then(onFulfilled, onRejected)
引數可選
onFulfilled
和 onRejected
都是可選引數。
- 如果
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的使用來窺探下需要做啥:
- 新建Promise需要使用
new
關鍵字,那他肯定是作為物件導向的方式呼叫的,Promise是一個類。關於JS的物件導向更詳細的解釋可以看這篇文章。- 我們
new Promise(fn)
的時候需要傳一個函式進去,說明Promise的引數是一個函式- 建構函式傳進去的
fn
會收到resolve
和reject
兩個函式,用來表示Promise成功和失敗,說明建構函式裡面還需要resolve
和reject
這兩個函式,這兩個函式的作用是改變Promise的狀態。- 根據規範,promise有
pending
,fulfilled
,rejected
三個狀態,初始狀態為pending
,呼叫resolve
會將其改為fulfilled
,呼叫reject
會改為rejected
。- 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
}
resolve
和reject
方法
根據規範,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;
}
}
}
呼叫建構函式引數
最後將resolve
和reject
作為引數呼叫傳進來的引數,記得加上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
方法裡面應該幹什麼呢,其實規範也告訴我們了,先檢查onFulfilled
和onRejected
是不是函式,如果不是函式就忽略他們,所謂“忽略”並不是什麼都不幹,對於onFulfilled
來說“忽略”就是將value
原封不動的返回,對於onRejected
來說就是返回reason
,onRejected
因為是錯誤分支,我們返回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) {
throw 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
,我們應該將onFulfilled
和onRejected
兩個回撥存起來,等到fn
有了結論,resolve
或者reject
的時候再來呼叫對應的程式碼。因為後面then
還有鏈式呼叫,會有多個onFulfilled
和onRejected
,我這裡用兩個陣列將他們存起來,等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);
});
上述程式碼輸出如下圖,符合我們的預期,說明到目前為止,我們的程式碼都沒問題:
then
的返回值
根據規範then
的返回值必須是一個promise,規範還定義了不同情況應該怎麼處理,我們先來處理幾種比較簡單的情況:
- 如果
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;
}
}
- 如果
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;
}
- 如果
onRejected
不是函式且promise1
拒絕執行,promise2
必須拒絕執行並返回相同的據因。這個要求其實在我們檢測onRejected
不是函式的時候已經做到了,因為我們預設給的onRejected
裡面會throw一個Error,所以程式碼肯定會走到catch裡面去。但是我們為了更直觀,程式碼還是跟規範一一對應吧。需要注意的是,如果promise1
的onRejected
執行成功了,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;
}
- 如果
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'));
}
if (x instanceof MyPromise) {
// 如果 x 為 Promise ,則使 promise 接受 x 的狀態
// 也就是繼續執行x,如果執行的時候拿到一個y,還要繼續解析y
// 這個if跟下面判斷then然後拿到執行其實重複了,可有可無
x.then(function (y) {
resolvePromise(promise, y, resolve, reject);
}, reject);
}
// 如果 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);
}
}
onFulfilled
和 onRejected
的執行時機
在規範中還有一條:onFulfilled
和 onRejected
只有在執行環境堆疊僅包含平臺程式碼時才可被呼叫。這一條的意思是實踐中要確保 onFulfilled
和 onRejected
方法非同步執行,且應該在 then
方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。所以在我們執行onFulfilled
和 onRejected
的時候都應該包到setTimeout
裡面去。
// 這塊程式碼在then裡面
if(this.status === FULFILLED) {
var promise2 = new MyPromise(function(resolve, reject) {
// 這裡加setTimeout
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
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;
}
if (this.status === PENDING) {
var promise2 = new MyPromise(function (resolve, reject) {
that.onFulfilledCallbacks.push(function () {
// 這裡加setTimeout
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);
});
that.onRejectedCallbacks.push(function () {
// 這裡加setTimeout
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;
}
測試我們的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完美通過了所有用例:
其他Promise方法
在ES6的官方Promise還有很多API,比如:
Promise.resolvePromise.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()
方法接受一個陣列作為引數,p1
、p2
、p3
都是 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 例項。上面程式碼中,只要p1
、p2
、p3
之中有一個例項率先改變狀態,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上看:
// 先定義三個常量表示狀態
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) {
if (that.status === PENDING) {
that.status = FULFILLED;
that.value = value;
// resolve裡面將所有成功的回撥拿出來執行
that.onFulfilledCallbacks.forEach(callback => {
callback(that.value);
});
}
}
// reject方法引數是reason
function reject(reason) {
if (that.status === PENDING) {
that.status = REJECTED;
that.reason = reason;
// resolve裡面將所有失敗的回撥拿出來執行
that.onRejectedCallbacks.forEach(callback => {
callback(that.reason);
});
}
}
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'));
}
if (x instanceof MyPromise) {
// 如果 x 為 Promise ,則使 promise 接受 x 的狀態
// 也就是繼續執行x,如果執行的時候拿到一個y,還要繼續解析y
// 這個if跟下面判斷then然後拿到執行其實重複了,可有可無
x.then(function (y) {
resolvePromise(promise, y, resolve, reject);
}, reject);
}
// 如果 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
// 後面返回新promise的時候也做了onFulfilled的引數檢查,這裡可以刪除,暫時保留是為了跟規範一一對應,看得更直觀
var realOnFulfilled = onFulfilled;
if (typeof realOnFulfilled !== 'function') {
realOnFulfilled = function (value) {
return value;
}
}
// 如果onRejected不是函式,給一個預設函式,返回reason的Error
// 後面返回新promise的時候也做了onRejected的引數檢查,這裡可以刪除,暫時保留是為了跟規範一一對應,看得更直觀
var realOnRejected = onRejected;
if (typeof realOnRejected !== 'function') {
realOnRejected = function (reason) {
throw 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 () {
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);
});
that.onRejectedCallbacks.push(function () {
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;
}
}
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
。下面再回顧下幾個要點:
- Promise其實是一個釋出訂閱模式
-
then
方法對於還在pending
的任務,其實是將回撥函式onFilfilled
和onRejected
塞入了兩個陣列 - Promise建構函式裡面的
resolve
方法會將陣列onFilfilledCallbacks
裡面的方法全部拿出來執行,這裡面是之前then方法塞進去的成功回撥 - 同理,Promise建構函式裡面的
reject
方法會將陣列onRejectedCallbacks
裡面的方法全部拿出來執行,這裡面是之前then方法塞進去的失敗回撥 -
then
方法會返回一個新的Promise以便執行鏈式呼叫 -
catch
和finally
這些例項方法都必須返回一個新的Promise例項以便實現鏈式呼叫
文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。
作者博文GitHub專案地址: https://github.com/dennis-jiang/Front-End-Knowledges