Part 15 promise principle

This time, thoroughly understand Promise principle

Promise must be in one of the following three states: Pending, Fulfilled, and Rejected. Once promise is resolve d or Rejected, it cannot be migrated to any other state (i.e. immutable).
Basic process:

  1. Initialize Promise status (pending)
  2. Execute the fn function passed in Promise immediately, pass the resolve and reject functions inside Promise as parameters to fn, and handle them according to the timing of the event mechanism
  3. Execute then(...) to register the callback processing array (the then method can be called multiple times by the same promise)
  4. The key in Promise is to ensure that the parameters onFulfilled and onRejected passed in by the then method must be executed in the new execution stack after the event cycle in which the then method is called.

The real chain promise means that after the current promise reaches the completed state, the next promise starts

call chaining

First, from the Promise execution results, there is the following code:

new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
            resolve({ test: 2 })
            reject({ test: 2 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
    },(data1)=>{
        console.log('result2',data1)
    }).then((data) => {
        console.log('result3', data)
    })
    //result1 { test: 1 }
    //result3 undefined

Obviously, different data is output here. It can be seen that:

  1. A chain call can be made, and each time then returns a new promise (the two printing results are inconsistent. If it is the same instance, the printing results should be consistent.
  2. Only the content resolve d for the first time is output, and the content rejected is not output, that is, Promise is stateful, and the state can only be pending - > fully or pending - > rejected, which is irreversible.
  3. A new Promise is returned in then, but the callback registered in then still belongs to the previous Promise.

Based on the above points, let's write a PromiseA+ Canonical Promise model with resolve method only:

 function Promise(fn){ 
        let state = 'pending';
        let value = null;
        const callbacks = [];

        this.then = function (onFulfilled){
            return new Promise((resolve, reject)=>{
                handle({ //Bridge, put the resolve method of the new promise into the callback object of the previous promise
                    onFulfilled, 
                    resolve
                })
            })
        }

        function handle(callback){
            if(state === 'pending'){
                callbacks.push(callback)
                return;
            }
            
            if(state === 'fulfilled'){
                if(!callback.onFulfilled){
                    callback.resolve(value)
                    return;
                }
                const ret = callback.onFulfilled(value) //Process callback
                callback.resolve(ret) //resolve to process the next promise
            }
        }
        function resolve(newValue){
            const fn = ()=>{
                if(state !== 'pending')return

                state = 'fulfilled';
                value = newValue
                handelCb()
            }
            
            setTimeout(fn,0) //Based on promise a + specification
        }
        
        function handelCb(){
            while(callbacks.length) {
                const fulfiledFn = callbacks.shift();
                handle(fulfiledFn);
            };
        }
        
        fn(resolve)
    }

This model is simple and easy to understand. The key point here is to create a Promise in then. The node whose state changes to full is when the callback of the previous Promise is completed. That is, when the state of a Promise is full, its callback function will be executed, and the result returned by the callback function will be returned to the next Promise as value (that is, the Promise generated in then). At the same time, the state of the next Promise will be changed (execute resolve or reject), and then execute its callback, and so on... The effect of chain call comes out.
However, if it is only the case in the example, we can write as follows:

new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
        //dosomething
        console.log('result3')
    })
    //result1 { test: 1 }
    //result3

In fact, our commonly used chain call is used in asynchronous callback to solve the problem of "callback hell". The following example:

new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve({ test: 1 })
  }, 1000)
}).then((data) => {
  console.log('result1', data)
  //dosomething
  return test()
}).then((data) => {
  console.log('result2', data)
})

function test(id) {
  return new Promise(((resolve) => {
    setTimeout(() => {
      resolve({ test: 2 })
    }, 5000)
  }))
}
//Based on the first Promise model, the output after execution
//result1 { test: 1 }
//result2 Promise {then: ƒ}

Using the Promise model above, the result is obviously not what we want. Look at the above model carefully. When executing callback.resolve, the passed parameter is the return of callback.onFulfilled execution. Obviously, this test example returns a Promise, and the resolve method in our Promise model has no special treatment. Then we will resolve Change:

 function Promise(fn){ 
        ...
        function resolve(newValue){
            const fn = ()=>{
                if(state !== 'pending')return

                if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){
                    const {then} = newValue
                    if(typeof then === 'function'){
                        // newValue is the newly generated promise, and resolve is the resolve of the previous promise
                        //It is equivalent to calling the then method of the newly generated promise and injecting the resolve of the previous promise as its callback
                        then.call(newValue,resolve)
                        return
                    }
                }
                state = 'fulfilled';
                value = newValue
                handelCb()
            }
            
            setTimeout(fn,0)
        }
        ...
    }

Using this model and testing our example, we get the correct results:

new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
        //dosomething
        return test()
    }).then((data) => {
        console.log('result2', data)
    })

    function test(id) {
        return new Promise(((resolve, reject) => {
            setTimeout(() => {
            resolve({ test: 2 })
            }, 5000)
        }))
    }
    //result1 { test: 1 }
    //result2 { test: 2 }

Obviously, the newly added logic is for the processing when the resolve input parameter is Promise. Let's look at the Promise created in test, which does not call the then method. From the above analysis, we know that the Promise callback function is registered by calling its then method, so the Promise callback function created in test is empty.
Obviously, if there is no callback function, there is no way to chain down when executing resolve. Therefore, we need to actively inject callback functions into it.
As long as we delay the execution of the resolve function of the Promise generated in the first then until the Promise in the test is onFulfilled, the chain can continue. Therefore, when the resolve input parameter is Promise, call its then method to inject the callback function, and inject the resolve method of the previous Promise To bind the point of this with call.
Based on the new Promise model, the Promise instance and its callback function generated by the above execution process can be used as shown in the following table:

Promisecallback
P1[{onfulfilled: C1 (FN in the first then), resolve: p2resolve}]
P2 (generated when P1 calls then)[{onfulfilled: C2 (FN in the second then), resolve: p3resolve}]
P3 (generated when P2 calls then)[]
P4 (generate [call test] in execution c1)[{onFulfilled:p2resolve,resolve:p5resolve}]
P5 (generated in then.call logic when p2resolve is called)[]

With this table, we can clearly know the order of callback execution in each instance:
c1 -> p2resolve -> c2 -> p3resolve -> [] -> p5resolve -> []
The above is the principle of chain call.

reject

Next, let's complete the logic of reject. Just add the logic of reject when registering callback and changing state.
The complete code is as follows:

function Promise(fn){ 
        let state = 'pending';
        let value = null;
        const callbacks = [];

        this.then = function (onFulfilled,onRejected){
            return new Promise((resolve, reject)=>{
                handle({
                    onFulfilled, 
                    onRejected,
                    resolve, 
                    reject
                })
            })
        }

        function handle(callback){
            if(state === 'pending'){
                callbacks.push(callback)
                return;
            }
            
            const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;
            const next = state === 'fulfilled'? callback.resolve:callback.reject;

            if(!cb){
                next(value)
                return;
            }
            const ret = cb(value)
            next(ret)
        }
        function resolve(newValue){
            const fn = ()=>{
                if(state !== 'pending')return

                if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){
                    const {then} = newValue
                    if(typeof then === 'function'){
                        // newValue is the newly generated promise, and resolve is the resolve of the previous promise
                        //It is equivalent to calling the then method of the newly generated promise and injecting the resolve of the previous promise as its callback
                        then.call(newValue,resolve, reject)
                        return
                    }
                }
                state = 'fulfilled';
                value = newValue
                handelCb()
            }
            
            setTimeout(fn,0)
        }
        function reject(error){

            const fn = ()=>{
                if(state !== 'pending')return

                if(error && (typeof error === 'object' || typeof error === 'function')){
                    const {then} = error
                    if(typeof then === 'function'){
                        then.call(error,resolve, reject)
                        return
                    }
                }
                state = 'rejected';
                value = error
                handelCb()
            }
            setTimeout(fn,0)
        }
        function handelCb(){
            while(callbacks.length) {
                const fn = callbacks.shift();
                handle(fn);
            };
        }
        fn(resolve, reject)
    }

exception handling

Exceptions usually refer to errors caused by code errors when executing success / failure callbacks. For such exceptions, we can use try catch to catch errors and set Promise to rejected status.
The handle code is modified as follows:

function handle(callback){
        if(state === 'pending'){
            callbacks.push(callback)
            return;
        }
        
        const cb = state === 'fulfilled' ? callback.onFulfilled:callback.onRejected;
        const next = state === 'fulfilled'? callback.resolve:callback.reject;

        if(!cb){
            next(value)
            return;
        }
        try {
            const ret = cb(value)
            next(ret)
        } catch (e) {
            callback.reject(e);
        }  
    }

When we actually use it, we are used to registering the catch method to handle errors, for example:

 new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ test: 1 })
        }, 1000)
    }).then((data) => {
        console.log('result1', data)
        //dosomething
        return test()
    }).catch((ex) => {
        console.log('error', ex)
    })

In fact, both errors and exceptions are ultimately implemented through reject. That is, they can be handled through the error callback in then. Therefore, we can add such a catch method:

 function Promise(fn){ 
        ...
        this.then = function (onFulfilled,onRejected){
            return new Promise((resolve, reject)=>{
                handle({
                    onFulfilled, 
                    onRejected,
                    resolve, 
                    reject
                })
            })
        }
        this.catch = function (onError){
            this.then(null,onError)
        }
        ...
    }

Finally method

In practical application, it is easy to encounter such a scenario. Regardless of the final state of Promise, we have to perform some final operations. We put these operations into finally, that is, finally registered functions are independent of the state of Promise and do not depend on the execution results of Promise. Therefore, we can write the logic of finally:

function Promise(fn){ 
        ...
        this.catch = function (onError){
            this.then(null,onError)
        }
        this.finally = function (onDone){
            this.then(onDone,onDone)
        }
        ...
    }

resolve method and reject method

In practical application, we can use Promise.resolve and Promise.reject methods to wrap non Promise instances as Promise instances. The following examples are:

Promise.resolve({name:'winty'})
Promise.reject({name:'winty'})
// Equivalent to
new Promise(resolve => resolve({name:'winty'}))
new Promise((resolve,reject) => reject({name:'winty'}))

In these cases, the input parameters of Promise.resolve may be as follows:

  • No parameter [directly return a Promise object in resolved status]
  • Normal data object [directly return a Promise object in resolved status]
  • A Promise instance [return the current instance directly]
  • A thenable object (a thenable object refers to an object with a then method) [turn into a Promise object and immediately execute the then method of the thenable object.]

Based on the above points, we can implement a Promise.resolve method as follows:

function Promise(fn){ 
        ...
        this.resolve = function (value){
            if (value && value instanceof Promise) {
                return value;
            } else if (value && typeof value === 'object' && typeof value.then === 'function'){
                let then = value.then;
                return new Promise(resolve => {
                    then(resolve);
                });
            } else if (value) {
                return new Promise(resolve => resolve(value));
            } else {
                return new Promise(resolve => resolve());
            }
        }
        ...
    }

Promise.reject is similar to Promise.resolve. The difference is that promise.reject always returns the rejected promise instance in a state. If the parameter of Promise.resolve is a promise instance, it returns the promise instance corresponding to the parameter, so the state is not necessarily. Therefore, the implementation of reject is much simpler, as follows:

 function Promise(fn){ 
        ...
        this.reject = function (value){
            return new Promise(function(resolve, reject) {
				reject(value);
			});
        }
        ...
    }

Promise.all

The input parameter is an array of Promise instances, then a then method is registered, and then the then method is executed after the state of Promise instances in the array is changed to fully. Here is mainly a counting logic. Whenever the state of a Promise changes to full, save the data returned by the instance, and then reduce the count by one. When the counter changes to 0, it means that all Promise instances in the array have been executed.

function Promise(fn){ 
        ...
        this.all = function (arr){
            var args = Array.prototype.slice.call(arr);
            return new Promise(function(resolve, reject) {
                if(args.length === 0) return resolve([]);
                var remaining = args.length;

                function res(i, val) {
                    try {
                        if(val && (typeof val === 'object' || typeof val === 'function')) {
                            var then = val.then;
                            if(typeof then === 'function') {
                                then.call(val, function(val) {
                                    res(i, val);
                                }, reject);
                                return;
                            }
                        }
                        args[i] = val;
                        if(--remaining === 0) {
                            resolve(args);
                        }
                    } catch(ex) {
                        reject(ex);
                    }
                }
                for(var i = 0; i < args.length; i++) {
                    res(i, args[i]);
                }
            });
        }
        ...
    }

Promise.race

With the understanding of Promise.all, Promise.race is easier to understand. Its input parameter is also an array of Promise instances, and its then registered callback method is executed when the state of a Promise in the array becomes fully. Because the Promise state can only be changed once, we only need to inject the resolve method of the Promise object generated in Promise.race into the callback function in each Promise instance in the array.

function Promise(fn){ 
    ...
    this.race = function(values) {
        return new Promise(function(resolve, reject) {
            for(var i = 0, len = values.length; i < len; i++) {
                values[i].then(resolve, reject);
            }
        });
    }
    ...
    }  

summary

Promise source code is only a few hundred lines. We can start from the execution results, analyze the execution process of each step, and then think about its role. The key point is to understand that the then function is responsible for registering callbacks, and the real execution is after the promise state is changed. When the input parameter of resolve is a promise, if you want to call it chain, you must call its then method (then.call) and inject the resolve method of the previous promise into its callback array.

Supplementary notes

Although then is generally considered a micro task. However, the browser cannot simulate micro tasks. At present, it either uses setImmediate, which is also a macro task, and it is still based on setTimeout when it is incompatible. In addition, setTimeout is also used in Polyfill (ES6 promise) of promise. Therefore, setTimeout is directly used here to replace micro tasks with macro tasks.

reference material

Complete Promise model

function Promise(fn) {
  let state = 'pending'
  let value = null
  const callbacks = []

  this.then = function (onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      handle({
        onFulfilled,
        onRejected,
        resolve,
        reject,
      })
    })
  }

  this.catch = function (onError) {
    return this.then(null, onError)
  }

  this.finally = function (onDone) {
    this.then(onDone, onError)
  }

  this.resolve = function (value) {
    if (value && value instanceof Promise) {
      return value
    } if (value && typeof value === 'object' && typeof value.then === 'function') {
      const { then } = value
      return new Promise((resolve) => {
        then(resolve)
      })
    } if (value) {
      return new Promise(resolve => resolve(value))
    }
    return new Promise(resolve => resolve())
  }

  this.reject = function (value) {
    return new Promise(((resolve, reject) => {
      reject(value)
    }))
  }

  this.all = function (arr) {
    const args = Array.prototype.slice.call(arr)
    return new Promise(((resolve, reject) => {
      if (args.length === 0) return resolve([])
      let remaining = args.length

      function res(i, val) {
        try {
          if (val && (typeof val === 'object' || typeof val === 'function')) {
            const { then } = val
            if (typeof then === 'function') {
              then.call(val, (val) => {
                res(i, val)
              }, reject)
              return
            }
          }
          args[i] = val
          if (--remaining === 0) {
            resolve(args)
          }
        } catch (ex) {
          reject(ex)
        }
      }
      for (let i = 0; i < args.length; i++) {
        res(i, args[i])
      }
    }))
  }

  this.race = function (values) {
    return new Promise(((resolve, reject) => {
      for (let i = 0, len = values.length; i < len; i++) {
        values[i].then(resolve, reject)
      }
    }))
  }

  function handle(callback) {
    if (state === 'pending') {
      callbacks.push(callback)
      return
    }

    const cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected
    const next = state === 'fulfilled' ? callback.resolve : callback.reject

    if (!cb) {
      next(value)
      return
    }	
    let ret;
    try {
     ret = cb(value)
    } catch (e) {
      callback.reject(e)
    }
	callback.resolve(ret);
  }
  function resolve(newValue) {
    const fn = () => {
      if (state !== 'pending') return

      if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
        const { then } = newValue
        if (typeof then === 'function') {
          // newValue is the newly generated promise, and resolve is the resolve of the previous promise
          // It is equivalent to calling the then method of the newly generated promise and injecting the resolve of the previous promise as its callback
          then.call(newValue, resolve, reject)
          return
        }
      }
      state = 'fulfilled'
      value = newValue
      handelCb()
    }

    setTimeout(fn, 0)
  }
  function reject(error) {
    const fn = () => {
      if (state !== 'pending') return

      if (error && (typeof error === 'object' || typeof error === 'function')) {
        const { then } = error
        if (typeof then === 'function') {
          then.call(error, resolve, reject)
          return
        }
      }
      state = 'rejected'
      value = error
      handelCb()
    }
    setTimeout(fn, 0)
  }
  function handelCb() {
    while (callbacks.length) {
      const fn = callbacks.shift()
      handle(fn)
    }
  }
  try {
  fn(resolve, reject)
  } catch(ex) {
	reject(ex);
  }
}

Tags: Javascript

Posted on Sun, 21 Nov 2021 15:01:38 -0500 by CentralOGN