Promise 學習筆記三 :: 剖析 Promise 運作原理

前面兩篇文章的介紹大致知道Promise如何操作及運作,為了能更瞭解細節及運作邏輯,將遵循Promise/A+規範手動來打造實作一個Promise。
Promise定義
以下是規範原文:
1.1. "promise" is an object or function with a then method whose behavior conforms to this specification.
1.2. "thenable" is an object or function that defines a then method.
1.3. "value" is any legal JavaScript value (including undefined, a thenable, or a promise).
1.4. "exception" is a value that is thrown using the throw statement.
1.5. "reason" is a value that indicates why a promise was rejected.
- 「promise」是一個物件或函式,且then函式的行為符合此規範
- 定義了then方法的物件或函式即是「thenable」
- 「value」是一個合法的JavaScript值,包括(undefinded, thenable, promise)
- 「exception」指的是該throw拋出值
- 「reason」指的是Promise被拒絕的原因
Promise狀態的改變
規範原文:
2.1.1. When pending, a promise:
2.1.1.1. may transition to either the fulfilled or rejected state.
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.
Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.
- 當狀態為pending時可以轉變為fulfilled或rejected。
- 當狀態為fulfilled時無法轉換成其他狀態,必須有value且該value不得改變。
- 當狀態為rejected時無法轉換成其他狀態,必須有reason且該reason不得改變。
這邊說的不得改變(即是 ===),但不意味著深度的不變性。
let obj = { a: 1 };
let promise = new Promise((resolve, reject) => {
resolve(obj);
});
promise.then((value) => {
console.log(value); // { a: 1 }
// 修改物件的屬性
value.a = 2;
console.log(value); // { a: 2 }
});
此時說的不得改變是指他的參照不能改變,但若value是物件則該物件的屬性仍然可以被修改。
Promise.prototype.then 函式解析
Promise必須提供then函式來存取當前或最終的值或原因。
promise.then(onFulfilled, onRejected)
then函式能接收兩個參數分別為onFulfilled及onRejected。
規範原文:
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.
2.2.2. If onFulfilled is a function:
2.2.2.1. it must be called after promise is fulfilled, with promise’s value as its first argument.
2.2.2.2. it must not be called before promise is fulfilled.
2.2.2.3. it must not be called more than once.
2.2.3. If onRejected is a function,
2.2.3.1. it must be called after promise is rejected, with promise’s reason as its first argument.
2.2.3.2. it must not be called before promise is rejected.
2.2.3.3. it must not be called more than once.
2.2.4. onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
2.2.5. onFulfilled and onRejected must be called as functions (i.e. with no this value). [3.2]
2.2.6. then may be called multiple times on the same promise.
2.2.6.1. If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then.
2.2.6.2. If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
2.2.7. then must return a promise [3.3]. "promise2 = promise1.then(onFulfilled, onRejected);"
2.2.7.1. If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
2.2.7.2. If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason.
2.2.7.3. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
2.2.7.4. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.
3.1. Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
3.2. That is, in strict mode this will be undefined inside of them; in sloppy mode, it will be the global object.
3.3. Implementations may allow promise2 === promise1, provided the implementation meets all requirements. Each implementation should document whether it can produce promise2 === promise1 and under what conditions.
- onFulfilled及onRejected皆是可選參數,若傳入的onFulfilled或onRejected不是函式,則忽略並執行預設函式。
- 若onFulfilled是一個函式,則必須在已解決時呼叫onFulfilled並將解決的值傳入onFulfilled的第一個參數,在已解決之前不得呼叫且只能呼叫一次。
- 若onReject是一個函式,則必須在拒絕時呼叫onRejected並將錯誤原因傳入onRejected的第一個參數,在拒絕之前不得呼叫且只能呼叫一次。
- onFulfilled及onRejected必須在下一個隊列中執行,可使用「setTimeout、queueMicrotask、MutationObserver」實現將觸發時機加入到下一個事件隊列中。
- 必須將onFulfilled和onRejected 做完函式執行 (沒有this指向)
- 同一個Promise可以呼叫多次then函式 (註一)
- then的返回值是一個新的Promise
- 當Promise1的onFulfilled或onRejected執行完成後會回傳一個值為x,同時將Promise2變更為已解決並將x傳入到onFulfilled中。
- 當執行Promise1的onFulfilled或onRejected的過程中發生錯誤會拋出異常e,此時將Promise2變更為拒絕並將e傳入onRejected中。 (註二)
- 當Promise1已解決但then中onFulfilled不是函式,則會將已解決的值傳遞到Promise2,此行為即是穿透。 (註三)
- 當Promise1拒絕但then中onRejected不是函式,則會將拒絕拋出的異常傳遞到Promise2,此行為即是穿透。 (註四)
程式碼範例
(註一):p1 then函式可以呼叫多次,一但解決或失敗將會批次執行。
const p1 = new Promise((resolve, reject) => {
resolve(`nicklabs.cc`);
});
p1.then((value) => {
console.log(`success1: `, value);
});
p1.then((value) => {
console.log(`success2: `, value);
});
// 輸出
// success1: nicklabs.cc
// success2: nicklabs.cc
(註二):在p1 then函式中因語法錯誤導致拋出錯誤,此錯誤會傳遞到p2 then的onRejected。
const p1 = new Promise((resolve) => {
resolve(`nicklabs.cc`);
});
const p2 = p1.then(
(value) => {
console.log(`success1: `, value2);
},
(reason) => {
console.log(`error1: `, reason2);
}
);
const p3 = p2.then(
(value) => {
console.log(`success2: `, value);
},
(reason) => {
console.log(`error2: `, reason);
}
)
// 輸出
(註三):p1 then方法中未傳入onFulfilled,則會將已解決的值直接傳遞到p2 then中。
const p1 = new Promise((resolve, reject) => {
resolve(`nicklabs.cc`);
});
const p2 = p1.then();
p2.then(
(value) => {
console.log(`success 2:`, value);
},
(reason) => {
console.log(`reason 2:`, reason);
}
);
// 輸出
// success 2: nicklabs.cc
(註四):p1 then方法中未傳入onRejected,則會將拒絕的原因直接傳遞到p2 then中。
const p1 = new Promise((resolve, reject) => {
reject(`nicklabs.cc`);
});
const p2 = p1.then();
p2.then(
(value) => {
console.log(`success 2:`, value);
},
(reason) => {
console.log(`reason 2:`, reason);
}
);
// 輸出
// reason 2: nicklabs.cc
解析 Promise 解決或拒絕的過程
規範原文:
The promise resolution procedure is an abstract operation taking as input a promise and a value, which we denote as [[Resolve]](promise, x). If x is a thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise. Otherwise, it fulfills promise with the value x.
This treatment of thenables allows promise implementations to interoperate, as long as they expose a Promises/A+-compliant then method. It also allows Promises/A+ implementations to “assimilate” nonconformant implementations with reasonable then methods.
2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
2.3.2. If x is a promise, adopt its state [3.4]:
2.3.2.1. If x is pending, promise must remain pending until x is fulfilled or rejected.
2.3.2.2. If/when x is fulfilled, fulfill promise with the same value.
2.3.2.3. If/when x is rejected, reject promise with the same reason.
2.3.3. Otherwise, if x is an object or function,
2.3.3.1. Let then be x.then. [3.5]
2.3.3.2. If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
2.3.3.3. If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:
2.3.3.3.1. If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
2.3.3.3.2. If/when rejectPromise is called with a reason r, reject promise with r.
2.3.3.3.3. If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
2.3.3.3.4. If calling then throws an exception e,
2.3.3.3.4.1. If resolvePromise or rejectPromise have been called, ignore it.
2.3.3.3.4.2. Otherwise, reject promise with e as the reason.
2.3.3.4. If then is not a function, fulfill promise with x.
2.3.4. If x is not an object or function, fulfill promise with x.
If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of [[Resolve]](promise, thenable) eventually causes [[Resolve]](promise, thenable) to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and reject promise with an informative TypeError as the reason. [3.6]
3.4. Generally, it will only be known that x is a true promise if it comes from the current implementation. This clause allows the use of implementation-specific means to adopt the state of known-conformant promises.
3.5. This procedure of first storing a reference to x.then, then testing that reference, and then calling that reference, avoids multiple accesses to the x.then property. Such precautions are important for ensuring consistency in the face of an accessor property, whose value could change between retrievals.
3.6. Implementations should not set arbitrary limits on the depth of thenable chains, and assume that beyond that arbitrary limit the recursion will be infinite. Only true cycles should lead to a TypeError; if an infinite chain of distinct thenables is encountered, recursing forever is the correct behavior.
Promise解決的過程及背後運作的邏輯需要兩個東西,一個是Promise另一個是值,將其表示為[[Resolve]](promise, x)。
[[Resolve]](promise.x)
可以理解成
Promise.resolve(x)
- 如果promise和x引用同一個物件,則會以TypeError的理由拒絕(註五)
- 若x是promise且狀態為pending則需等待狀態改變才執行下一步,已解決則將value傳遞給下一個promise,若拒絕則將reason傳遞給下一個promise。(註六)
- 若x是物件或函式且有then函式則執行then函式,以x作為then函式this指向執行並傳入resolve即reject。在執行then中若發生異常則執行reject並傳入reason,且resolve及reject僅能執行一次。
- 如果在執行then的過程中發生異常,則執行reject函式並傳入異常原因,但若先前已執行過reject函式則忽略此次異常。
- 如果then不是函式就以此x傳入resolve解決promise。
- 如果x不是物件或函式就以此x傳入resolve解決promise。
(註五):then返回的promise不能和已解決的值相同,否則可能會造成無限迴圈的問題。
const p1 = new Promise((resolve, reject) => {
resolve(`nicklabs.cc`);
});
const p2 = p1.then(() => {
return p2;
});
// 輸出 TypeError: Chaining cycle detected for promise #<Promise>
(註六):若x為promise則需要等待x處理完才處理下一個promise。
const x = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`nicklabs.cc`);
}, 5000);
});
const p2 = new Promise((resolve, reject) => {
resolve(x);
})
.then(
(value) => {
console.log(`success: `, value);
},
(reason) => {
console.log(`error: `, value);
}
);
這篇大部分篇幅在於剖析Promise/A+規範,為了能讓下一篇手寫實作Promise更夠更加順利。