There are handwritten promises on the Internet. According to their own understanding, also wrote a, published hope can be pointed out. Also give yourself a grooming process. For starters, look at this document dialectically; you need to be familiar with native Promises.

The code is pretty simple, less than 100 lines of code, and it’s all about understanding what native Promises can do and what features they have. Listing the requirements for simulating promises based on these features solves most of the problem. The features of native Promises are listed step by step here. Add the code step by step. Each step of the code is based on the previous step of the code is added or modified, in the code will be marked where the changes and additions, so that you do not have to flip through the code of the previous step, to ensure consistency of thinking

Just to be clear, we’re looking at native promises in a superficial way. The code in this article was run and tested in Chrome (version 81.0.4044.129 (official) (64-bit))

Although the code in this article has been tested, there is no guarantee that the copy and paste process is correct. So it’s best to do it yourself

More importantly, the use of setTimeout to simulate microtasks will be executed in a different order than the native Promise. That’s important to understand. EventLoop (JavaScript) EventLoop (JavaScript) EventLoop

The body of the text looks a little long, but in fact it is all repetitive code space, the content is very simple

Promise is a container

The first is to simulate the most basic requirements that Promise needs to fulfill

  • Promise is a container that stores the results of asynchronous or synchronous execution.

  • There are also states that reflect the stages of synchronous or asynchronous processing. There are three states

    1. Pending The execution phase of asynchrony or synchronization, in which the result is undefined
    2. This is a big pity, which usually means that the result is successfully returned synchronously or asynchronously. At this time, the result value will record the correct result returned
    3. Rejected: indicates the status of the synchronization or asynchronous error, and the result value records the cause of the error
  • The state and value of a Promise cannot be accessed directly from outside the Promise object

  • You also need to have a synchronous or asynchronous function (an execution function) that takes the Promise argument and executes it in the Promise constructor

The code:

/ *The MyPromise function is defined. The parameter executor is of type Funciotn and is used to perform synchronous or asynchronous operations.* / / *Down down down down down down down down down down down down down down* /function MyPromise(executor){
    /*'↓↓↓ 'Defines three states PENDING, depressing and REJECTED↓↓↓'*/
    const PENDING = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED = "rejected"; / *'↓↓↓ Value Stores asynchronous results (the value of the Promise), which is undefined in the pending state, fulfilled in the fulfilled state and the error cause in the Rejected state.* /letvalue; / *'↓↓↓state is used to store the Promise state, which is initially pending↓↓↓'* /letstate = PENDING; / *↓↓↓ All defined, then run the function ↓↓↓'*/
    executor()
}

Copy the code

The most basic part is done

Second, internal operation status and value

The above defines the state of the container and the variables that need to store the value, and runs the user-defined execution method, but when the execution function has the result, how to change the state of the container and store the result? This requires defining operation state and value methods to handle. Requirements are as follows

  • Values and states can only change once
  • The method of operating the state and value is not exposed externally. Internally, the state is changed to depressing through the resolve method, and the state is changed to Rejected through the reject method, and the corresponding value of the state is changed. These two methods are passed as arguments to the executor function, and the user decides when to change the state and value through these methods.

Continue to improve MyPromise ↓↓↓↓

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    letstate = PENDING; / *'left left left left left left left left down down down down down down down left left left left left left left left left left down down down down down down down down down here is the change of the new state and the method of value left left left left left left left left left left down down down down down down down down down left left left left left left left left left down down down down down down down down'* /function change(newState, newValue){ //'Methods used to modify state and corresponding values'
        if (state === PENDING) {//'State pending limits changes to state and value changes only once.'value = newState; state = newValue; }}letresolve = change.bind(this, FULFILLED); //'This state can only be fulfilled if the resolve function is defined.'
    letreject = change.bind(this, REJECTED); //'Reject defined, can only change the state to depressing'
    //'Resolve' and 'reject' are partial functions of change, so binding this to 'resolve 'and' reject 'doesn't make sense. Just focus on' change 'and read the code./ *'write write write write write write write write write write write write write write write write write write here is the change of the new state and the method of value write write write write write write write write write write write write write write write write write write'* / executor (resolve, reject) / / please please please'Pass it outside of Promise, via the executor argument.'
}
Copy the code

You need to register callback functions

You can store state and value, and you can modify state and value, but you also need methods that handle values in the corresponding state, so you need to register callback functions.

There are two methods in the Promise object, then and catch, that register callbacks

  • The second parameter is used to register the callback of the error cause when the status changes to Rejected. The second parameter is used to register the callback of the error cause when the status changes to Rejected. Register at any time to handle the fixed cause of the error
  • The catch method registers the same callback as the second function of then.
  • Catch and then both return a new Promise
  • A Promise object can register many callbacks, which means that a Promise can invoke many then or catch calls. Let me give you an example
let p = new Promise((resolve, reject) => {})
p.then(value => {})
p.then(value => {})
p.catch(value => {})
/*'The code above is to register a number of callbacks.'* / / *'Each callback is independent, and the new Promise returned is not the same, which is equivalent to a branching of Promise state passing, which will be expanded later.'* / / *Down down down down down down down down down down down down down down down down down down*/
p.then(value => {}).then(value => {}).then(value => {}).then(value => {})

Copy the code

Add the two methods used to register callbacks as required above

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    function change(newState, newValue){
        if(state === PENDING) { value = newState; state = newValue; }}let resolve = change.bind(this, FULFILLED);
    letreject = change.bind(this, REJECTED); / *'Down down down down down down down down down down down down down down here is how to register for a callback.'*/
    var onQueue = []; //'→→→→→ to register multiple callbacks, onQueue is defined to store registered callbacks and wait for the state to change. '
    function register(onFulfilled, onRejected) {//'→→→→→ method used to register callbacks'
        let nextPromise = new MyPromise((nextResolve, nextReject) => {
          /*'left left left left left left left left left down down down down down down down down to onQueue add registration, the method of using the object structure is for the sake of convenient called after left left left left left left left left left down down down down down down down down'* / / *'↓↓↓↓↓↓ 'As to why I wrote it in a Promise, it will be explained in detail later.*/
          onQueue.push({
            [FULFILLED]: { on: onFulfilled},
            [REJECTED]: { on: onRejected},
          });
        })
        return nextPromise;
    }
    this.then = register.bind(this);            //'→→→→→ then methods that define Promise objects register callback methods that handle returned values'this.catch = register.bind(this, undefined); //'→→→→→ the catch method that defines a Promise object is the syntactic sugar for then '/ *'This is the method for registering callbacks.'*/
    
    executor(resolve, reject)
}
Copy the code

Need a mechanism to handle registered callbacks.

Registered callbacks need to be executed when the state is not pending, so there needs to be a mechanism for handling callbacks. Add a run method to handle this functionality

Before we add functionality, let’s see what native Promises do

First of all, it should be noted that the implementation of the registered callback function is not meant to be directly executed, but to follow the EVENT loop mechanism of JS. The native Promise is to put the callback into the microtask and wait until the macro task completes before executing the current microtask

Here we use the setTimeout macro task to simulate microtasks

See also when to start handling callbacks:

  • When the state changes from Pending to fulfilled or Rejected, the registered callback needs to be processed
  • When the state is not pending, it needs to be handled immediately after the callback is registered

Don’t worry, there’s more. Let’s see what happens with chain calls, as in the above example:

new Promise((resolve, reject) => {
    // resolve('p ok')
    reject('p err')
}).then(value => {
    console.log("Success 1"+value)
    return "p1"
}, error => {
    console.log("Failure 1"+error)//←←←← Output herereturn "p1 err"
}).catch(err => {
    console.log("Failure 2"+error)
    return "p2 err"
}).then(value => {
    console.log("Success 3"+value //←←←← Output herereturn new Promise((resolve, reject) => {
        reject("The new Promise failed.")
    })
}, error => {
    console.log("Failure 3"+error)
    return "p3 err"
}).then(value => {
    console.log("Success 4"+value)
}, error => {
    console.log("Failure 4"+error)//←←←← Output here}) // Output: // failed 1 P err //test.html:56 Succeeded 3 P1 err //test.html:66 Failed 4 The new Promise failedCopy the code

It’s a long chain. A brief explanation of the situation. To make it clear, let’s use a picture:

  • The Promise state and value returned by the then method are related to the value returned by the callback function registered with it.
    1. When the callback function returns a non-Promise object, the state of the Promise object returned by then is fulfilled, and the value is the return value of the callback function
    2. When the callback returns a Promise, the state and value of the Promise returned by then inherit the state and value of the Promise returned by the callback.
  • In the current (current THEN or catch) segment, the state and value are passed down when there is no callback to the corresponding state.

Building on the original Promise features above, we continue to add MyPromise functionality with a run method to implement a mechanism for handling registered callback functions

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
        run()//'→→→→→ The run method is executed here. This will be a pity; this will be a pity. let reject = change.bind(this, REJECTED); / * 'Down down down down down down down down down down down down down down down down down down down down down down down down down down down down down down down'*/ function run(){ if (state === PENDING) { return; } while (onQueue.length) { let onObj = onQueue.shift(); / / '←← Take one of the registered callbacks and put it into the simulated microtask below' setTimeout(() => { //'Please please pleasesetTimeout simulates the microtask, puts the callback in, and waits for execution' if (onObj[state].on) { //'←← Check whether the callback function is registered in the current state' let returnvalue = onObj[state].on(value); / / '←← if yes, run the callback function and get the return value' if (returnvalue instanceof MyPromise) { //'←← Determine whether the returned value is of type MyPromise'/ *'↓↓↓ The return value is of type MyPromise, which is used for this MyPromise objectthenThe resolve and reject methods of nextPromise are used as parameters to obtain the state and value, thus achieving the inheritance of state and value'*/ returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next); } else { //'↓↓↓ The return value is not of MyPromise type, but directly changes the nextPromise state to depressing, and the value is the return value of the callback function' onObj[FULFILLED].next(returnvalue); } } else { /*'↓↓↓ The state of the nextPromise object is changed by using the saved resolve or Reject of the nextPromise object'*/ onObj[state].next(value); }}, 0); }} / * '↑ : The callback function that handles registration in this room'*/ var onQueue = []; function register(onFulfilled, onRejected) { let nextPromise = new MyPromise((nextResolve, NextReject) => {onqueue.push ({/*"↓↓↓ ↓ next attribute added, why ↓↓↓ The state and value will be passed down, ↓↓↓ to change the state of the nextPromise object, This will be a big pity, which is gradually forgotten. This will be a big pity, which can be FULFILLED only once. ↓↓↓"*/ [depressing]: {on: ondepressing, next: nextResolve}, [REJECTED]: { on: onRejected, next: nextReject},//'←← The reason for separating is because it is convenient to pass down the state'}); }) run() //'→→→→ The run method is executed here. The run method does not handle callback functions pending' return nextPromise; } this.then = register.bind(this); this.catch = register.bind(this, undefined); executor(resolve, reject) }Copy the code

Five, error handling

Let’s start with a few examples of native Promise features to summarize the requirements.

  • Promise errors are not thrown

Let’s start with a little experiment

new Promise(() => { xxx; })// Console output error console.log('I'm the code behind') / /'But it also prints this sentence, and it's not blocked, so it's not thrown out of the Promise.'
Copy the code

The above example is printed with an error, and the following code executes. The error is simply output, indicating that it was not thrown out of the Promise. But what happens if the Promise argument is not of type function

new Promise("hahaha") //Uncaught TypeError: Promise resolver hahaha is not a function
console.log('I'm the code behind') / /'There's no output output here.'
Copy the code

The above example output error, the following code does not output. That means the error was thrown, so:

  • The Promise argument is not of type function and will throw an error

Here’s another example of how Promise handles errors in each part.

new Promise((resolve, reject) => {
    xxx;
    // resolve("ok")
    // reject('no') // xxx; }).then(value => { //xxx; Console. log(value)// Output position 0},err => {// XXX; console.log("err1",err)// Output position 1}). Catch (err => {console.log("err2",err)// Output position 2}) When the Prosmise execution function has an error, output position 1 output: err1 ReferenceError: XXX is not defined whenthenErr1 ReferenceError: XXX is not defined when the first parameter has an errorthenErr2 ReferenceError: XXX is not definedthenThe second argument is removed when Prosmise executes the function andthenErr2 ReferenceError: XXX is not definedCopy the code

And you can test the error handling a little bit more. It can be summarized according to the above examples

  • The Promise object will change its status to Rejected and call its registered onRejected function.
  • If an error is reported after the state of the Promise object changes, it is not handled, let alone thrown out of the object
  • If you do not register the onRejected function, errors are passed down the hierarchy. Until the end. That is, the error is handled in the nearest onRejected function. This is the same as the Rejected state.
function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
        run()
      }
    }
    
    let resolve = change.bind(this, FULFILLED);
    let reject = change.bind(this, REJECTED);
    
    function run() {if (state === PENDING) {
        return;
      }
      while (onQueue.length) {
        let onObj = onQueue.shift();
        setTimeout(() => {
          if (onObj[state].on) {
            try{//'Fetching an error while running with a callback function'
              let returnvalue = onObj[state].on(value);
              if (returnvalue instanceof MyPromise) {
                returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next);
              } else{ onObj[FULFILLED].next(returnvalue); } }catch(error){ onObj[REJECTED].next(error); //'←←← If the callback returns an error, pass it down in the rejected state '}}else{ onObj[state].next(value); }}, 0); } } var onQueue = [];function register(onFulfilled, onRejected) {
        let nextPromise = new MyPromise((nextResolve, nextReject) => {
          onQueue.push({
            [FULFILLED]: { on: onFulfilled, next: nextResolve},
            [REJECTED]: { on: onRejected, next: nextReject},
          });
        })
        run() 
        return nextPromise;
    }
    this.then = register.bind(this);
    this.catch = register.bind(this, undefined);
    
    //If executor is not a function type, an error is thrown.
    if(! (executor instanceof Function)) { throw new TypeError(executor +"It's not a function. Pro! The MyPromise parameter has to be a function."); } / /'↓↓↓ ↓ 'insert try (' fetch error')
    //The problem is that if executor is not a function, simply adding a try does not throw an error, so add a judgment in front of it.
    try {
      executor(resolve, reject);
    } catch (error) {
      /*↓↓↓ If an error is reported during the function, change the status to Rejected. ↓↓↓ If an error is reported after the status change, this command is also executed, but the status change is limited to one time. ↓↓↓ The reject method is useless here. To achieve the status change after the error does not handle the effect. '*/ reject(error); }}Copy the code

At this point, the basic promise is fulfilled. But there are a few features that are missing, which is the icing on the cake. The example

new Promise((resolve, reject) => {
    // xxx;
    resolve("ok")
    // reject('no'}) //resolve, reject, Uncaught ()inNo promise). This means that the error is not handled before the state changes. The error output is in the console, but it is only output, not thrown. Because it doesn't block the codeCopy the code

This example shows that the rejected and error states are not handled, although they are not thrown and will not affect the program. But there will be a red message on the console. So finally add this prompt function, the following code to adjust the order, look more pleasing to the eye, and then add the prompt function

function MyPromise(executor){
    const PENDING   = "pending";
    const FULFILLED = "fulfilled";
    const REJECTED  = "rejected";
    let value;
    let state = PENDING;
    
    lettipTask; //'In order to remove the prompt task'
    
    function change(newState, newValue){
        if (state === PENDING) {
        value = newState;
        state = newValue;
        if(! onQueue.length && state === REJECTED) { /*'Put a prompt task in the task when the status is Rejected and the callback function is not registered. If the callback is registered in the macro task that MyPromise runs, the task is prompted in Run to be removed until the end, and there must be no MyPromise object registered with the callback function. The object will perform the prompt task such as a chain call p.teng ().then().... Then () can't be infinite, there will always be a last one. This last returned promise will not be registered for a callback. So the hint task added here will be executed. ' */
          tipTask = setTimeout(() => {//'In order to remove this task. Put variables in MyPromise scope '
            console.error("In MyPromise, you need to register a callback \n that handles errors." + (value || ' '));
          }, 0);
        }
        run()
      }
    }
    
    function run() {if (state === PENDING) {
        return;
      }
      while(onQueue.length) { clearTimeout(tipTask); //'If a callback is registered. Here I remove the hint task from the change method.
        
        let onObj = onQueue.shift();
        setTimeout(() => {
          if (onObj[state].on) {
            try{
              let returnvalue = onObj[state].on(value);
              if (returnvalue instanceof MyPromise) {
                returnvalue.then(onObj[FULFILLED].next, onObj[REJECTED].next);
              } else{ onObj[FULFILLED].next(returnvalue); } }catch(error){ onObj[REJECTED].next(error); }}else{ onObj[state].next(value); }}, 0); } } var onQueue = [];function register(onFulfilled, onRejected) {
        let nextPromise = new MyPromise((nextResolve, nextReject) => {
          onQueue.push({
            [FULFILLED]: { on: onFulfilled, next: nextResolve},
            [REJECTED]: { on: onRejected, next: nextReject},
          });
        })
        run() 
        return nextPromise;
    }
    
    let resolve = change.bind(this, FULFILLED);
    let reject = change.bind(this, REJECTED);
    
    this.then = register.bind(this);
    this.catch = register.bind(this, undefined);
    
    if(! (executor instanceof Function)) { throw new TypeError(executor +"It's not a function. Pro! The MyPromise parameter has to be a function."); } try { executor(resolve, reject); } catch (error) { reject(error); }}Copy the code

So you have a relatively perfect handwritten code that mimics Promise.

Uncaught (in promise) no or internal error is not handled in console output, in fact, in Chrome is all currently existing macro task queue tasks (not all, is currently, that is, when the macro task running this promise runs, All existing macro tasks are executed and then output.

The microtask simulated by setTimeout here will be different from the original Promise in the sequence of JS event loop.

This article hopes to be able to give you help, also hope to be able to get advice. The words used in this article are relatively colloquial, so if you use them in a formal situation, such as an interview, please use more professional language. thank you