Promise 學習筆記四 :: 手寫 Promise

Promise 學習筆記四 :: 手寫 Promise
Promise 學習筆記四 :: 手寫 Promise

上篇介紹了Promise的規範以及執行的邏輯,接下來將一步步遵循規範來開始寫我們自己的Promise吧!

為了跟原生Promise做出區別,以下我們實作的Promise將以NPromise命名。

Promise在建構時需傳入一個閉包函式,該函式提供resolve及reject用來改變Promise的狀態。

改變狀態將遵循Promise/A+規範:

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.
class NPromise{
	constructor(executor){
		const resolve = (value) => {
			
		};
		const reject = (reason) => {
			
		};
		
		try{
			executor(resolve, reject);
		}catch(error){
			reject(error);
		}
	}
}
class NPromise{
	static PENDING = 'pending';
	static FULFILLED = 'fulfilled';
	static REJECTED = 'rejected';
	constructor(executor){
		this.state = NPromise.PENDING;
		this.value = undefined;
		const resolve = (value) => {
			this.value = value;
		};
		const reject = (reason) => {
			this.value = reason;
		};
		
		try{
			executor(resolve, reject);
		}catch(error){
			reject(error);
		}
	}
}
class NPromise{
	static PENDING = 'pending';
	static FULFILLED = 'fulfilled';
	static REJECTED = 'rejected';
	constructor(executor){
		this.state = NPromise.PENDING;
		this.value = undefined;
		const resolve = (value) => {
			if(this.state !== NPromise.PENDING){
				return;
			}
			this.state = NPromise.FULFILLED;
			this.value = value;
		};
		const reject = (reason) => {
			if(this.state !== NPromise.PENDING){
				return;
			}
			this.state = NPromise.REJECTED;
			this.value = reason;
		};
		
		try{
			executor(resolve, reject);
		}catch(error){
			reject(error);
		}
	}
}

上述程式碼已經可以完成初步的運作。

new NPromise((resolve, reject) => {
	resolve(`nicklabs.cc`);
});
// NPromise {state: 'fulfilled', value: 'nicklabs.cc'}

new NPromise((resolve, reject) => {
	reject(`nicklabs.cc`);
});
// NPromise {state: 'rejected', value: 'nicklabs.cc'}


new NPromise((resolve, reject) => {
	resolve(`nicklabs.cc`);
	resolve(`nicklabs.cc`);
});
// NPromise {state: 'fulfilled', value: 'nicklabs.cc'}
// 重複呼叫resolve,也只會執行一次
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.
  • 實作then且讓onFulfilled及onRejected設計為可選參數,若不是傳入function則以預設函式代替。
then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
}
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.
  • resolve及reject會的導致狀態改變所以用this.state的狀態來決定是否執行onFulfilled或onRejected。
  • 若onFulfilled是一個函式,則必須在已解決時呼叫onFulfilled並將解決的值傳入onFulfilled的第一個參數,在已解決之前不得呼叫且只能呼叫一次。
  • 若onReject是一個函式,則必須在拒絕時呼叫onRejected並將錯誤原因傳入onRejected的第一個參數,在拒絕之前不得呼叫且只能呼叫一次。
then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	if(this.state === NPromise.PENDING){
		
	}else if(this.state === NPromise.FULFILLED){
		onFulfilled(this.value);
	}else if(this.state === NPromise.REJECTED){
		onRejected(this.value);
	}
}
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]

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.
  • onFulfilled及onRejected必須在下一個隊列中執行,可使用「setTimeout、queueMicrotask、MutationObserver」實現將觸發時機加入到下一個事件隊列中。
  • 將onFulfilled和onRejected做完函式作為函式執行(即没有 this 值)
then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	if(this.state === NPromise.PENDING){
		
	}else if(this.state === NPromise.FULFILLED){
		queueMicrotask(() => {
			onFulfilled(this.value);
		});
	}else if(this.state === NPromise.REJECTED){
		queueMicrotask(() => {
			onRejected(this.value);
		});
	}
}
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.
  • 宣告變數handlers陣列用以儲存onFulfilled、onRejected。
  • 在pending狀態時還未執行需要先把onFulfilled及onRejected儲存起來,當resolve或reject時才執行。
constructor(executor){
	this.state = NPromise.PENDING;
	this.value = undefined;
	this.handlers = [];
	const resolve = (value) => {
		if(this.state !== NPromise.PENDING){
			return;
		}
		this.state = NPromise.FULFILLED;
		this.value = value;
		this.handlers.forEach((handler) => {
			handler.onFulfilled(value);
		})
	};
	const reject = (reason) => {
		if(this.state !== NPromise.PENDING){
			return;
		}
		this.state = NPromise.REJECTED;
		this.value = reason;
		this.handlers.forEach((handler) => {
			handler.onRejected(reason);
		})
	};
	
	try{
		executor(resolve, reject);
	}catch(error){
		reject(error);
	}
}

then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	if(this.state === NPromise.PENDING){
		this.handlers.push({
			onFulfilled: () => {
				onFulfilled(this.value)
			},
			onRejected: () => {
				onRejected(this.value)
			}
		});
	}else if(this.state === NPromise.FULFILLED){
		queueMicrotask(() => {
			onFulfilled(this.value);
		});
	}else if(this.state === NPromise.REJECTED){
		queueMicrotask(() => {
			onRejected(this.value);
		});
	}
}
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.
  • then的返回值是一個新的Promise
  • 當Promise1的onFulfilled或onRejected執行完成後會回傳一個值為x,同時將Promise2變更為已解決並將x傳入到onFulfilled中。
  • 當執行Promise1的onFulfilled或onRejected的過程中發生錯誤會拋出異常e,此時將Promise2變更為拒絕並將e傳入onRejected中。
  • 當Promise1已解決但then中onFulfilled不是函式,則會將已解決的值傳遞到Promise2,此行為即是穿透。
  • 當Promise1拒絕但then中onRejected不是函式,則會將拒絕拋出的異常傳遞到Promise2,此行為即是穿透。
then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	return new NPromise((resolve, reject) => {
		if(this.state === NPromise.PENDING){
			this.handlers.push({
				onFulfilled: () => {
					resolve(onFulfilled(this.value))
				},
				onRejected: () => {
					resolve(onRejected(this.value))
				}
			});
		}else if(this.state === NPromise.FULFILLED){
			queueMicrotask(() => {
				try{
					resolve(onFulfilled(this.value));
				}catch(error){
					reject(error);
				}
			});
		}else if(this.state === NPromise.REJECTED){
			queueMicrotask(() => {
				try{
					resolve(onRejected(this.value));
				}catch(error){
					reject(error);
				}
			});
		}
	});
}

此時then方法已大致完成,完整程式碼如下:

class NPromise{
	static PENDING = 'pending';
	static FULFILLED = 'fulfilled';
	static REJECTED = 'rejected';
	constructor(executor){
		this.state = NPromise.PENDING;
		this.value = undefined;
		this.handlers = [];
		const resolve = (value) => {
			if(this.state !== NPromise.PENDING){
				return;
			}
			this.state = NPromise.FULFILLED;
			this.value = value;
			this.handlers.forEach((handler) => {
				handler.onFulfilled(value);
			})
		};
		const reject = (reason) => {
			if(this.state !== NPromise.PENDING){
				return;
			}
			this.state = NPromise.REJECTED;
			this.value = reason;
			this.handlers.forEach((handler) => {
				handler.onRejected(reason);
			})
		};
		 
		try{
			executor(resolve, reject);
		}catch(error){
			reject(error);
		}
	}
	
	then(onFulfilled, onRejected){
		if(typeof onFulfilled !== 'function'){
			onFulfilled = (value) => {
				return value;
			}
		}
		if(typeof onRejected !== 'function'){
			onRejected = (reason) => {
				throw reason;
			}
		}
		return new NPromise((resolve, reject) => {
			if(this.state === NPromise.PENDING){
				this.handlers.push({
					onFulfilled: () => {
						resolve(onFulfilled(this.value))
					},
					onRejected: () => {
						resolve(onRejected(this.value))
					}
				});
			}else if(this.state === NPromise.FULFILLED){
				queueMicrotask(() => {
					try{
						resolve(onFulfilled(this.value));
					}catch(error){
						reject(error);
					}
				});
			}else if(this.state === NPromise.REJECTED){
				queueMicrotask(() => {
					try{
						resolve(onRejected(this.value));
					}catch(error){
						reject(error);
					}
				});
			}
		});
	}
}

目前程式碼看起來有多處相似之處,可以將重複的部分提取出來部分複用。

  • then中判斷state的部分有不少相似的程式碼可以重構:
  1. 判斷狀態換到queueMicrotask的閉包中判斷
  2. 統一將閉包存入handlers再執行
  3. 建立runHandlers函式執行handlers
then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	return new NPromise((resolve, reject) => {
		this.handlers.push(() => {
			queueMicrotask(() => {
				try{
					const resultHandler = (this.state === NPromise.FULFILLED) ? onFulfilled : onRejected;
					const result = resultHandler(this.value);
					resolve(result);
				}catch(error){
					reject(error);
				}
			})
		});
		this.runHandlers();
	});
}

runHandlers(){
	if(this.state === NPromise.PENDING){
		return;
	}
	this.handlers.forEach(handler => handler());
	this.handlers = [];
}
  • constructor中resolve及reject執行邏輯有不少相似的程式碼可以重構
  1. 重構resolve, reject 抽出邏輯到updateState函式
  2. 上方已重構抽出runHandlers函式,在updateState中可直接使用
constructor(executor){
	this.state = NPromise.PENDING;
	this.value = undefined;
	this.handlers = [];
	
	try{
		executor(
			value => this.updateState(NPromise.FULFILLED, value),
			reject => this.updateState(NPromise.REJECTED, reject)
		);
	}catch(error){
		this.updateState(NPromise.REJECTED, error);
	}
}

updateState(state, value){
	if(this.state !== NPromise.PENDING){
		return;
	}
	this.state = state;
	this.value = value;
	this.runHandlers();
}

重構後完整程式碼:

class NPromise{
    static PENDING = 'pending';
    static FULFILLED = 'fulfilled';
    static REJECTED = 'rejected';
    constructor(executor){
        this.state = NPromise.PENDING;
        this.value = undefined;
        this.handlers = [];

        try{
            executor(
                value => this.updateState(NPromise.FULFILLED, value),
                reject => this.updateState(NPromise.REJECTED, reject)
            );
        }catch(error){
            this.updateState(NPromise.REJECTED, error);
        }
    }

    updateState(state, value){
        if(this.state !== NPromise.PENDING){
            return;
        }
        this.state = state;
        this.value = value;
        this.runHandlers();
    }

    then(onFulfilled, onRejected){
        if(typeof onFulfilled !== 'function'){
            onFulfilled = (value) => {
                return value;
            }
        }
        if(typeof onRejected !== 'function'){
            onRejected = (reason) => {
                throw reason;
            }
        }
        return new NPromise((resolve, reject) => {
            this.handlers.push(() => {
                queueMicrotask(() => {
                    try{
                        const resultHandler = (this.state === NPromise.FULFILLED) ? onFulfilled : onRejected;
                        const result = resultHandler(this.value);
                        resolve(result);
                    }catch(error){
                        reject(error);
                    }
                })
            });
            this.runHandlers();
        });
    }

    runHandlers(){
        if(this.state === NPromise.PENDING){
            return;
        }
        this.handlers.forEach(handler => handler());
        this.handlers = [];
    }
}
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.

到此手寫Promise已經完成大部分功能了,接下來需要處理最後兩點:

  • 如果promise和x引用同一個物件,則會以TypeError的理由拒絕
  • 若x是thenable則執行then函式並傳入resolve及reject
then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	const promise =  new NPromise((resolve, reject) => {
		this.handlers.push(() => {
			queueMicrotask(() => {
				try{
					const resultHandler = (this.state === NPromise.FULFILLED) ? onFulfilled : onRejected;
					const result = resultHandler(this.value);
					if(promise === result){
						throw TypeError(`Chaining cycle detected for promise`);
					}
					resolve(result);
				}catch(error){
					reject(error);
				}
			})
		});
		this.runHandlers();
	});
	return promise;
}

需要判斷是否是thenable我們需要建立一個函式來幫助判斷

isPromiseLike(obj){
	return typeof obj === 'object' && typeof obj.then === 'function';
}

修改過後的then函式

then(onFulfilled, onRejected){
	if(typeof onFulfilled !== 'function'){
		onFulfilled = (value) => {
			return value;
		}
	}
	if(typeof onRejected !== 'function'){
		onRejected = (reason) => {
			throw reason;
		}
	}
	const promise =  new NPromise((resolve, reject) => {
		this.handlers.push(() => {
			queueMicrotask(() => {
				try{
					const resultHandler = (this.state === NPromise.FULFILLED) ? onFulfilled : onRejected;
					const result = resultHandler(this.value);
					if(promise === result){
						throw TypeError(`Chaining cycle detected for promise`);
					}
					if(this.isPromiseLike(result)){
						result.then(resolve, reject)
					}else{
						resolve(result);
					}
				}catch(error){
					reject(error);
				}
			})
		});
		this.runHandlers();
	});
	return promise;
}
class NPromise {
    static PENDING = 'pending';
    static FULFILLED = 'fulfilled';
    static REJECTED = 'rejected';

    constructor(executor) {
        this.state = NPromise.PENDING;
        this.value = undefined;
        this.handlers = [];

        try{
            executor(
                value => this.updateState(NPromise.FULFILLED, value),
                reject => this.updateState(NPromise.REJECTED, reject)
            );
        }catch(error){
            this.updateState(NPromise.REJECTED, error);
        }
    }

    updateState(state, value) {
        if(this.state !== NPromise.PENDING){
            return;
        }
        this.state = state;
        this.value = value;
        this.runHandlers();
    }

    then(onFulfilled, onRejected){
        if(typeof onFulfilled !== 'function'){
            onFulfilled = (value) => {
                return value;
            }
        }
        if(typeof onRejected !== 'function'){
            onRejected = (reason) => {
                throw reason;
            }
        }
        const promise =  new NPromise((resolve, reject) => {
            this.handlers.push(() => {
                queueMicrotask(() => {
                    try{
                        const resultHandler = (this.state === NPromise.FULFILLED) ? onFulfilled : onRejected;
                        const result = resultHandler(this.value);
                        if(promise === result){
                            throw TypeError(`Chaining cycle detected for promise`);
                        }
                        if(this.isPromiseLike(result)){
                            result.then(resolve, reject)
                        }else{
                            resolve(result);
                        }
                    }catch(error){
                        reject(error);
                    }
                })
            });
            this.runHandlers();
        });
        return promise;
    }

    runHandlers() {
        if(this.state === NPromise.PENDING){
            return;
        }
        this.handlers.forEach(handler => handler());
        this.handlers = [];
    }

    isPromiseLike(obj){
        return typeof obj === 'object' && typeof obj.then === 'function';
    }
}

Promise A+ 規範

MDN Promise

ECMA-262

作者頭像
Nick

是一位專業的網站開發者,不止擅長前端技術,也精通後端技術。