preface

I’ve blogged about Promise’s syntax before, but only at an introductory level. Behind always want to write Promise to achieve, due to the understanding of the degree is limited, many times the pen failed to satisfy. It has been delayed and delayed until now.

As the Promise/A+ specification and ECMAscript specification implement the Promise API, the basic unit of asynchronous Javascript operations is gradually shifting from callback to Promise. Most of the new asynchronous apis (Fetch, Service worker) added to JavaScript/DOM platforms are also built on Promise. This understanding of Promise is not something that can be fully grasped by just reading the API and a few practices. The author of this article, analysis of the details, with readers grow together, forge ahead.

This paper is the second part of the front-end asynchronous programming solution practice series, mainly analyzing the internal mechanism and implementation principle of Promise. Subsequent asynchronous series will also include Generator, Async/Await related, and pit space.

Note: this articlePromiseComply with thePromises/A+Specification, implementation of referencethen/promise.

What is the Promise

Now that we’re talking about implementation principles, we need to make sure that we understand what promises are. Refer to the documentation as follows:

A promise represents the eventual result of an asynchronous operation. –Promises/A+

A Promise is an object that is used as a placeholder for the eventual results of a deferred (and possibly asynchronous) computation. –ECMAscript

Promises/A+ represent the end result of an asynchronous operation. ECMAscript defines Promises as placeholders for delayed or asynchronous computations. Promises/A+ represent the end result of an asynchronous operation. It’s short, but a little dense, so how can it be more straightforward?

As a story goes, a Promise is a beautiful Promise that itself will make the right delayed or asynchronous action. It promises to solve the problems that callback handling asynchronous callbacks may cause, such as calling too early, calling too late, calling too many times or too few times, and swallowing possible errors or exceptions. Also promise to accept first resolve only (..) Or reject (..) The state of the promise itself will not change after the change, the promise all through then(..) Registered callbacks are always called asynchronously in turn, promising that all exceptions will always be caught and thrown. She is a trustworthy promise.

Strictly speaking, Promise is an easy-to-reuse mechanism for encapsulation and composition in the future, enabling separation of concerns, asynchronous process control, exception bubbling, serial/parallel control, and more.

Note: mentioned in the articlecallbackSee < JavaScript You Don’t Know (Middle Volume) >> sections 2.3 and 3.3 for details of the problem

The standard interpretation

The Promise A+ specification is short and to the point, but A close reading reveals A few points that stand out.

Thenable object

Thenable is a definition of then(..) The object or function of a method. The thenable object exists to make the implementation of promises more generic, as long as it exposes A THEN (..) that follows the Promise/A+ specification. Methods. It also allows implementations that follow the Promise/A+ specification to coexist well with less formal but usable implementations.

Thenable or promise-like objects can be identified by whether they have then(..) This is actually called type checking or duck typing. For thenable value, duck type detection is roughly similar to:

if( p ! = =null && 
     (
       typeof p === 'object' || 
       typeof p === 'function'
     ) &&
     typeof p.then === 'function'
) {
    // thenable
} else {
    / / not thenable
}
Copy the code
Then callbacks are executed asynchronously

As we all know, when a Promise is instantiated, the passed function executes immediately, then(…) The callback in the. The reason for the delay will be explained later. An important point here is when the callback function is called asynchronously.

onFulfilled or onRejected must not be called until the execution context stack contains only platform code –Promise/A+

OnFulfilled or onRejected can only be called if the implementation environment stack contains only platform code. “In practice, ensure that the onFulfilled and onRejected methods are implemented asynchronously, and should be implemented in A new execution stack after the event loop in which the THEN method is called. This event queue can be implemented using either the macro task mechanism or the micro task mechanism.”

Although Promise A+ does not specify whether it will be queued as A microtask or macroTask, But the ECMAScript specification makes it clear that promises must join Job Queues (also known as Microtasks) as Promise jobs. A Job Queue is a new concept in ES6. It is built on event loop queues. Job queues also exist for asynchronous operations with low latency.

Macrotask microtask refers to two types of asynchronous tasks. When a task is suspended, the JS engine will divide all tasks into two queues according to their categories. The first task is taken out from the MacroTask queue (also called task queue), and all tasks in the MicroTask queue are taken out and executed in sequence after completion. The MacroTask task is then fetched, and the cycle continues until both queues are complete.

The timing of microTask execution is also described in the WHATWG HTML specification, which can be found here. See the appendix Event Loop for more articles.

Let’s look at another example to understand:

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
  console.log('promise1');
}).then(function () {
  console.log('promise2');
});
Copy the code

Print order? The correct answer is: promise1, promise2, setTimeout.

Before further implementing the Promise object, simply simulate asynchronous execution functions for subsequent Promise callbacks (you can also use asap libraries, etc.).

var asyncFn = function () {
  if (typeof process === 'object'&& process ! = =null && 
      typeof(process.nextTick) === 'function'
  ) {
    return process.nextTick;
  } else if (typeof(setImmediate) === 'function') {
    return setImmediate;
  }
  returnsetTimeout; } ();Copy the code
State of Promise

A Promise must be one of the following three states: Pending, Fulfilled and Rejected. Once a Promise is resolved or rejected, it cannot be migrated to any other state (that is, state immutable).

To keep the code clear, there is no exception handling. At the same time, for the convenience of expression, it is agreed as follows:

  • Forget and use phase-out instead
  • OnFulfilled uses onResolved instead

Promise constructor

Starting with the constructor, we step up to implement promises that comply with the Promsie A+ specification. Outline what the Promise constructor needs to do.

  1. Initialize thePromiseState (pending)
  2. Initialize thethen(..)Register the callback processing array (thenThe method can be the samepromiseCall multiple times)
  3. Execute incoming immediatelyfnFunction, passed inPromiseinternalresolve,rejectfunction
  4. .
function Promise (fn) {
  // omit non-new instantiation processing
  // omit fn non-function exception handling

  // Promise state variable
  // 0 - pending
  // 1 - resolved
  // 2 - rejected
  this._state = 0;
  // promise executes the result
  this._value = null;
 
  // then(..) Registers the callback processing array
  this._deferreds = [];

  // Execute the fn function immediately
  try {
    fn(value= > {
      resolve(this, value);
    },reason => {
      reject(this, reason); })}catch (err) {
    // Handle the fn execution exception
    reject(this, err); }}Copy the code

The _state and _value variables are easy to understand. What does the _deferreds variable do? Specification description: The then method can be called multiple times by the same promise. To accommodate multiple calls to then registration callback processing, the internal choice is to use the _DeferReds array to store processing objects. See the then function section to deal with object structures.

Finally, the FN function is executed and the private methods resolve and Reject inside the Promise are called. The internal details of Resolve and Reject follow.

Then the function

Promise A+ mentions that the specification focuses on providing common THEN methods. The then method can be called multiple times by the same Promise, returning a new Promise object each time. The then method accepts two parameters onResolved and onRejected (optional). After a promise is resolved or rejected, all onResolved or onRejected functions must be called back in the order in which they were registered and not more than once.

According to the above, the then function execution flow is roughly as follows:

  1. Instantiate an emptypromiseThe object is used to return (holdthenChain call)
  2. structurethen(..)Register the callback handler structure
  3. Determine the currentpromiseState,pendingState stores delay processing objectsdeferred,pendingState to performonResolvedonRejectedThe callback
  4. .
Promise.prototype.then = function (onResolved, onRejected) {

  var res = new Promise(function () {});
  // Use onResolved, onRejected to instantiate the process object Handler
  var deferred = new Handler(onResolved, onRejected, res);

  // The current state is pendding, storing deferred processing objects
  if (this._state === 0) {
    this._deferreds.push(deferred);
    return res;
  }

  // The current promise state is not pending
  // Call handleResolved to execute onResolved or onRejected callback
  handleResolved(this, deferred);
  
  // Returns the new Promise object, maintaining the chained call
  return res;
};
Copy the code

The Handler function encapsulates the storage onResolved, onRejected, and newly generated Promise objects.

function Handler (onResolved, onRejected, promise) {
  this.onResolved = typeof onResolved === 'function' ? onResolved : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}
Copy the code

Why should a chain call return a new promise

As we understand, in order for the THEN function to be chain-called, then needs to return a Promise instance. But why return a new Promise instead of just returning the current object this? Take a look at the following sample code:

var promise2 = promise1.then(function (value) {
  return Promise.reject(3)})Copy the code

If the then function returns this, then promise2 === PROMISe1, the state of promise2 should also be resolved. The onResolved callback returns a Rejected object. Promise2 cannot switch to the Promised-out state returned by the callback function. This is because the Promise state cannot be migrated when resolved or Rejected returns.

The handleResolved function asynchronously executes the onResolved or onRejected callback based on the current Promise state. Resolve or reject functions also require related functions, so they are extracted into separate modules. Scroll down to check it out.

Resolve function

When a Promise is instantiated, the incoming FN function is immediately executed, passing the internal resolve function as an argument to change the Promise state. Determine and change the current promise state and store resolve(..). The value of value. Determine if there is a current then(..) Register callback execution functions and, if present, asynchronously execute onResolved callbacks in sequence.

However, as described in thenable earlier, in order to make Promise implementation more universal, when value is then(..) The thenable object of the method needs to do the Promise Resolution Procedure described by the specification as [[Resolve]](Promise, x). (x is the value argument).

The specific logical process is as follows:

  • If a promise and x point to the same object, reject the promise as TypeError grounds

  • If x is a Promise, make the Promise accept the state of X

  • If x is an object or a function

    1. thex.thenAssigned tothen
    2. If you takex.thenIs thrown with an error e, and is rejected based on epromise
    3. ifthenTheta is a function of thetaxCalled this as the scope of the function.
    4. ifxIs not an object or a functionxExecute for parameterspromise

The original text refers to the Promise A+ specification Promise Resolution Procedure.

function resolve (promise, value) {
  // The non-pending state is immutable
  if(promise._state ! = =0) return;
  
  // Promise and value point to the same object
  // Correspond to Promise A+ specification 2.3.1
  if (value === promise) {
    return reject( promise, new TypeError('A promise cannot be resolved with itself.')); }// If value is a Promise, make the Promise accept the state of value
  // Correspond to Promise A+ specification 2.3.2
  if (value && value instanceof Promise && value.then === promise.then) {
    var deferreds = promise._deferreds
    
    if (value._state === 0) {
      // Value indicates pending status
      // Pass promise._deferreds to value._deferreds
      // Take the time to use the ES6 expansion operator
      // Corresponds to the Promise A+ specification 2.3.2.1value._deferreds.push(... deferreds) }else if(deferreds.length ! = =0) {
      // Value is not pending
      // Use value as the current promise to perform then registration callback processing
      // Corresponding to the Promise A+ specification 2.3.2.2, 2.3.2.3
      for (var i = 0; i < deferreds.length; i++) {
        handleResolved(value, deferreds[i]);
      }
      // Empty the then registration callback processing array
      value._deferreds = [];
    }
    return;
  }

  // value is an object or function
  // Correspond to Promise A+ specification 2.3.3
  if (value && (typeof value === 'object' || typeof value === 'function')) {
    try {
      // Corresponds to the Promise A+ specification 2.3.3.1
      var then = obj.then;
    } catch (err) {
      // Corresponds to the Promise A+ specification 2.3.3.2
      return reject(promise, err);
    }

    // If then is a function, value is called in the function's scope this
    // Corresponds to the Promise A+ specification 2.3.3.3
    if (typeof then === 'function') {
      try {
        // Execute the then function
        then.call(value, function (value) {
          resolve(promise, value);
        }, function (reason) { reject(promise, reason); })}catch (err) {
        reject(promise, err);
      }
      return; }}// Change the internal state of promise to 'resolved'
  // Corresponding to Promise A+ specification 2.3.3.4, 2.3.4
  promise._state = 1;
  promise._value = value;

  // Promise has then registered callback functions
  if(promise._deferreds.length ! = =0) {
    for (var i = 0; i < promise._deferreds.length; i++) {
      handleResolved(promise, promise._deferreds[i]);
    }
    // Empty the then registration callback processing arraypromise._deferreds = []; }}Copy the code

The resolve function logic is complex and focuses on handling multiple possibilities for value (x) values. If value is a Promise and the state is pending, make the Promise accept the state of value. When value is pending, simply assign the Promise’s DeferReds callback processing array to the Value Deferreds variable. Non-pending state, the deferReds registered with the Promise are called back using the value internal value.

If value is thenable, call this with value as the function’s scope, and the inner resolve(..) callback. And reject (..) Function.

In other cases, the promise is executed with value and the onResolved or onRejected handler is called.

In fact, the Promise Resolution Procedure process defined by the Promise A+ specification is used to handle then(..) The relationship between the return value of the registered onResolved or onRejected call and the then newly generated promise. However, consider that fn calls resolve(..) internally. The generated value and the current promise value still have the same relationship, logically consistent, written into the same module.

Reject function

The Promise internal private method reject is much simpler logically than resolve. As follows:

function reject (promise, reason) {
  // The non-pending state is immutable
  if(promise._state ! = =0) return;

  // Change the internal state of promise to 'rejected'
  promise._state = 2;
  promise._value = reason;

  // Determine if there is then(..) Register callback processing
  if(promise._deferreds.length ! = =0) {
    // Execute the callback asynchronously
    for (var i = 0; i < promise._deferreds.length; i++) { handleResolved(promise, promise._deferreds[i]); } promise._deferreds = []; }}Copy the code

HandleResolved function

Once you’ve looked at the Promise constructor, then, and internal resolve and Reject implementations, you’ll see that handleResolved is uniformly called for all of its callbacks. What does handleResolved do? What are the implications of implementation?

HandleResolved calls onResolved, onRejected, and then(..) based on the promise state. Register callbacks for empty cases, and maintain chained THEN (..) Function subsequent calls. The concrete implementation is as follows:

function handleResolved (promise, deferred) {
  // Execute the registration callback asynchronously
  asyncFn(function () {
    var cb = promise._state === 1 ? 
            deferred.onResolved : deferred.onRejected;

    // Pass the registration callback function is empty
    if (cb === null) {
      if (promise._state === 1) {
        resolve(deferred.promise, promise._value);
      } else {
        reject(deferred.promise, promise._value);
      }
      return;
    }

    // Perform the registration callback
    try {
      var res = cb(promise._value);
    } catch (err) {
      reject(deferred.promise, err);
    }

    // Handle chain then(..) Register to handle function calls
    resolve(deferred.promise, res);
  });
}
Copy the code

In the following example, the registration callback function CB is empty. If the current callback cb is empty, deferred. Promise is used as the current promise and the subsequent handler is called with value to continue the execution, and the value is passed forward through the empty handler.

Promise.resolve(233)
  .then()
  .then(function (value) {
    console.log(value)
  })
Copy the code

Just a quick word about then chain calls. Implement chain calls to then functions, just in promise.prototype.then (..) Just return the new Promise instance from the handler. But beyond that, you need to call the then-registered callback handlers in turn. As shown in resolve(deferred. Promise, res), the last line of the handleResolved function.

Then why does the registration callback function execute asynchronously

The onResolved and onRejected functions registered with then can be executed asynchronously. Let’s look at another example of code.

var a = 1;

promise1.then(function (value) {
  a = 2;
})

console.log(a)
Copy the code

Promise1 Performs internal synchronous or asynchronous operations. If the then registration callback is not specified to be executed asynchronously, there may be two values for printing a here. A === 2 for promise1 internal synchronization and 1 for asynchronous operation. To shield external uncertainties from dependence, the specification specifies the onFulfilled and onRejected methods to be executed asynchronously.

Promise internal error or exception

If the promise is rejected, the reject callback is called and passed in. For example, if an exception occurs during the Promise creation (when the FN executes), the exception will be caught and onRejected will be called.

One detail, though, is what happens if an exception error occurs when onResolved is called after the Promise has completed to see the result? Note that onRejected will not be triggered because the onResolved internal exception will not change the current promise state (still Resolved). Instead, the promise state in then will be changed to Rejected. The exception is not lost but the error handler is not called.

How to deal with it? The Ecmascript specification defines the promise.prototype. catch method. If you’re not confident in onResolved or have an exception case, it’s best to call the catch method after the then function.

Promise related method implementation

Check the Promise documentation or books, and you’ll also find promising apis: promise.race, promise.all, Promise.resolve, promise.reject. Here is a demonstration of the Promise. Race method implementation, and the rest can be implemented by reference.

Promise.race = function (values) {
  return new Promise(function (resolve, reject) {
    values.forEach(function(value) {
      Promise.resolve(value).then(resolve, reject);
    });
  });
};
Copy the code

conclusion

At this point, the core Promise implementation is gradually completed, and the internal details of the Promise are described in the text or in the code. Due to the limited ability of the author, the internal realization of PROMISE has not reached the level of paoding yet, and some parts have been mentioned, which may make readers confused. It is recommended to read it twice or refer to a book for understanding.

If you can read a little bit of harvest, but also to reach the original intention of the author, we grow together. Finally, the author is not perfect, the text can not avoid sentences or words, hope to understand. If you have any questions or mistakes about this article, please let me know and thank you in advance.

The appendix

Reference documentation

  1. ECMA262 Promise

  2. Promises/A+ Specification

  3. Promises/A+ Promises

  4. then/promise

  5. Write A Promise that Promises/A+ specifications and can be used with ES7 Async /await

  6. Analyze the internal structure of Promise, step by step to achieve a complete Promise class that can pass all Test cases

  7. Break down the basics of Promises

Reference books

  1. JavaScript You Don’t Know (Middle Volume)

  2. Deep Understanding of ES6

  3. JavaScript Framework Design (Version 2)

  4. Introduction to ES6 Standards (3rd Edition)

event loop

  1. Tasks, microtasks, queues and schedules

  2. Queues and schedules

  3. Difference between microtask and macrotask within an event loop context

  4. Explore javaScript asynchrony and browser update rendering timing from the Event Loop specification

  5. Delve deeper into the timing of Eventloop and browser rendering

Hit an advertisement, welcome to pay attention to the author’s public number