Promise goes from entry to proficiency

1.Why do we need promise?

Javascript is a single-threaded language, so in the early days when we solved asynchronous scenarios, most of the time it came in through callback functions
That's ok.
For example, sending an ajax request in a browser is an asynchronous scenario that often occurs. After sending the request, it takes some time for the server to respond to it before we get the result. If we want to perform an operation after the asynchronous end, we can only do so through a callback function.

var dynamicFunc = function(cb) { setTimeout(function() {
	cb();
	}, 1000);
}
dynamicFunc(function() {console.log(123)});
  • dynamicFunc in the example above is an asynchronous function
  • The setTimeout executed inside calls the incoming cb function after 1s
  • Following the call above, 123 results will be printed after the last 1s
    Similarly, if there is anything to output at the end of an asynchronous function, multiple asynchronous functions are required to be nested, which is very detrimental to subsequent maintenance.
setTimeout(function() { 
	console.log(123); 
	setTimeout(function() {
		console.log(321);
		// ...
	}, 2000); 
}, 1000);

In order to make callback functions call in a more elegant way, ES6 js produced a new specification called promise, which makes asynchronous operations almost "synchronized".

2.Promise Foundation

In an advanced browser environment that supports ES6, we can construct a promise instance through Promise().
This constructor accepts a function that accepts two parameters, resolve and reject, representing the need to change the state of the current instance to either completed or rejected.

function promise1() {
	return new Promise(function(resolve, reject) {
		// Define Asynchronous Content 
  	setTimeout(function() {
			console.log('1s Post-output');
			// When the output is complete, call the resolve function passed in by the function, mark the promise instance as complete, and the current promise serial execution continues
			resolve(); 
    }, 1000);
	});
}
function promise2() {
  return new Promise(function(resolve) {
		setTimeout(function() { 
			console.log('2s Post-output'); resolve();
		}, 2000); 
	});
}

The two promise instances above are concatenated and written as:
promise1().then(function() { return promise2(); });

It can also be abbreviated as promise1().then(promise2);

After execution in the browser, you can see that after 1s the font will be output after 1s, and after 2S the font will be output after 2s. As we can see in this example, if the current promise state becomes complete (resolve method is executed), then the next promise function in the then method will be executed.

Similarly, if our promise becomes rejected (the reject method is executed), it enters a subsequent exception-handling function.

function promise3() {
	return new Promise(function(resolve, reject) {
		var random = Math.random() * 10; // Random 1 - 10 number 
    setTimeout(function() {
      if (random >= 5) { 
        resolve(random);
      } else {
        reject(random);
      }
		}, 1000);
	}); 
}

var onResolve = function(val) { 
  console.log('Completed:The output number is', val);
};

var onReject = function(val) { 
  console.log('Rejected:The output number is', val);
}

// promise's then also accepts two functions, the first after resolve and the second after reject
promise3().then(onResolve, onReject);

// You can also use the.catch method to intercept when the state becomes rejected 
promise promise3().catch(onReject).then(onResolve);

// You can also try catch to intercept a promise that has been rejected 
try {
	promise3().then(onResolve); 
} catch (e) {
  onReject(e);
}
  • This example uses three ways to intercept a promise that eventually becomes a "rejected" state, one for each
    • Using the second parameter of the
    • Use the.Catch method to catch exceptions thrown by the forward promise
    • Use try catch to intercept exceptions thrown by promise in the code block

We can also find that when we call the resolve and reject functions when changing the promise state, we can also pass parameters to the function that will be executed in the next one. In this example, we pass randomly generated numbers to the resolve and reject functions, and we can also get this value when we execute the function in the then.

To summarize this section:

  1. There are three states of promise, in-progress Completed and Rejected. The in-progress state can be changed to Completed or Rejected and cannot be changed after the status has been changed (for example, from Completed to Rejected).

  2. The Promise constructor in ES6, which we construct, needs to pass in a function that accepts two function parameters. After the first parameter is executed, the current promise state is changed to Completed, and after the second parameter is executed, it becomes Rejected.

  3. The.then method allows you to continue executing the next function or promise when the last promise has reached completion, while passing in parameters when resolve or reject gives you an initial value for the next function or promise.

  4. A rejected promise can then be captured by either the.Catch method or the second parameter of the.Then method or try catch.

3.How to encapsulate asynchronous operations as promise

We can encapsulate any function that accepts callbacks as a promise. Here are a few simple examples to illustrate.

// Primitive function
function dynamicFunc(cb) {
	setTimeout(function() { 
    console.log('1s Rear display'); 
    cb();
	}, 1000); 
}

var callback = function() { 
  console.log('After asynchronization ends log');
}

// Execute as incoming callback function 
dynamicFunc(callback);

The example above is the most traditional way to execute a function after asynchronization by using an incoming callback function. We can turn this asynchronous function into a promise by encapsulating a promise. The following:

function dynamicFuncAsync() {
  return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log('1s Rear display'); 
      resolve();
		});
  });
}

var callback = function() { 
  console.log('After asynchronization ends log');
}

dynamicFuncAsync().then(function() { callback() });

For another example, sending an ajax request can also be encapsulated:

function ajax(url, success, fail) {
	var client = new XMLHttpRequest(); 
  client.open("GET", url); 
  client.onreadystatechange = function() {
		if (this.readyState !== 4) { 
      return;
		}
    
		if (this.status === 200) {
			success(this.response); } else {
			fail(new Error(this.statusText)); 
    }
	};
	client.send(); 
};

ajax('/ajax.json', function() {console.log('Success')}, function() {console.log('fail')});

We can see that callbacks to ajax methods need to pass in callback functions for success and fail. Instead of passing in callback functions, we can change the current promise state where the callback function was originally executed by encapsulating the promise, and we can call it chained.

function ajaxAsync(url) {
	return new Promise(function(resolve, reject){
		var client = new XMLHttpRequest(); 
    client.open("GET", url); 
    client.onreadystatechange = function() {
			if (this.readyState !== 4) { 
        return;
			}
			if (this.status === 200) {
				resolve(this.response);
   		} else {
				reject(new Error(this.statusText));
			} 
    };
		client.send(); 
  });
};

ajax('/ajax.json')
	.catch(function() {
		console.log('fail'); 
	})
	.then(function() { 
  	console.log('Success');
	})

To summarize the current section:

  1. We can easily change any function or asynchronous function to promise, especially asynchronous function, which can then be chained for readability.

  2. Changing asynchronous with a callback function to promise is also simple, simply by executing the corresponding function that changes the promise state where the callback function was originally executed after internally instantiating the promise.

4.Interpretation of promise specification

Any object or function that conforms to the promise specification can be a promise, promise A plus specification address:https://promisesaplus.com/

We are familiar with the use of the overall promise above, how to create a promise, how to use it, and how to transform the callback function to promise later. In this subsection, we go through the promise A+ specification in detail to understand the specifications of how promise works.

4.1 Terminology

Promise: promise is an object or function that has the then method and behaves in accordance with this specification

With the then method: is an object or function that defines the then method

Value: The legal value of any JavaScript (including undefined, thenable, and promise)

Exception: is a value thrown using a throw statement

Reason: Indicates a promise's rejection reason.

4.2 Requirements

1.Status of promise

The current state of a Promise must be one of three states: Pending, Fulfilled, and Rejected.

2. There must be a then method

A promise must provide a then method to access its current value and reason.

The then method of promise accepts two parameters: promise.then(onFulfilled, onRejected) are both optional parameters, and they are both functions. If onFulfilled or onRejected are not functions, they need to be ignored.

  • If onFulfilled is a function

    • When promise finishes execution it must be called, and its first argument is the result of promise
    • promise cannot be called until its execution ends
    • It cannot be called more than once
  • If onRejected is a function

    • When a promise is rejected for execution it must be called, and its first argument is the reason for promise
    • promise cannot be called until it is rejected for execution
    • It cannot be called more than once
  • onFulfilled or onRejected must not be called until the execution context stack contains only platform code

  • onFulfilled and onRejected must be called as normal functions (i.e. uninstantiated so that the inner this of the function points to window in a non-strict mode)

  • The then method can be called multiple times by the same promise

    • When promise is successfully executed, all onFulfilled callbacks are required in the order in which they are registered
    • When promise is rejected, all onRejected callbacks must be made in the order in which they are registered
  • The then method must return a promise object promise2 = promise1.then(onFulfilled, onRejected);

    • As long as onFulfilled or onRejected returns a value x, promise 2 will enter the onFulfilled state
    • If onFulfilled or onRejected throws an exception e, promise2 must reject execution and return rejection e
    • If onFulfilled is not a function and the promise1 state becomes complete, promise2 must execute successfully and return the same value
    • If onRejected is not a function and the promise1 state becomes rejected, promise2 must perform a reject callback and return the same reason
var promise1 = new Promise((resolve, reject) => {reject() }); 

promise1
	.then(null, function() { 
  	return 123;
	})
	.then(null, null) 
	.then(null, null) 
	.then(
  	() => {
			console.log('promise2 Completed');
		},
  	() => {
			console.log('promise2 Rejected'); 
   });

5.Static method on Promise constructor

Promise.resolve

Returns a promise instance with its status set to Completed and its results as values passed into the promise instance

var promise = Promise.resolve(123);
promise 
  .then(function(val) {
		console.log('Completed', val); 
	});
// 123 Completed

Similarly, the parameters of Promise.resolve can handle objects, functions, and so on in the same way as described in the above specification.

Promise.reject

Returns a promise instance, sets its status to rejected, and passes its result as a reason into the onRejected function

var promise = Promise.reject(123);
promise
	.then(null, function(val) {
	console.log('Rejected', val); 
	});
// Rejected 123

Promise.all

Returns a promise instance, accepts an array containing multiple promise instances, enters the completed state when all promise instances become completed, or enters the rejected state.

var promise1 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(1); 
      resolve();
		}, 1000)
  }); 
}
 
var promise2 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(2); 
      resolve();
		}, 2000); 
  });
}

Promise.all([promise1(), promise2()]) 
  .then(function() {
		console.log('whole promise All completed'); 
	});

Note that multiple promises occur at the same time, that is, in the above example, after waiting for 1s to print 1, then waiting for 1s will print 2 and "All promises have been completed".

Promise.race

Returns a promise instance, accepts an array containing multiple promise instances, and enters that state when one of the promise instances changes. All promise instances here are competitive, and only the first one that enters the changed state is selected.

var promise1 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(1); 
      resolve(1);
		}, 1000) 
  });
}

var promise2 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(2); 
      resolve(2);
		}, 2000); 
  });
}

Promise.race([promise1(), promise2()]) 
  .then(function(val) {
		console.log('There is one promise The state has changed', val); 
	});

Six.Implement Promise

The following code does not strictly follow the Promise A+ specification and is only demo testing

class MyPromise{
  // 1.handleFn: (resolve,reject)=>{}
  // 2. Frame:
  // (1) Method invoked by instantiation: then...promise.then()
  // (2) The method called by the constructor: static resolve...Promise.resolve()
  constructor(handleFn){
    // state
    this.status = 'pending';
    // value
    this.value = undefined;
    // fulfilled completed callback array
    this.fulfilledList = [];
    // Rejected rejected callback array
    this.rejectedList = [];

    // Execute handleFn, 4.triggerResolve, triggerReject
    handleFn(this.triggerResolve.bind(this), this.triggerReject.bind(this));
  }

  /**
   * Define the success method as a parameter to the function body passed in by Promise
   * Implement PromiseA+State Transition Define Success Parameters
   */
  triggerResolve(val){
    // Because you need to get the callback functions registered in the then before you know which callback functions to execute in the resolve, you can execute the callback again in the next Event Loop event loop in the form of setTimeout (()=>, 0)
    // The triggerResolve function here, although handleFn is executed within the constructor, setTimeout is executed in the next event loop, so it's actually the one that executes first that's the code inside setTimeout
    setTimeout(() => {
      // Here, by defining the then method, you can get the callback function [function () {console.log ('resolve')) in the then to execute the corresponding method while resolving 
      // If the code here is executed synchronously, not in setTimeout, the state has changed at this point
      // When it actually happens, it's not done completely, so you have to decide
      if(this.status !== 'pending') return;

      // Determine if the result passed in by the resolve method is promise
      if(val instanceof MyPromise){
        // Yes promise, you need to define a promise for it, and its state will change all the current states
        val.then(
          value => {},
          err => {}
        )
      }else{
        // The resolve method passes in a normal value
        // Change the current promise status to Completed
        this.status = 'fulfilled';
        // Record values at the same time
        this.value = val;
        // Then trigger all callbacks previously recorded in the previous event loop
        this.triggerFulfilled(val);
      }
    }, 0);
  }

  /**
   * Define the failure method as a parameter to the function body passed in by Promise
   * Implementing PromiseA+State Transition Definition Failure Parameters
   */
  triggerReject(val){
    // Write by Yourself
    setTimeout(() => {
      if (this.status !== 'pending') return;
      if(val instanceof MyPromise){
        val.then(
          value => {},
          err => {}
        )
      } else{
        this.status = 'rejected';
        this.value = val;
        this.triggerRejected(val);
      }
    },0);
  }

  /**
   * Execute all callbacks in completed state
   */
  triggerFulfilled(val){
    // Get all the completed callbacks defined on this and execute them as forEach 
    this.fulfilledList.forEach(item => item(val));
    this.fulfilledList = [];
  }

  /**
   * Execute all callbacks in rejected state
   */
  triggerRejected(val){
    this.rejectedList.forEach(item => item(val));
    this.rejectedList = [];
  }

  /**
   * 3.Register the asynchronous function [callback function] in the then and execute it when you call resolve [triggerResolve]
   * @param onFulfilled Registered callback function when the state to be executed changes during asynchronous execution
   * @param onRejected Registered callback function when the state to be executed changes during asynchronous execution
   * @returns Each then returns a Promise
   */
  then(onFulfilled, onRejected){
    const {status,value} = this;

    // Each then returns a Promise
    const promiseInstance = new MyPromise((onNextFulfilled, onNextRejected) => {

      /**
       * Call down for chain
       * @param val triggerFulfilled(val)When, to execute, when the current value changes, to change this parameter
       * @description 
       * ① To concatenate onFulfilled with onNextFulfilled; when the onFulfilled function is executed, you know how to execute onNextRejected
       * ② During the recording of callback functions, [the callback function registered in the last one] is merged with [the resolve function returned from the new promise] in the form of an internal closure function. The result of the callback function to be executed is
       * ③ During the onFinalFulfilled execution, you can execute the resolution [onNextFulfilled] function of the newly created promise [new MyPromise ((onNextFulfilled, onNextRejected) =>{})]
       * ④ When the last registered callback function is executed, we can execute the result of the next promise
       */
      function onFinalFulfilled(val){
        if(typeof onFulfilled !== 'function'){
          // When will the next promise result be executed?
          // Specification: If the result returned by the previous promise is not a function, go directly to the previous promise and put the result returned by the next promise to execute  
          onNextFulfilled(val);
        } else {
          // Let's start with the promise from the previous step, which is res
          const res = onFulfilled(val);
          if(res === promiseInstance){
            throw new TypeError('Can't be the same promise')
          }
          // Determine if the return is promise
          if(res instanceof MyPromise){
            // Register onNextFulfilled and onNextRejected callbacks using the promise.then method
            // How do I get to the next one?
            // Status via res:
            // (1) If res is a resolve state, that is, a completed state, then go to the next promise's resolve state [onNextFulfilled]
            // (2) If res is rejected, that is, rejected, then go to the reject state of the next promise [onNextRejected]
            res.then(onNextFulfilled, onNextRejected);
          } else {
            // Returns a normal value by calling the result of the next promise
            onNextFulfilled(val);
          }
        }
      }

      function onFinalRejected(error){
        if(typeof onRejected !== 'function'){
          onNextRejected(error);
        }else{
          let res = null;
          try {
            res = onRejected(error);
          } catch (e) {
            // If catch has a problem at this step, pass on the problem exposed at this step when executing the next promise chain;
            onNextRejected(e);
          }

          // Otherwise, this is still a resolve state
          if(res instanceof MyPromise){
            res.then(onNextFulfilled, onNextRejected);
          }else{
            onFulfilled(res);
          }
        }
      }

      // Execute different methods based on different promise States
      switch (status) {
        case 'pending':{
          // You need to record the two callback functions that are currently registered
          this.fulfilledList.push(onFinalFulfilled);
          this.rejectedList.push(onFinalRejected);
          break;
        }
        case 'fulfilled': {
          onFinalFulfilled(value);
          break;
        }

      }
    })

    return promiseInstance;
  }

  catch(onRejected){
    return this.then(null, onRejected)
  }
  
  /**
   * MyPromise.resolve Realization
   */
  static resolve(value){
    // If the returned value is promise, return it directly
    if(value instanceof MyPromise) return value;
    // If not, return a promise instance and set its status to Completed, with its result value as the value passed in to the promise instance
    return new MyPromise(resolve => resolve(value));

  }

  /**
   * MyPromise.reject Realization
   */
  static reject(reason){
    if(reason instanceof MyPromise) return reason;
    return new MyPromise((resolve,reject) => reject(reason))
  }

  /**
   * MyPromise.all Realization
   */
  static all(list){
    return new MyPromise((resolve,reject) => {
      if(!Array.isArray(list)) {
        return reject(new Error('Please pass in an array'))
      }

      let couter = 0
      let values = [];
      // Here for of is parallel; however, in async await, for of may have asynchronous iterations that execute in turn
      // You can also use forEach, for...
      for (const [i, MyPromiseInstance] of list.entries()) {
        MyPromise.resolve(MyPromiseInstance)
        .then(
          res => {
            values[i] = res;
            couter++;
            if(couter === list.length) resolve(values);
          }, 
          err => {
            reject(err)
          }
        )
      }
    })
  }

  /**
   * MyPromise.race Realization
   */
  static race(list){
    return new MyPromise((resolve,reject) => {
       if(!Array.isArray(list)) {
        return reject(new Error('Please pass in an array'))
      }
      list.forEach((item,i) => {
        //  When a promise enters the resolve state, it enters then
        MyPromise.resolve(item)
        .then(
          res => {
            resolve(res)
          },
          err => {
            reject(err)
          }
        )
      })
    })
  }
}




// new Promise execution order during instantiation:
// First instantiate, execute handleFunc, then enter the then, register the callback function 
// (2) That's when you go to setTimeout() of triggerResolve
const promise1 = new MyPromise(function(resolve,reject) { 
  // Note: The pending state is when you enter the then!!!  
  // Since the unction (resolution, reject) {} function here is executed immediately at first, its resolve function is not registered yet, so it is still pending at the time of registration
  // After registration, execute the method in resolve in the next event loop
  resolve()
  // reject()
})

promise1.then(function () { console.log('resolve') })

7.supplement

function sleep(time = 1) {
    return new Promise(resolve => setTimeout(function() {console.log('promise resolve'); resolve()}, time * 1000))
}

const promiseCreatorList = [
    sleep,
    sleep,
    sleep
]

/********************************************************************/
// 1.Promise.all parallel execution
console.log('Promise.all start', new Date().getTime())
Promise.all(
    promiseCreatorList.map(item => item())
).then(
    () => console.log('Promise.all end', new Date().getTime())
)

/********************************************************************/
// 2.For of async iteration executes in turn
async function main() {
    console.log('for of async start', new Date().getTime())
    async function forOfLoop() {
        for (const promiseInstance of promiseCreatorList) {
            await promiseInstance()
        }
    }
    await forOfLoop()
    console.log('for of async end', new Date().getTime())
}
main()

/********************************************************************/
// 3.Concatenating Parallel Promise s
const promiseChain = promiseCreatorList.reduce((memo, current)=>{
    if(memo.then){
        return memo.then(current)
    }
    return memo().then(current)

})
// Equivalent to sleep().then(sleep).then(sleep)
promiseChain.then(function () {
    console.log('Completed')
})

/********************************************************************/
// 4.Execute sequentially through Promise implementation
const promiseChain2 = promiseCreatorList.reduce((memo, current)=>{
    return memo().then(current)
}, Promise.resolve())
// Equivalent to Promise.resolve().then(sleep).then(sleep).then(sleep)
promiseChain2.then(function () {
    console.log('Completed')
})

Tags: Javascript

Posted by brianlange on Mon, 20 Sep 2021 17:23:15 +0530