brief introduction
Before learning, you need to have a basic understanding of Promise. It is assumed that everyone is familiar with Promise
This time, we will take our partners to realize the basic functions of Promise
- Promise's basic skeleton
- Promise then
- Promise. Multiple calls of then
- then chain call
- Implementation of catch
- finally implementation
01 - build basic framework
const PROMISE_STATUS_PENDING = "PROMISE_STATUS_PENDING"; const PROMISE_STATUS_FULFILLED = "PROMISE_STATUS_FULFILLED"; const PROMISE_STATUS_REJECTED = "PROMISE_STATUS_REJECTED"; class ZXPromise { constructor(executor) { this.status = PROMISE_STATUS_PENDING; const resolve = (value) => { if (this.status === PROMISE_STATUS_PENDING) { this.status = PROMISE_STATUS_FULFILLED; console.log(value); } } const rejected = (reason) => { if (this.status === PROMISE_STATUS_PENDING) { this.status = PROMISE_STATUS_REJECTED; console.log(reason); } } executor(resolve, rejected) } } // Preliminarily build Promise's constructor structure const promise = new ZXPromise((resolve, rejected) => { resolve("123"); rejected("wushichu") })
- Because Promise has three states: pending, fulfilled and rejected, we declare three constants here to represent these three states
- In Promise, a callback function needs to be passed. Its parameters include resolve and rejected. After calling resolve, the state will change to fully qualified. When calling rejected, the state will change to rejected
- I have defined a class. We define the required resolve and rejected functions in the constructor, and then pass these two functions into the executor, so that the basic skeleton of Promise has been built, which is very simple
02 - realize Promise's then function
const PROMISE_STATUS_PENDING = "PROMISE_STATUS_PENDING"; const PROMISE_STATUS_FULFILLED = "PROMISE_STATUS_FULFILLED"; const PROMISE_STATUS_REJECTED = "PROMISE_STATUS_REJECTED"; class ZXPromise { constructor(executor) { this.status = PROMISE_STATUS_PENDING; const resolve = (value) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { //Because only the pending state can be changed if(this.status!==PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_FULFILLED; if (this.onfufilled) this.onfufilled(value); }) } } const rejected = (reason) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if(this.status!==PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_REJECTED; if (this.onrejected) this.onrejected(reason); }) } } executor(resolve, rejected) } then(onfufilled, onrejected) { this.onfufilled = onfufilled; this.onrejected = onrejected; } } // Next, start writing the then method const promise = new ZXPromise((resolve, rejected) => { resolve("123"); rejected("wushichu"); }) promise.then((res) => { console.log("res", res); }, (err) => { console.log("err", err); })
- The then method accepts two parameters: on fulfilled and onrejected functions, which correspond to the states of fully and rejected respectively
- One thing to note here is that I use queueMicrotask in both resolve and rejected. The purpose of using queueMicrotask here is to ensure the consistency of sequential execution and ensure that the relevant code is executed after the then method is executed. Here you need to be familiar with micro task queue and macro task queue. I recommend you to read this article
03-Promise.then called multiple times
You can experiment with the code in the previous part. If you call it many times, you will find that there is only the last output, which is used in the timer, and the result will be undefined
p1.then((res) => { console.log("res1", res); }); p1.then((res) => { console.log('res2: ', res); }); setTimeout(() => { p1.then((res) => { console.log("res4", res); }) }, 1000);
Now let's solve the above problem and look at the code
const PROMISE_STATUS_PENDING = "PROMISE_STATUS_PENDING"; const PROMISE_STATUS_FULFILLED = "PROMISE_STATUS_FULFILLED"; const PROMISE_STATUS_REJECTED = "PROMISE_STATUS_REJECTED"; class ZXPromise { constructor(executor) { this.status = PROMISE_STATUS_PENDING; this.value = undefined; this.reason = undefined; this.onfufilled = []; this.onrejected = []; const resolve = (value) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_FULFILLED; this.value = value; this.onfufilled.forEach(fn => { fn(value); }); }) } } const rejected = (reason) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_REJECTED; this.reason = reason; this.onrejected.forEach(fn => { fn(reason); }) }) } } executor(resolve, rejected) } // Next, in order that Promise can be called multiple times for optimization then(onfufilled, onrejected) { if (this.status === PROMISE_STATUS_FULFILLED) { onfufilled(this.value); } if (this.status === PROMISE_STATUS_REJECTED) { onrejected(this.value); } if (this.status === PROMISE_STATUS_PENDING) { this.onfufilled.push(onfufilled); this.onrejected.push(onrejected); } } }
- After the improvement, we need to store the value and reason values of resolve and rejected, so we define these two values
- In order to meet multiple calls, we need to change onfulfilled and onrejected in promise into array storage to meet our multiple calls
- Let me talk about the timer. Because setTimeout is a macro task, micro tasks will be executed after the synchronization code is executed, so the macro task is executed last. Therefore, the code in promise is executed, but the then method wrapped in the timer does not get the result
- Therefore, here I decide to let the code in the timer execute directly without pressing it into the array. Because the code before the timer has been executed and the state of promise has changed, I judge the state of promise in the then method. If it is in the fully and rejected States, the passed function will execute directly
Chained call of 04 then method
To realize the chain call, the then method must return the Promise object again. Speaking of this, do you have any ideas?
const PROMISE_STATUS_PENDING = "PROMISE_STATUS_PENDING"; const PROMISE_STATUS_FULFILLED = "PROMISE_STATUS_FULFILLED"; const PROMISE_STATUS_REJECTED = "PROMISE_STATUS_REJECTED"; class ZXPromise { constructor(executor) { this.status = PROMISE_STATUS_PENDING; this.value = undefined; this.reason = undefined; this.onfufilled = []; this.onrejected = []; const resolve = (value) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_FULFILLED; this.value = value; this.onfufilled.forEach(fn => { fn(value); }); }) } } const rejected = (reason) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_REJECTED; this.reason = reason; this.onrejected.forEach(fn => { fn(reason); }) }) } } try{ executor(resolve, rejected) }catch(err){ console.log(err); } } then(onfufilled, onrejected) { return new ZXPromise((resolve, rejected) => { if (this.status === PROMISE_STATUS_FULFILLED) { try { //If there is a return value in then, it will be used as the value received by the next then const value = onfufilled(this.value); resolve(value); } catch (err) { rejected(err); } } if (this.status === PROMISE_STATUS_REJECTED) { try { const value = onrejected(this.value); resolve(value); } catch (err) { rejected(err); } } if (this.status === PROMISE_STATUS_PENDING) { try { this.onfufilled.push(() => { const value = onfufilled(this.value); resolve(value); }); } catch (err) { rejected(err); } try { this.onrejected.push(() => { const value = onrejected(this.value); resolve(value); }); } catch (err) { rejected(err); } } }) } } const promise = new ZXPromise((resolve, rejected) => { resolve("123"); rejected("wushichu"); }) promise.then((res) => { console.log("res1:", res); return "abc"; }, (err) => { console.log("err1", err); }).then((res) => { console.log("res2", res); }, (err) => { console.log("err2", err); })
- The biggest change is the then method. You can see that I returned ZXPromise again. I wrote it clearly in the code
Implementation of 05 catch method
The catch method is actually the syntax sugar of the second parameter of then. What do you understand here?
const PROMISE_STATUS_PENDING = "PROMISE_STATUS_PENDING"; const PROMISE_STATUS_FULFILLED = "PROMISE_STATUS_FULFILLED"; const PROMISE_STATUS_REJECTED = "PROMISE_STATUS_REJECTED"; const execFnWithCatchError = (execFn, value, resolve, reject) => { try { const result = execFn(value); resolve(result); } catch (err) { reject(err); } } class ZXPromise { constructor(executor) { this.status = PROMISE_STATUS_PENDING; this.value = undefined; this.reason = undefined; this.onfufilled = []; this.onrejected = []; const resolve = (value) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_FULFILLED; this.value = value; this.onfufilled.forEach(fn => { fn(value); }); }) } } const rejected = (reason) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_REJECTED; this.reason = reason; this.onrejected.forEach(fn => { fn(reason); }) return this.reason; }) } } executor(resolve, rejected) } then(onfufilled, onrejected) { //This paragraph is to pass on the error code const defaultOnRejected = err => { throw err } onrejected = onrejected || defaultOnRejected return new ZXPromise((resolve, rejected) => { if (this.status === PROMISE_STATUS_FULFILLED && onfufilled) { execFnWithCatchError(onfufilled, this.value, resolve, rejected); } if (this.status === PROMISE_STATUS_REJECTED && onrejected) { execFnWithCatchError(onrejected, this.reason, resolve, rejected); } if (this.status === PROMISE_STATUS_PENDING) { if (onfufilled) this.onfufilled.push(() => { execFnWithCatchError(onfufilled, this.value, resolve, rejected); }); if (onrejected) { this.onrejected.push(() => { execFnWithCatchError(onrejected, this.reason, resolve, rejected); }); } } }) } catch(onrejected) { return this.then(undefined, onrejected); } }
- You can see that the catch code actually has only one line, that is, it calls the then method. Is it quite simple
- Then I think the try catch code has high repeatability, so I extracted it for reuse
- Then let's look at the beginning of then. The onrejected function is given a default value. If then does not pass the second parameter, it will be given a default value of the error handling function. After throwing an error, it will be automatically captured by try catch and rejected. In this way, the sub error will be passed layer by layer until it is finally executed by the catch function
Implementation of 06 finally
finally is the function to be executed at the end. In any case, the implementation is very simple
finally(fn) { return this.then(() => { fn() }, () => { fn() }); }
- Just add this code to the class, because finally cannot receive any resolved and rejected values, so we execute fn in the passed function to avoid the resolved and rejected values being passed to finally
07 - complete code overview
const PROMISE_STATUS_PENDING = "PROMISE_STATUS_PENDING"; const PROMISE_STATUS_FULFILLED = "PROMISE_STATUS_FULFILLED"; const PROMISE_STATUS_REJECTED = "PROMISE_STATUS_REJECTED"; const execFnWithCatchError = (execFn, value, resolve, reject) => { try { const result = execFn(value); resolve(result); } catch (err) { reject(err); } } class ZXPromise { constructor(executor) { this.status = PROMISE_STATUS_PENDING; this.value = undefined; this.reason = undefined; this.onfufilled = []; this.onrejected = []; const resolve = (value) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_FULFILLED; this.value = value; this.onfufilled.forEach(fn => { fn(value); }); }) } } const rejected = (reason) => { if (this.status === PROMISE_STATUS_PENDING) { queueMicrotask(() => { if (this.status !== PROMISE_STATUS_PENDING) return this.status = PROMISE_STATUS_REJECTED; this.reason = reason; this.onrejected.forEach(fn => { fn(reason); }) return this.reason; }) } } executor(resolve, rejected) } then(onfufilled, onrejected) { //This paragraph is to pass on the error code const defaultOnRejected = err => { throw err } onrejected = onrejected || defaultOnRejected return new ZXPromise((resolve, rejected) => { if (this.status === PROMISE_STATUS_FULFILLED && onfufilled) { execFnWithCatchError(onfufilled, this.value, resolve, rejected); } if (this.status === PROMISE_STATUS_REJECTED && onrejected) { execFnWithCatchError(onrejected, this.reason, resolve, rejected); } if (this.status === PROMISE_STATUS_PENDING) { if (onfufilled) this.onfufilled.push(() => { execFnWithCatchError(onfufilled, this.value, resolve, rejected); }); if (onrejected) { this.onrejected.push(() => { execFnWithCatchError(onrejected, this.reason, resolve, rejected); }); } } }) } catch(onrejected) { return this.then(undefined, onrejected); } finally(fn) { return this.then(() => { fn() }, () => { fn() }); } }
- You can test by yourself