[handwriting series] take you to realize a simple Promise

Posted by melefire on Tue, 08 Mar 2022 03:56:24 +0100

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

  1. Promise's basic skeleton
  2. Promise then
  3. Promise. Multiple calls of then
  4. then chain call
  5. Implementation of catch
  6. 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

Topics: Javascript