Links to the original text of the personal home page
Self-realization of a promise is one of the most common interview questions in front-end (although I haven't met it yet), and it's also a way to understand promise deeply. Through self-realization, we can get a deeper understanding of its principles.
To realize promise by oneself, one must first understand it. Promises/A+Regulations What exactly is it?~
- promise represents the final result of an asynchronous operation.
- The main way to interact with promise is through its then method, which registers callbacks to receive the value of promise or fails to fulfill its reason.
- The specification details the behavior of the then method.
1. Implementing a synchronous version of promise
Synchronized edition can be executed in the order of promise - > then without considering others.
- Firstly, according to the specifications, set a few marker quantities.
const PENDING = "pending" const FULFILLED = "fulfilled" const REJECTED = "rejected"
- Prom. the synchronous execution
function Promise(fn){ let that = this this.status = PENDING this.value = undefined this.reason = undefined let resolve = function(value){ that.value = value that.status = FULFILLED } let reject = function(reason){ that.reason = reason that.status = REJECTED } try{ fn(resolve,reject) }catch(e){ reject(e) } } Promise.prototype.then = function(onFulfilled,onRejected){ if(this.status === FULFILLED){ onFulfilled(this.value) } if(this.status === REJECTED){ onRejected(this.reason) } }
II. Achieving Asynchronism
Next, two more problems will be solved on the basis of the synchronized version.
- The synchronous version does not return anything when executing the asynchronous code
new Promise((resolve,reject)=>{ setTimeout(()=>{resolve(1)}) }).then(x=>console.log(x))
- This is because when the then is executed, the resolve has not been executed, status is still the status of PENDING
- If the parameter in that is not Function, there should be a default callback function
Therefore, if you perform to the then, you need to do the following:
- Judging whether onFulfilled and onRejected are functions first
- Then determine the status of status. If status is PENDING, you can save the two callback functions in that and then execute them when resolve or reject is executed.
function Promise(fn){ let that = this this.status = PENDING this.value = undefined this.reason = undefined this.resolvedCb = [] this.rejectedCb = [] let resolve = function(value){ that.value = value that.status = FULFILLED that.resolvedCb.forEach(cb=>cb(that.value)) } let reject = function(reason){ that.reason = reason that.status = REJECTED that.rejectedCb.forEach(cb=>cb(that.reason)) } try{ fn(resolve,reject) }catch(e){ reject(e) } } Promise.prototype.then = function(onFulfilled,onRejected){ onFulfilled = onFulfilled instanceof Function?onFulfilled:()=>{} onRejected = onRejected instanceof Function?onRejected:()=>{} if(this.status === FULFILLED){ onFulfilled(this.value) } if(this.status === REJECTED){ onRejected(this.reason) } if(this.status === PENDING){ this.resolvedCb.push(onFulfilled) this.rejectedCb.push(onRejected) } }
The key codes are as follows:
- If the state is PENDING, save the callback function first
if(this.status === PENDING){ this.resolvedCb.push(onFulfilled) this.rejectedCb.push(onRejected) }
- In the case of asynchrony, resolve or reject will be executed after that, so when executed, the callback function has been put into the Cb array and can traverse the callback function in the execution Cb.
that.resolvedCb.forEach(cb=>cb(that.value)) that.rejectedCb.forEach(cb=>cb(that.reason))
- If onFulfilled and onRejected are not functions, a default function should be used
onFulfilled = onFulfilled instanceof Function?onFulfilled:()=>{} onRejected = onRejected instanceof Function?onRejected:()=>{}
Chain Call of then
According to the Promises/A+protocol, then should also return a promise, which is also a condition for the chain call of then.
- Consider the following first
Promise.prototype.then = function(onFulfilled,onRejected){ onFulfilled = onFulfilled instanceof Function?onFulfilled:()=>{} onRejected = onRejected instanceof Function?onRejected:()=>{} if(this.status === FULFILLED){ onFulfilled(this.value) return new Promise(()=>{}) } if(this.status === REJECTED){ onRejected(this.reason) return new Promise(()=>{}) } if(this.status === PENDING){ this.resolvedCb.push(onFulfilled) this.rejectedCb.push(onRejected) return new Promise(()=>{}) } }
Then in the states of FULFILLED and REJECTED, the content returned by the then will directly become the input of the callback function of the next then.
- The content of return is the input of onFulfilled for the next one, which should be wrapped in resolve
- The throw content is the onRejected input for the next one, and should be wrapped in reject
First, you need to be able to capture throw, and then decide whether to use resolve or reject based on the type of result
if (this.status === FULFILLED) { return new Promise((resolve, reject) => { try { let res = onFulfilled(this.value) resolve(res) } catch (e) { reject(e) } }) }
This implements the synchronized version of the then chain call
function Promise(fn) { ...... } Promise.prototype.then = function (onFulfilled, onRejected) { onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {} onRejected = onRejected instanceof Function ? onRejected : () => {} if (this.status === FULFILLED) { return new Promise((resolve, reject) => { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } }) } if (this.status === REJECTED) { return new Promise((resolve, reject) => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) } if (this.status === PENDING) { this.resolvedCb.push(onFulfilled) this.rejectedCb.push(onRejected) return new Promise(() => { }) } }
Then consider the asynchronous case, asynchronous case, the state of execution to the then is PENDING, then the then is also called on the basis of promise s returned in the PENDING state.
Considering asynchrony, when there is no chain call, only onFulfilled and onRejected need to be put into the callback array. If chain call is made, callback functions in promises of FULFILLED and REJECTED states should be put into the callback array (the logic here is quite complex, mainly to ensure the execution of content in resolvedCb or rejected Cb). Has the same effect as promise callbacks in FULFILLED or REJECTED)
Note here that the input of onFulfilled and onRejected is the value or reason attribute of the new promise, so use this directly, but the entire callback is placed in the array of the previous promise, so use that to identify the original this
Promise.prototype.then = function (onFulfilled, onRejected) { onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {} onRejected = onRejected instanceof Function ? onRejected : () => {} let that = this ...... if (this.status === PENDING) { return new Promise((resolve, reject) => { that.resolvedCb.push(() => { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } }) that.rejectedCb.push(() => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) }) } }
The complete procedure for this step is as follows
const PENDING = "pending" const FULFILLED = "fulfilled" const REJECTED = "rejected" function Promise(fn) { let that = this this.status = PENDING this.value = undefined this.reason = undefined this.resolvedCb = [] this.rejectedCb = [] let resolve = function (value) { that.value = value that.status = FULFILLED that.resolvedCb.forEach(cb => cb(that.value)) } let reject = function (reason) { that.reason = reason that.status = REJECTED that.rejectedCb.forEach(cb => cb(that.reason)) } try { fn(resolve, reject) } catch (e) { reject(e) } } Promise.prototype.then = function (onFulfilled, onRejected) { onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {} onRejected = onRejected instanceof Function ? onRejected : () => {} let that = this if (this.status === FULFILLED) { return new Promise((resolve, reject) => { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } }) } if (this.status === REJECTED) { return new Promise((resolve, reject) => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) } if (this.status === PENDING) { return new Promise((resolve, reject) => { that.resolvedCb.push(() => { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } }) that.rejectedCb.push(() => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) }) } }
This version is basically consistent with the standard promise, but there are some details, such as the execution results of the following code
new Promise((resolve, reject) => { resolve(new Promise((rs, rj) => {rs(9)})) }).then(res => { console.log(`res:${res}`) },err => { console.log(`err:${err}`) }) //res:9 new Promise((resolve, reject) => { resolve(new Promise((rs, rj) => {rj(9)})) }).then(res => { console.log(`res:${res}`) },err => { console.log(`err:${err}`) }) //err:9
So the next step is to solve the problem that the content of resolve is promise.
ps: The effect of the following code is consistent, that is, only resolve parses the content of internal promise
new Promise((resolve, reject) => { reject(new Promise((rs, rj) => {rs(9)})) }).then(res => { console.log(`res:${res}`) },err => { console.log(`err:${err}`) }) //err:[object Promise] new Promise((resolve, reject) => { reject(new Promise((rs, rj) => {rj(9)})) }).then(res => { console.log(`res:${res}`) },err => { console.log(`err:${err}`) }) //err:[object Promise]
Similarly, the promises returned by return in the then will also be resolved, and the promises returned by throw will not be resolved.
new Promise((resolve, reject) => { resolve(1) }).then(res => { return new Promise(resolve=>resolve(res)) }).then(res => { console.log(`res:${res}`) },err => { console.log(`err:${err}`) }) //res:1 new Promise((resolve, reject) => { resolve(1) }).then(res => { throw new Promise(resolve=>resolve(res)) }).then(res => { console.log(`res:${res}`) },err => { console.log(`err:${err}`) }) // err:[object Promise]
4. Solving the problem that the content of resolve is promise
Only promise s in resolve and return are resolved, that is, only this.value is resolved, and this.reason is not.
So you just need to modify the contents of if (this. status == FULFILLED) {} and that. resolvedCb. push () => {})
Consider the synchronization state first. When this.status === FULFILLED is executed, if this.value is a promise, then the chain call of the next one is received after the promise. Therefore:
if (this.status === FULFILLED) { return new Promise((resolve, reject) => { if (this.value instanceof Promise) { this.value.then(onFulfilled, onRejected) } else { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } } }) }
Consider asynchronous, direct, and the above step Empathy
if (this.status === PENDING) { return new Promise((resolve, reject) => { that.resolvedCb.push(() => { if (this.value instanceof Promise) { this.value.then(onFulfilled, onRejected) } else { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } } }) that.rejectedCb.push(() => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) }) }
The full version is as follows:
const PENDING = "pending" const FULFILLED = "fulfilled" const REJECTED = "rejected" function Promise(fn) { let that = this this.status = PENDING this.value = undefined this.reason = undefined this.resolvedCb = [] this.rejectedCb = [] let resolve = function (value) { that.value = value that.status = FULFILLED that.resolvedCb.forEach(cb => cb(that.value)) } let reject = function (reason) { that.reason = reason that.status = REJECTED that.rejectedCb.forEach(cb => cb(that.reason)) } try { fn(resolve, reject) } catch (e) { reject(e) } } Promise.prototype.then = function (onFulfilled, onRejected) { onFulfilled = onFulfilled instanceof Function ? onFulfilled : () => {} onRejected = onRejected instanceof Function ? onRejected : () => {} let that = this if (this.status === FULFILLED) { return new Promise((resolve, reject) => { if (this.value instanceof Promise) { this.value.then(onFulfilled, onRejected) } else { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } } }) } if (this.status === REJECTED) { return new Promise((resolve, reject) => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) } if (this.status === PENDING) { return new Promise((resolve, reject) => { that.resolvedCb.push(() => { if (this.value instanceof Promise) { this.value.then(onFulfilled, onRejected) } else { try { resolve(onFulfilled(this.value)) } catch (e) { reject(e) } } }) that.rejectedCb.push(() => { try { resolve(onRejected(this.reason)) } catch (e) { reject(e) } }) }) } }
Effect comparison:
Part of API Implementation of promise
Promise.resolve
Promise.resolve = function(value){ return new Promise(resolve=>{ resolve(value) }) }
Promise.reject
Promise.reject = function(reason){ return new Promise((resolve,reject)=>{ reject(reason) }) }
Promise.all
The Promise.all method is used to wrap multiple Promise instances into a new Promise instance.
const p = Promise.all([p1, p2, p3]);
The state of P is determined by p1, p2 and p3, which can be divided into two cases.
- Only when the states of p1, p2 and p3 are fulfilled, will the states of P become fulfilled. At this time, the return values of p1, p2 and p3 form an array, which is passed to the callback function of P.
- As long as one of p1, p2 and p3 is rejected, the state of P becomes rejected, and the return value of the first rejected instance is passed to the callback function of P.
Promise.all = function (promises) { let resolveList = [] return new Promise((resolve, reject) => { if(promises.length === 0){ //If promises are empty arrays, resolve([]) is returned. resolve(resolveList) } promises.forEach(p => { Promise.resolve(p).then(re => { resolveList.push(re) if (promises.length === resolveList.length) { //Because promise is asynchronous, it still needs to be put in it. resolve(resolveList) } }, rj => { reject(rj) }) }) }) }
ps: There is a bug, as follows
var a = new Promise((resolve, reject) => { setTimeout(() => { resolve('3') },300) }) var b = new Promise((resolve, reject) => { setTimeout(() => { resolve('2') },200) }) var c = new Promise((resolve, reject) => { setTimeout(() => { resolve('1') },100) }) var d = new Promise((resolve, reject) => { setTimeout(() => { resolve('4') },400) }) Promise.all([a, b, c, d]).then(res => console.log(res), res => console.log(res))
The real promise.all return is [3, 2, 1, 4], and my return is [1, 2, 3, 4]
Promise.race
The Promise.race method also wraps multiple Promise instances into a new Promise instance.
const p = Promise.race([p1, p2, p3]);
In the above code, as long as one instance of p1, p2, and p3 takes the lead in changing the state, the state of P changes accordingly. The return value of the Promise instance, which was the first to change, is passed to the callback function of P.
Promise.race = function (promises) { let flag = true return new Promise((resolve, reject) => { promises.forEach(p => { Promise.resolve(p).then(re => { if (flag) { flag = false resolve(re); } }, rj => { if (flag) { flag = false reject(rj); } }) }) }) }