相比於回撥函式,Promise 解決了 “回撥地獄” 和 “信任問題” 等痛點,並且大大提高了程式碼的可讀性。在現代前端開發中,Promise 幾乎成了處理非同步的首選(雖然還有更方便的 async/await,逃)。這篇文章從 Promise 的思想和執行機制入手,深入理解每個 API,最後手寫一個遵循 Promise/A+ 規範的 Promise 來。
非同步方式
JavaScript 非同步方式共有有下面六種。
-
事件監聽
-
回撥函式
-
釋出/訂閱
-
Promise
-
生成器
-
async/await
回撥函式
面試中被問到 回撥函式
有什麼缺點,相信你一定不假思索地回答 回撥地獄
。的確如此,當我們需要傳送多個非同步請求,並且每個請求之間需要相互依賴時,就會產生回撥地獄。
前段時間寫了一個天氣微信小程式 Natsuha,它獲取天氣的邏輯大致如下(當然真實場景複雜的多)。
-
首先要獲取使用者的經緯度 (介面 A)
-
根據經緯度反查城市 (介面 B)
-
根據城市拿到相應的天氣資訊 (介面 C)
按照回撥的方式去處理這個邏輯,大致會寫成下面的樣子:
ajax(A, () => {
// 獲取經緯度
ajax(B, () => {
// 根據經緯度反查城市
ajax(C, () => {
// 根據城市獲取天氣資訊
});
});
});
複製程式碼
看起來很醜陋不是嗎?相信大家對回撥函式的缺點大致都瞭解,這裡就不展開,只做個總結。
-
程式碼邏輯書寫順序與執行順序不一致,不利於閱讀與維護。
-
非同步操作的順序變更時,需要大規模的程式碼重構。
-
回撥函式基本都是匿名函式,bug 追蹤困難。
-
回撥函式是被第三方庫程式碼(如上例中的 ajax )而非自己的業務程式碼所呼叫的,造成了控制反轉(IoC)。
簡單談一談 控制反轉
,《你不知道的 JavaScript (中卷)》把回撥函式的最大缺點歸結為 信任問題
。例子中 ajax 是一個三方的函式(你完全可以把它想象成 jQuery 的 $.ajax()),我們把自己的業務邏輯,也就是將回撥函式 交給了
ajax 去處理。但 ajax 對我們來說僅僅是一個黑盒,如果 ajax 本身有缺陷的話,我們的回撥函式就處於危險之中,這也就是所謂的“信任問題”。
不過 Promise 的出現解決了這些缺點,它能夠把控制反轉再反轉回來。這樣的話,我們可以不把自己程式的傳給第三方,而是讓第三方給我們提供瞭解其任務何時結束的能力,進而由我們自己的程式碼來決定下一步做什麼。
何為 Promise
《你不知道的 JavaScript (中卷)》舉了一個例子:
我在快餐店點了一個漢堡,並支付了 1.07 美金。這意味著我對某個值(漢堡)發出了請求。
接著收銀員給我一張 取餐單據
,它保證了我最終會得到漢堡,因此 取餐單據
就是一個 承諾
。
在等待取餐的過程中,我可以做點其他的事情,比如刷刷推特,看看 996.icu 今天又漲了多少 star。之所以我可做點兒其他的事情,是因為 取餐單據
代表了我 未來的
漢堡。它在某種意義上已經成了漢堡的 佔位符
。從本質上來講,這個 佔位符
使得這個值不再依賴時間,這是一個 未來值
。
終於,我聽到服務員在喊 250號前來取餐
,我就可以拿著 取餐單據
換我的漢堡了。
但是可能還有另一種結果,在我去取餐時,服務員充滿抱歉的告訴我漢堡已經售罄了,除了憤怒,我們還可以看到 未來值
可能成功,也可能失敗。
Promise 基礎知識
Promise 的生命週期
每個 Promise 都會經歷一個短暫的生命週期:先是處於 進行中 (pending)
,此時操作尚未完成,因此它也是 未處理 (unsettled)
的;一旦非同步操作執行結束,Promise 變成 已處理 (settled)
狀態,此時它會進入到以下兩個狀態中的其中一個:
-
Fulfilled:Promise 非同步操作成功完成
-
Rejected:由於程式錯誤或其他原因,非同步操作未能成功完成
Promise 建構函式
Promise 本身是一個建構函式,它接收一個叫做 executor
的函式,該函式會被傳遞兩個名為 resolve()
和 reject()
的函式作為引數。resolve()
函式在執行器成功時被呼叫,而 reject()
在執行器操作失敗後被呼叫。看下面這個例子。
const fs = require('fs');
const promise = path =>
// 執行器接收 resolve() 和 reject() 作為引數
new Promise((resolve, reject) => {
fs.readFile(__dirname + '/' + path, 'utf-8', (err, data) => {
if (err) {
// 失敗時呼叫 reject()
reject(err);
return;
}
// 成功時時呼叫 resolve()
resolve(data);
});
});
複製程式碼
Promise 的 then 方法
then() 方法接收兩個函式作為引數,第一個作為 完成
時的回撥,第二個作為 拒絕
時的回撥。兩個引數均為可選,因此你可以只監聽 完成
,或者只監聽 拒絕
。其中當第一個引數為 null
,第二個引數為回撥函式時,它意味著監聽 拒絕
。在實際應用中,完成
和 拒絕
都應當被監聽。
const promise = new Promise((resolve, reject) => {
resolve('success');
});
// 監聽完成和拒絕
promise.then(
res => {
// 完成
console.log(res);
},
e => {
// 拒絕
console.log(e);
},
);
// 只監聽完成
promise.then(res => {
console.log(res);
});
// 第一個引數為 null 時意味著拒絕
promise.then(null, res => {
// 完成
console.log(res);
});
複製程式碼
Promise 還有兩個方法分別是 catch()
和 finally()
,前者用於監聽 拒絕
,後者無論成功失敗都會被執行到。鏈式呼叫顯然可讀性更高,所以我們推薦下面這種寫法。
promise
.then(res => {
console.log(res);
})
.catch(e => {
console.log(e);
})
.finally(() => {
console.log('無論成功失敗都會執行這句');
});
複製程式碼
Promise 鏈式呼叫
每次呼叫 then() 或 catch() 方法時都會 建立並返回一個新的 Promise
,只有當前一個 Promise 完成或被拒絕後,下一個才會被解決。
看下面這個例子,p.then() 完成後返回第二個 Promise,接著又呼叫了它的 then() 方法,也就是說只有當第一個 Promise 被解決之後才會呼叫第二個 then() 方法的 then()
。
let p = new Promise((resolve, reject) => {
resolve(42);
});
p.then(value => {
console.log(value); // 42
}).then(() => {
console.log('可以執行到'); // '可以執行到'
});
複製程式碼
將上述示例拆開,看起來是這樣的。呼叫 p1.then() 的結果被儲存到 p2 中,p2.then() 被呼叫來新增最終的 then()
。
let p1 = new Promise((resolve, reject) => {
resolve(42);
});
let p2 = p1.then(value => {
console.log(value);
});
p2.then(() => {
console.log('可以執行到');
});
複製程式碼
我們通過一個例項來看一下鏈式呼叫。下面是獲取城市天氣的場景:我們首先需要呼叫 getCity
介面來獲取 城市id
,接著呼叫 getWeatherById/城市id
來獲取城市的天氣資訊。首先用 Promise 封裝一個原生 Ajax。(敲黑板,面試可能要求手寫)
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject) {
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open('GET', url);
client.onreadystatechange = handler;
client.responseType = 'json';
client.setRequestHeader('Accept', 'application/json');
client.send();
});
return promise;
};
const baseUrl = 'https://5cb322936ce9ce00145bf070.mockapi.io/api/v1';
複製程式碼
通過鏈式呼叫來請求資料,最後別忘了捕獲錯誤。
getJSON(`${baseUrl}/getCity`)
.then(value => getJSON(`${baseUrl}/getWeatherById/${value.cityId}`))
.then(value => console.log(value))
.catch(e => {
console.log(e);
});
複製程式碼
捕獲錯誤
當 then() 方法或者 catch() 方法丟擲錯誤時,鏈式呼叫的下一個 Promise 中的 catch() 方法可以通過 catch()
接收這個錯誤。側面來講,異常不一定只發生在 Promise 中,還有可能發生在 then()
或者 catch()
中。
let p1 = new Promise((resolve, reject) => {
resolve(42);
});
p1.then(value => {
throw new Error(' `then()` 錯誤');
}).catch(e => {
console.log(e.message); // ' `then()` 錯誤'
});
複製程式碼
不僅 then()
可以丟擲異常,catch()
也可以丟擲的異常,且可以被下一個 catch()
捕獲。因此,無論如何都應該在 Promise 鏈的末尾留一個 catch()
,以保證能夠正確處理所有可能發生的錯誤。看下面這個例子。
let p1 = new Promise((resolve, reject) => {
throw new Error('執行器錯誤');
});
p1.catch(e => {
console.log(e.message); // '執行器錯誤'
throw new Error(' `catch()` 錯誤');
}).catch(e => {
console.log(e.message); // ' `catch()` 錯誤'
});
複製程式碼
Promise 鏈的返回值
Promise 鏈的一個重要特性是能從一個 Promise 傳遞資料給下一個 Promise,通過完成處理函式的返回值,來將資料沿著一個鏈傳遞下去。我們看下面這個例子。
function task() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('task');
}, 1000);
});
}
task()
.then(res => {
console.log(res);
return 'taskB';
})
.then(res => {
console.log(res);
return 'taskC';
})
.then(res => {
console.log(res);
throw new Error();
})
.catch(e => {
console.log(e);
return 'taskD';
})
.then(res => {
console.log(res);
});
複製程式碼
執行結果如上圖所示。我們知道,每次呼叫 then() 或者 catch() 都會返回一個新的 Promise 例項,通過指定處理函式的返回值,可以沿著一個鏈繼續傳遞資料。
因此第一個 then() 將 'taskB' 作為下一個 then() 的引數傳遞下去,同樣第二個 then() 將 'taskC' 作為第三個 then() 的引數傳遞下去。
而第三個 then() 裡面丟擲一個異常,上面說到處理函式中的丟擲異常一定會被後面的拒絕處理函式捕獲,所以 catch() 裡能夠列印出上一個 then() 的錯誤。
別忘了 catch() 返回 'taskD' 也可以被最後一個 then() 捕獲。
其他構造方法
Promise.resolve() 和 Promise.reject()
Promise.resolve() 和 Promise.reject() 類似於快捷方式,用來建立一個 已完成
或 已被拒絕
的 promise。此外,Promise.resolve() 還能接受非 Promise 的 thenable
的作為引數,也就是所謂 擁有 then 方法的物件
。
// p1 和 p2 等價
const p1 = new Promise((resolve, reject) => {
reject('Oops');
});
const p2 = Promise.reject('Oops');
// p3 和 p4 等價
const p3 = new Promise((resolve, reject) => {
resolve('Oops');
});
const p4 = Promise.resolve('Oops');
複製程式碼
而對於 Promise.resolve(),它還能接收一個非 Promise 的 thenable
作為引數。它可以建立一個已完成的 Promise,也可以建立一個以拒絕的 Promise。
let thenable1 = {
then(resolve, reject) {
resolve(1);
},
};
let p1 = Promise.resolve(thenable1);
p1.then(value => console.log(value)); // 1
let thenable2 = {
then(resolve, reject) {
reject(1);
},
};
let p2 = Promise.resolve(thenable2);
p2.catch(reason => console.log(reason)); // 1
複製程式碼
Promise.all()
該方法接收單個迭代物件(最常見的就是陣列)作為引數,並返回一個 Promise。這個可迭代物件的元素都是 Promise,只有在它們都完成後,所返回的 Promise 才會被完成。
-
當所有的 Promise 均為完成態,將會返回一個包含所有結果的陣列。
-
只要有一個被拒絕,就不會返回陣列,只會返回最先被拒絕的那個 Promise 的原因
let p1 = new Promise((resolve, reject) => {
resolve(42);
});
let p2 = new Promise((resolve, reject) => {
reject(43);
});
let p3 = new Promise((resolve, reject) => {
reject(44);
});
let p4 = new Promise((resolve, reject) => {
resolve(45);
});
// 全部完成,返回陣列
let p5 = Promise.all([p1, p4]);
p5.then(value => console.log(value)); // [42, 45]
// 只要有一個出錯,就不會返回陣列,且只會返回最先被拒絕的那個 Promise 的原因
let p6 = Promise.all([p1, p2, p3, p4]);
p6.catch(value => console.log(value)); // 43
複製程式碼
Promise.race()
該方法同樣接收單個迭代物件(最常見的就是陣列)作為引數,不同的是,該方法只要檢測到任意一個被解決,該方法就會做出響應。因此一個有趣的例子是把 請求介面
和一個 setTimeout
進行競逐,如果 setTimeout
先做出響應,就證明這個介面請求超時。
const p = Promise.race([
fetch('/some-api'),
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('請求超時')), 3000);
}),
]);
p.then(value => {
console.log(value);
}).catch(reason => {
console.log(reason);
});
複製程式碼
Promise 的侷限性
看起來 Promise 很美好,解決了回撥函式的種種問題,但它也有自己的侷限性。
-
一旦建立一個 Promise 併為其註冊完成/拒絕處理函式,Promise 將無法被取消。
-
當處於 pending 狀態時,你無法得知當前進展到哪一塊
-
因為 Promise 只能被決議一次(完成或拒絕),如果某些事件不斷髮生,stream 模式會更合適。
-
如果不設定回撥函式,Promise 內部丟擲的錯誤,不會反應到外部。
手撕程式碼
手撕程式碼的之前可以參照一下後面的 Promise A+ 規範翻譯,最好還是自己去官網翻譯一遍,這樣寫起來才會得心應手。下面的程式碼幾乎每句都加了註釋,並且連結到每一條規範。
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class Promise {
constructor(executor) {
// state 的初始狀態為等待態
this.state = PENDING;
// 成功的值 (1.3)
this.value = undefined;
// 失敗的原因 (1.5)
this.reason = undefined;
// 因為 then 在相同的 promise 可以被呼叫多次,所以需要將所有的 onFulfilled 存到陣列 (2.2.6)
this.onResolvedCallbacks = [];
// 因為 then 在相同的 promise 可以被呼叫多次,所以需要將所有的 onRejected 存到陣列 (2.2.6)
this.onRejectedCallbacks = [];
const resolve = value => {
// 只有當前是 pending,才可能轉換為 fulfilled
// 並且不能再轉換成其他任何狀態,且必須擁有一個不可變的值
if (this.state === PENDING) {
this.state = FULFILLED;
this.value = value;
// onFulfilled 回撥按原始呼叫順序依次執行 (2.2.6.1)
this.onResolvedCallbacks.forEach(fn => fn());
}
};
const reject = reason => {
// 只有當前是 pending,才可能轉換為 rejected
// 並且不能再轉換成其他任何狀態,且必須擁有一個不可變的原因
if (this.state === PENDING) {
this.state = REJECTED;
this.reason = reason;
// onRejectec 回撥按原始呼叫順序依次執行 (2.2.6.1)
this.onRejectedCallbacks.forEach(fn => fn()); // (2.2.6.2)
}
};
// 若 executor 報錯,直接執行 reject()
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// onFulfilled 和 onRejected 都是可選引數 (2.2.1)
// 如果 onFulfilled 不是函式,則必須將它忽略 (2.2.1.1)
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : value => value;
// 如果 onRejected 不是函式,則必須將它忽略 (2.2.1.2)
onRejected =
typeof onRejected === 'function'
? onRejected
: err => {
throw err;
};
// 為了做到鏈式呼叫,規定每個 then 方法必須返回一個 promise,稱為 promise2
const promise2 = new Promise((resolve, reject) => {
// 在 promise 完成後方可呼叫 onFulfilled (2.2.2)
if (this.state === FULFILLED) {
// onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
setTimeout(() => {
try {
// value 作為完成函式的第一個引數 (2.2.2.1)
// onFulfilled 函式被記做 x (2.2.7.1)
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
// 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
reject(e);
}
}, 0);
}
// 在 promise 被拒絕後方可呼叫 onRejected (2.2.3)
if (this.state === REJECTED) {
// onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
setTimeout(() => {
try {
// reason 作為拒絕函式的第一個引數 (2.2.3.1)
// onRejected 函式被記做 x (2.2.7.1)
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
// 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
reject(e);
}
}, 0);
}
if (this.state === PENDING) {
this.onResolvedCallbacks.push(() => {
// onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
// 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
// onFulfilled/onRejected 必須被非同步呼叫,因此我們用延時函式模擬 (2.2.4)
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
// 如果 onFulfilled/onRejected 丟擲異常,則 promise2 必須拒絕執行,並返回拒因 e (2.2.7.2)
reject(e);
}
}, 0);
});
}
});
// 返回 promise2 (2.2.7)
return promise2;
}
// catch 實際是 then 的語法糖
catch(fn) {
return this.then(null, fn);
}
finally(fn) {
return this.then(
value => Promise.resolve(fn()).then(() => value),
reason =>
Promise.resolve(fn()).then(() => {
throw reason;
}),
);
}
}
const resolvePromise = (promise2, x, resolve, reject) => {
// 如果 promise 和 x 指向同一個物件,將以 TypeError 作為拒因拒絕執行 promise (2.3.1)
if (x === promise2) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// onFulfilled 和 onRejected 只能被呼叫一次,因此這裡加一個 flag 作為判斷 (2.2.2.3 & 2.2.3.3)
let isCalled = false;
// 如果 x 是一個物件或者是一個函式 (2.3.3)
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
// (2.3.3.1)
const then = x.then;
// 如果 then 是函式,就以 x 作為 this 呼叫它 (2.3.3.2 & 2.3.3.3)
if (typeof then === 'function') {
// 後面接收兩個回撥,第一個是成功的回撥,第二個是失敗的回撥 (2.3.3.3)
then.call(
x,
y => {
if (isCalled) return;
isCalled = true;
// 如果 resolvePromise 以 y 為引數被呼叫,執行 [[Resolve]](promise, y) (2.3.3.3.1)
resolvePromise(promise2, y, resolve, reject);
},
r => {
if (isCalled) return;
isCalled = true;
// 如果 rejectPromise 以 r 為原因被呼叫,則以拒因 r 拒絕 promise (2.3.3.3.2)
reject(r);
},
);
} else {
// 如果 then 不是個函式,則以 x 為引數執行 promise (2.3.3.4)
resolve(x);
}
} catch (e) {
if (isCalled) return;
isCalled = true;
// 如果取 x.then 報錯,則以 e 為拒因拒絕 `promise` (2.3.3.2)
reject(e);
}
}
// 如果 then 不是個函式或者物件,則以 x 為引數執行 promise (2.3.4)
else {
resolve(x);
}
};
// Promise.resolve
Promise.resolve = function(promises) {
if (promises instanceof Promise) {
return promises;
}
return new Promise((resolve, reject) => {
if (promises && promises.then && typeof promises.then === 'function') {
setTimeout(() => {
promises.then(resolve, reject);
});
} else {
resolve(promises);
}
});
};
// Promise.reject
Promise.reject = reason => new Promise((resolve, reject) => reject(reason));
// Promise.all
Promise.all = promises => {
return new Promise((resolve, reject) => {
let resolvedCounter = 0;
let promiseNum = promises.length;
let resolvedValues = new Array(promiseNum);
for (let i = 0; i < promiseNum; i += 1) {
(i => {
Promise.resolve(promises[i]).then(
value => {
resolvedCounter++;
resolvedValues[i] = value;
if (resolvedCounter === promiseNum) {
return resolve(resolvedValues);
}
},
reason => {
return reject(reason);
},
);
})(i);
}
});
};
//race方法
Promise.race = promises => {
return new Promise((resolve, reject) => {
if (promises.length === 0) {
return;
} else {
for (let i = 0, l = promises.length; i < l; i += 1) {
Promise.resolve(promises[i]).then(
data => {
resolve(data);
return;
},
err => {
reject(err);
return;
},
);
}
}
});
};
複製程式碼
最後全域性安裝 yarn global add promises-aplus-tests
,插入下面這段程式碼,然後使用 promises-aplus-tests 該檔案的檔名
來驗證你手寫的 Promise 是否符合 Promises A+ 規範。
Promise.defer = Promise.deferred = function() {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
};
module.exports = Promise;
複製程式碼
附錄:[全文翻譯] Promises/A+ 規範
一個開放、可靠且通用的 JavaScript Promise 標準。由開發者制定,供開發者參考。
promise 代表著一個非同步操作的最終結果,與之互動的主要方式是它的 then
方法,該方法註冊了兩個回撥函式,用於接收 promise 最終的值或者失敗的原因。
該規範詳細描述了 then
方法的行為,所有遵循 Promises/A+ 規範實現的 promise 均可以本標準作為參照基礎來實施。因此,這份規範是很穩定的。雖然 Promises/A+ 組織偶爾會修訂這份規範,但大多是為了處理一些特殊的邊界情況。這些改動都是微小且向下相容的。如果我們要進行大規模不相容的更新,我們一定會在事先進行謹慎地考慮、詳盡的探討和嚴格的測試。
最後,核心的 Promises/A+ 規範不會提供如何建立、解決和拒絕 promise,而是專注於提供一個通用的 then
方法。上述對於 promises 的操作方法將來在其他規範中可能會提及。
1. 術語
1.1. 'promise' 是一個擁有 then
方法的物件或者函式,且其行為符合此規範。
1.2. 'thenable' 是一個用來定義 then
方法的物件或者函式。
1.3. 'value' 是任何一個合法的 JavaScript 值 (包括 undefined
,thenable 或者 promise)
1.4. 'exception' 是一個使用 throw 語句丟擲的值
1.5. 'reason' 表明了一個 promise 為什麼會被拒絕
2. 要求
2.1. Promise 狀態
promise 必須是三個狀態之一:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。
-
2.1.1. 當前狀態為 pending 時,一個 promise:
- 2.1.1.1 可以轉換成 fulfilled 或者 rejected 狀態
-
2.1.2. 當前狀態為 fulfilled 時,一個 promise:
-
2.1.2.1 不能再轉換成其他任何狀態
-
2.1.2.2 必須擁有一個不可變的值
-
-
2.1.3. 當前狀態為 rejected 時,一個 promise:
-
2.1.3.1 不能再轉換成其他任何狀態
-
2.1.3.2 必須擁有一個不可變的原因
-
這裡的不可變指的是恆等(即可用 === 判斷相等),而不是意味著更深層次的不可變。(即當 value 或者 reason 為引用型別時,只要求引用地址相等即可,但屬性值可以被修改)
2.2. then
方法
promise 必須提供一個 then
方法以訪問它當前或最終的值或被拒絕的原因。
一個 promise 的 then
方法接收兩個引數:
promise.then(onFulfilled, onRejected);
複製程式碼
-
2.2.1
onFulfilled
和onRejected
都是可選引數。-
2.2.1.1 如果
onFulfilled
不是個函式,它將被忽略 -
2.2.1.2 如果
onRejected
不是個函式,它將被忽略
-
-
2.2.2 如果
onFulfilled
是一個函式:-
2.2.2.1 它必須在
promise
完成式後被呼叫,並且以promise
的值作為它的第一個引數。 -
2.2.2.2 在
promise
未完成前不可呼叫 -
2.2.2.3 此函式僅可呼叫一次
-
-
2.2.3 如果
onRejected
是一個函式:-
2.2.3.1 它必須在
promise
被拒絕後被呼叫,並且以promise
的原因作為它的第一個引數。 -
2.2.3.2 在
promise
未被拒絕前不可呼叫 -
2.2.3.3 此函式僅可呼叫一次
-
-
2.2.4
onFulfilled
和onRejected
只有在 執行上下文 堆疊僅包含平臺程式碼時才可被呼叫。[1] -
2.2.5
onFulfilled
和onRejected
必須被作為函式呼叫 (即沒有 this 值)。[2] -
2.2.6
then
在相同的 promise 可以被呼叫多次-
2.2.6.1 當
promise
是完成態, 所有相應的onFulfilled
回撥必須按其原始呼叫的順序執行。 -
2.2.6.2 當
promise
是拒絕態,所有相應的onRejected
回撥必須按其原始呼叫的順序執行。
-
-
2.2.7 每個
then
方法必須返回一個 promise [3]。promise2 = promise1.then(onFulfilled, onRejected); 複製程式碼
-
2.2.7.1 如果
onFulfilled
或者onRejected
返回一個值x
,則執行下面的 Promise 解決過程:[[Resolve]](promise2, x)
-
2.2.7.2 如果
onFulfilled
或者onRejected
丟擲一個異常e
,則promise2
必須拒絕執行,並返回拒因e
-
2.2.7.3 如果
onFulfilled
不是函式且promise1
成功執行,promise2
必須成功執行並返回相同的值 -
2.2.7.4 如果
onRejected
不是函式且promise1
拒絕執行,promise2
必須拒絕執行並返回相同的拒因
-
2.3. Promise 解決過程
Promise 解決過程是一個抽象的操作,它接收一個 promise 和一個值,我們可以表示為 [[Resolve]](promise, x)
,如果 x
是一個 thenable 的物件,解決程式將試圖接受 x
的狀態,否則用 x
的值來執行 promise
。
這種對 thenales 的處理使得 promise 的實現更加有普適性,只要它暴露出一個相容 Promises/A+ 規範的 then
方法。它還允許讓遵循 Promise/A+ 規範的實現和不太規範但可用的實現良好共存。
為了執行 [[Resolve]](promise, x)
,要執行下面的步驟:
-
2.3.1 如果
promise
和x
指向同一個物件,將以TypeError
作為拒因拒絕執行promise
。 -
2.3.2 如果
x
是一個 promise,那麼將 promise 將接受它的狀態 [4]:-
2.3.2.1 如果
x
是等待態,promise
必須保留等待狀態直到x
被完成或者被拒絕。 -
2.3.2.2 如果
x
是完成態,用相同的值執行promise
-
2.3.2.3 如果
x
是拒態,用相同的原因拒絕promise
-
-
2.3.3 如果
x
是一個物件或者是一個函式,-
2.3.3.1 把
x.then
賦值給then
。[5] -
2.3.3.2 如果取
x.then
的值時丟擲錯誤e
,則以e
為拒因拒絕promise
-
2.3.3.3 如果
then
是函式,將x
作為函式的作用域this
來呼叫它。傳遞兩個回撥函式作為引數,第一個引數叫做resolvePromise
,第二個引數叫做rejectPromise
:-
2.3.3.3.1 如果
resolvePromise
以y
為引數被呼叫,執行[[Resolve]](promise, y)
-
2.3.3.3.2 如果
rejectPromise
以r
為原因被呼叫,則以拒因r
拒絕 promise -
2.3.3.3.3 如果
resolvePromise
和rejectPromise
都被呼叫,或者被同一引數呼叫了多次,則優先採用首次呼叫並忽略剩下的呼叫。 -
2.3.3.3.4 如果呼叫
then
丟擲一個異常e
-
2.3.3.3.4.1 如果
resolvePromise
和rejectPromise
都被呼叫,則忽略掉它 -
2.3.3.3.4.2 否則,以
e
為拒因拒絕這個promise
-
-
-
2.3.3.4 如果
then
不是個函式,則以x
為引數執行promise
-
-
2.3.4 如果
then
不是個函式或者物件,則以x
為引數執行promise
如果一個 promise 被一個迴圈的 thenable 鏈中的物件解決,而 [[Resolve]](promise, thenable)
的遞迴性質又使得其被再次呼叫,根據上述的演算法將會陷入無限遞迴之中。演算法雖不強制要求,但也鼓勵施者檢測這樣的遞迴是否存在,若檢測到存在則以一個可識別的 TypeError 為拒因來拒絕 promise [6]。
3. 註釋
這裡的“平臺程式碼”意味著引擎,環境和 promise 實施程式碼,在實踐中要確保
onFulfilled
和onRejected
非同步執行,且應該在then
方法被呼叫的那一輪事件迴圈之後的新執行棧中執行。這個事件佇列可以採用“巨集任務(macro-task)”機制,類似於setTimeOut
或者setImmediate
,也可以使用“微任務(micro-task)”機制來實現,類似於MutationObserver
或process.nextTick
。因為 promise 實現被認為是平臺程式碼,所以它本身可能包含一個任務排程佇列或跳板,在其中呼叫處理程式。 ↩︎在嚴格模式下
this
為undefined
,而在非嚴格模式中,this
為全域性物件。 ↩︎程式碼實現在滿足所有要求的情況下可以允許
promise2 === promise1
。每個實現都要文件說明其是否允許以及在何種條件下允許promise2 === promise1
。 ↩︎總體來說,如果
x
符合當前實現,我們才認為它是真正的 promise 。這一規則允許那些特例實現接受符合已知要求的 Promises 狀態。 ↩︎這步我們先是儲存了一個指向
x.then
的引用,然後測試並呼叫該引用,以避免多次訪問x.then
屬性。這種預防措施確保了該屬性的一致性,因為其值可能在檢索呼叫時被改變。 ↩︎實現不應該對 thenable 鏈的深度設限,並假定超出本限制的遞迴就是無限迴圈。只有真正的迴圈遞迴才應能導致
TypeError
異常;如果一條無限長的鏈上 thenable 均不相同,那麼遞迴下去永遠是正確的行為。 ↩︎