Analyze what logic is needed to implement promises

  • A Promise has three states: pending, fulfilled, and rejected. The outside world cannot change its state, and once the state changes, it will never change again
  • Instantiating a Promise requires passing in an executor function, where the business code is executed, and the executor function takes resolve and reject. Resolve and reject are built-in functions of the Promise constructor
new Promise((resolve,reject)=>{
	// do something
})
Copy the code
  • If the business code executes successfully in the Executor function, the resolve function is called to change the Promise’s state to successful, and the result of the successful execution of the business code is passed to the Promise with an argument
  • When the execution of the business code fails in the Executor function, the reject function is called, changing the state of the Promise to failed, and passing the reason for the failure into the Promise with an argument
  • The first argument of the instance method then is the callback function for the successful execution of the business code, and the second argument is the callback function for the failed execution of the business code. When the execution of the business code is complete, the corresponding callback function is called based on the execution result, and these callback functions receive the execution result of the business code as a parameter
  • The then method can be called chaining and is penetrable
  • Example method catch to add a callback function for business code execution failures

So let’s do what Promise does

Preliminary build

According to the analysis, first we need to complete these functions:

  • The Promise constructor takes an executor function as an argument and executes the executor function in it

  • The Promise constructor has resolve and reject built-in methods that are passed as arguments to the executor function

  • Set an instance property status to store the state

  • Resolve changes the state to successful, reject changes the state to failed, and once the state changes, it doesn’t change

Const Pending = Symbol('Pending'); const Fulfilled = Symbol('Fulfilled'); const Rejected= Symbol('Rejected'); Function Promise(excutor){// The initial state is pending this.status = pending; Const resolve = () => {const resolve = () => { This. Status === Pending){// This. Status == Pending; }}; Reject = () => {if(this.status === Pending){this.status === Pending; }}; excutor(resolve,reject); }Copy the code

Initial implementation of then method

Following the simple analysis of the business scenario for the THEN instance method above, we need to add instance properties to store the results of the execution of the business code in the Executor function when we call the callback function in the THEN instance method. The result of a successful execution is passed in the resolve method, and the cause of a failure is passed in the reject method

Because the THEN method is instance-callable, the THEN method is on the constructor prototype:

Const Pending = Symbol('Pending'); const Fulfilled = Symbol('Fulfilled'); const Rejected= Symbol('Rejected'); Function Promise(excutor){// The initial state is pending this.status = pending; this.value = undefined; this.error = undefined; Const resolve = value => {// we use the arrow function to refer to this. This. Status === Pending){// This. Status == Pending; this.value = value; }}; Const reject = value => {if(this.status === Pending){// This. Status = Rejected; this.error = value; }}; excutor(resolve,reject); }; Promise. Prototype. Then = function (resolveCallback rejectCallback) {/ / here why not use arrow function, This will be a big pity if(this.status === depressing){if(resolveCallback && Typeof resolveCallback === 'function'){ resolveCallback(this.value); } } if(this.status === Rejected){ if(rejectCallback && typeof rejectCallback === 'function'){ rejectCallback(this.error); }}}Copy the code

The then method is initially implemented, so LET me verify:

Const promise = new promise ((resolve,reject)=>{resolve(' execute '); }); promise.then(res=>{ console.log(res); })Copy the code

The console prints the result we want, which means we’ve written everything fine so far, but then we thought, what if our excutor was a function that executes an asynchronous method:

Const promise = new promise ((resolve,reject)=>{setTimeout(()=>{resolve(' execute successfully '); }, 2000); }); promise.then(res=>{ console.log(res); })Copy the code

The result can be found that the desired result is not printed after 2 seconds, because when the THEN instance method is called, the state of the Promise is Pending. Although the state of the Promise becomes a pity after 2 seconds, the THEN instance method has already been called

How do you control the timing of the callback function in the THEN instance method? This can be done using the publish subscriber design pattern.

When calling the THEN instance method, if the state of a Promise is Pending, the success callback and the failure callback are stored separately, and the resolve or reject built-in methods are triggered in the Executor function when the asynchronous task completes. Call these callback functions in turn.

const Pending = Symbol('Pending'); const Fulfilled = Symbol('Fulfilled'); const Rejected= Symbol('Rejected'); function Promise(excutor){ this.status = Pending; this.value = undefined; this.error = undefined; this.onFulfilled = []; // This. OnRejected = []; // This. Const resolve = value => {if(this.status === Pending){this.status = pity; this.value = value; this.onFulfilled.forEach(fn => fn()); }}; const reject = value => { if(this.status === Pending){ this.status = Rejected; this.error = value; this.onRejected.forEach(fn => fn()); }}; excutor(resolve,reject); }; Promise.prototype.then = function(resolveCallback,rejectCallback){ if(this.status === Fulfilled){ if(resolveCallback && typeof resolveCallback === 'function'){ resolveCallback(this.value); } } if(this.status === Rejected){ if(rejectCallback && typeof rejectCallback === 'function'){ rejectCallback(this.error); }} // If the state of the then method is pending, If (this.status === Pending){if(resolveCallback && Typeof resolveCallback === 'function'){if(resolveCallback && resolveCallback === 'function'){ this.onFulfilled.push(()=>{ resolveCallback(this.value); }); } if(rejectCallback && typeof rejectCallback === 'function'){ this.onRejected.push(()=>{ rejectCallback(this.error) }); }}}Copy the code

Instantiate again to verify:

Const promise = new promise ((resolve,reject)=>{setTimeout(()=>{resolve(' execute successfully '); }, 2000); }); promise.then(res=>{ console.log(res); })Copy the code

The console prints “Execute successfully” after 2 seconds, indicating that all our logic is correct.

Const promise = new promise ((resolve,reject)=>{setTimeout(()=>{reject(' reject ')); }, 2000); }); promise.then(res=>{ // do nothing },err=>{ console.log(err) })Copy the code

The console successfully prints “Execution failed” after 2 seconds.

Microtask mechanism of THEN method

Since the native Promise is a microtask provided by the V8 engine, we cannot restore the V8 engine implementation, so we use setTimeout to simulate async, so the native is the microtask, and here is the macro task instead (if you want to implement the Promise microtask, Instead of seiTimeout, mutationObserver can be used to implement microtasks. This is just simulating asynchrony.)

Promise.prototype.then = function(resolveCallback,rejectCallback){ if(this.status === Fulfilled){ if(resolveCallback && Typeof resolveCallback === 'function'){setTimeout(()=>{resolveCallback(this.value); }, 0); } } if(this.status === Rejected){ if(rejectCallback && typeof rejectCallback === 'function'){ setTimeout(()=>{ rejectCallback(this.error); }, 0); } } if(this.status === Pending){ if(resolveCallback && typeof resolveCallback === 'function'){ this.onFulfilled.push(()=>{ setTimeout(()=>{ resolveCallback(this.value); }, 0); }); } if(rejectCallback && typeof rejectCallback === 'function'){ this.onRejected.push(()=>{ setTimeout(()=>{ rejectCallback(this.error); }, 0); }); }}}Copy the code

Chain calls to THEN

Instance methods then chain calls have two requirements:

  • You can use instance method THEN directly after instance method THEN
  • So the first instance method then returns a value, and whatever value is available in the second instance method then, okay

If you want a chained call, then must also return a promise, passing either resolve(value) or reject(value) to the value returned by the instance method then

We also need a function that handles the type of the value returned by the then method:

Promise.prototype.then = function(resolveCallback,rejectCallback){ let promise = new Promise((resolve,reject)=>{ if(this.status === Fulfilled){ if(resolveCallback && typeof resolveCallback === 'function'){ setTimeout(()=>{ let x = resolveCallback(this.value); handleValue(promise,x,resolve,reject); },0); } } if(this.status === Rejected){ if(rejectCallback && typeof rejectCallback === 'function'){ setTimeout(()=>{ let x = rejectCallback(this.error); handleValue(promise,x,resolve,reject); }, 0); } } if(this.status === Pending){ if(resolveCallback && typeof resolveCallback === 'function'){ this.onFulfilled.push(()=>{ setTimeout(()=>{ let x = resolveCallback(this.value); handleValue(promise,x,resolve,reject); }, 0); }); } if(rejectCallback && typeof rejectCallback === 'function'){ this.onRejected.push(()=>{ setTimeout(()=>{ let x = rejectCallback(this.error); handleValue(promise,x,resolve,reject); }, 0); }); } } }) return promise }Copy the code

Then write the handleValue utility function:

Const handleValue = (promise,x,resolve,reject)=>{// If (promise === x){return reject(new TypeError(' chain loop called ')); } let once = false; If (typeof x === 'object' &&x! == null || typeof x === 'function'){ const then = x.then; If (then &&typeof then === 'function'){then. Call (x,y=>{if(once) return once  = true; HandleValue (Promise,y,resolve,reject); },r=>{if(once) return once = true; reject(r); Resolve (x) resolve(x) if x is a normal object; }}else{// If x is a primitive value, call resolve(x). }}Copy the code

In the above code, determining typeof then === ‘function’ is essentially determining whether the returned X is a Promise. If there is no then function, x is the normal value and resolve(x) is returned. If there is a then function, x is a Promise, and the Promise is recursively resolved until x is an ordinary value and returned as the final result

Typeof then === ‘function’ determines if x is a Promise, instead of x instanceof Promise. This is to make promises more generic, so a Thenable object can also be considered a Promise. The thenable object is an object that has a then method, as shown in the following code:

Let thenable = {then: function(resolve, reject){resolve(' execute successfully ')}}Copy the code

The result of successful execution is passed through resolve in the thenable.then method. However, the Thenable object does not come out of the Promise class new, so it cannot be determined by x instanceof Promise

Then the penetration

What if the following logic is implemented?

Const p = new Promise((resolve, reject) => {setTimeout(() => {resolve(' execute successfully ') ')}, 2000) }) p.then().then(res =>{ console.log(res) })Copy the code

If the then method returns the same value as the previous then method, then will return the same value as the previous then method.

Promise.prototype.then = function(resolveCallback,rejectCallback){ resolveCallback = typeof resolveCallback === 'function' ? resolveCallback : v => v; rejectCallback = typeof rejectCallback === 'function' ? rejectCallback : err => { throw err }; let promise = new Promise((resolve,reject)=>{ if(this.status === Fulfilled){ if(resolveCallback && typeof resolveCallback === 'function'){ setTimeout(()=>{ let x = resolveCallback(this.value); handleValue(promise,x,resolve,reject); }, 0); } } if(this.status === Rejected){ if(rejectCallback && typeof rejectCallback === 'function'){ setTimeout(()=>{ let x = rejectCallback(this.error); handleValue(promise,x,resolve,reject); }, 0); } } if(this.status === Pending){ if(resolveCallback && typeof resolveCallback === 'function'){ this.onFulfilled.push(()=>{ setTimeout(()=>{ let x = resolveCallback(this.value); handleValue(promise,x,resolve,reject); }, 0); }); } if(rejectCallback && typeof rejectCallback === 'function'){ this.onRejected.push(()=>{ setTimeout(()=>{ let x = rejectCallback(this.error); handleValue(promise,x,resolve,reject); }, 0); }); } } }) return promise }Copy the code

Catch method

Catch is an alias for then(NULL, rejectCallback)

Promise.prototype.catch =  function(rejectCallback){
	this.then(null,rejectCallback);
}
Copy the code

Optimize the complete code

Deal with internal errors, etc

const Fulfilled = Symbol('Fulfilled') const Rejected = Symbol('Rejected') const Pending = Symbol('Pending') const HandleValue = (promise,x,resolve,reject) => {if(promise === x){return Reject (new TypeError(' detect loop call '))} let once = false if(typeof x === 'object' && x ! == null || typeof x === 'function'){ const then = x.then if(typeof then === 'function'){ then.call(x,y=>{ if(once) return once = true handleValue(promise,y,resolve,reject) },r=>{ if(once) return once = true reject(r) }) }else{ resolve(x) } }else{ resolve(x) } } function Promise(excutor){ this.status = Pending this.value = undefined this.reason =  undefined this.onFulfilled = [] this.onRejected = [] const resolve = value => { if(this.status === Pending){ this.status = Fulfilled this.value = value this.onFulfilled.forEach(fn => { if(typeof fn === 'function'){ fn() } }) } } const reject = value => { if(this.status === Pending){ this.status = Rejected this.reason = value this.onRejected.forEach(fn => { if(typeof fn === 'function'){ fn() } }) } } try{ excutor(resolve,reject) } catch(err){ reject(err) } } Promise.prototype.then = function(fulfilledCallback,rejectedCallback){ fulfilledCallback = typeof fulfilledCallback === 'function' ? fulfilledCallback : v => v rejectedCallback = typeof rejectedCallback === 'function' ? rejectedCallback : err => { throw err } let promise = new Promise((resolve,reject)=>{ if(this.status === Pending){ if(fulfilledCallback && typeof fulfilledCallback === 'function'){ this.onFulfilled.push(()=>{ setTimeout(()=>{ try{ let x = fulfilledCallback(this.value) handleValue(promise,x,resolve,reject) }catch(err){ reject(err) } },0) }) } if(rejectedCallback && typeof rejectedCallback === 'function'){ this.onRejected.push(()=>{ setTimeout(()=>{ try{ let x =  rejectedCallback(this.reason) handleValue(promise,x,resolve,reject) }catch(err){ reject(err) } }) }) } } if(this.status  === Fulfilled){ if(fulfilledCallback && typeof fulfilledCallback === 'function'){ setTimeout(()=>{ try{ let x = fulfilledCallback(this.value) handleValue(promise,x,resolve,reject) }catch(err){ reject(err) } },0) } } if(this.status === Rejected){ if(rejectedCallback && typeof rejectedCallback === 'function'){ setTimeout(()=>{ try{ let x = rejectedCallback(this.reason) handleValue(promise,x,resolve,reject) }catch(err){ reject(err) } }) } } }) return promise } Promise.prototype.catch = function(rejectCallback){ this.then(null,rejectCallback) }Copy the code

So that’s where we’re going with the parsing of Promise and his instance methods then and catch, and then we’re going to do a more detailed analysis of his static methods