I thought I knew all about promises until A while ago when I tried to implement the Promise/A+ specification. In my accordance with the Promise/A+ specification to write specific code implementation process, I experienced from “very understand” to “strange”, and then to “understand” the roller coaster cognitive change, Promise has A deeper understanding!

TL; DR: Since many people don’t want to read A long article, here’s A straightforward Javascript implementation of the Promise/A+ specification I wrote.

  • Github Warehouse: Promises – APlus – Robin
  • The source code
  • Annotated version of source code

Promises – Tests all passed.

Promises come from the real world

A Promise literally translates as a Promise, and the latest little Red Book has translated it as a Promise. Of course, it doesn’t matter. Programmers just look at each other.

Make promise

As a worker, we will inevitably receive all kinds of cake, such as the cake of oral praise, the cake of appreciation and salary increase, the cake of equity incentive……

Some pies are cashed in immediately, such as verbal praise, because it costs the company nothing in itself. Some cakes, on the other hand, are of real interest to the company. They may have a long future, or they may die, or they may simply fail.

The action of drawing a pie, in Javascript terms, creates an instance of a Promise:

const bing = new Promise((resolve, If (' reject ') {resolve(' reject ') else {resolve(' reject ')}})Copy the code

Much like these cookies, promises come in three states:

  • Pending: The pie is ready, waiting for the realization.
  • This is a big pity: Cake really realizes and walks to the top of his life.
  • A: Rejected! Emmm…

Subscribe to the commitment

Someone draws the cake, someone takes it. To “catch the pie” is to imagine the possibilities of the pie. If the pie really realized, I will villa by the sea; If the cake fails, the worker will be in tears.

And that translates into the concept of Promise, which is a subscription model, where we subscribe to successes and failures and we respond to them. Subscriptions are implemented through then, catch, and so on.

Then (success => {console.log(' house by sea ')}, // Fail to respond to pie failures => {console.log(' Tears... ')})Copy the code

The chain transmission

As we all know, the boss can draw the pie for the top management or leaders, and the leaders take the pie drawn by the boss, must also continue to draw the pie for the employees below, so that the workers keep blood, so that everyone’s pie is possible to cash.

This top-down cookie sending behavior and the Promise of the chain call in the idea happened to coincide.

BossBing. Then (success => { Return leaderBing}). Then (success => {console.log('leader's pie is actually implemented, }, fail => {console.log('leader drew the cake burst, in tears... ')})Copy the code

In general, promises are pretty similar to real world promises.

However, there are many details about the implementation of Promise, such as the details of asynchronous processing, Resolution algorithm, and so on, which will be covered later. I’ll start with my first impressions of Promises, move on to macro and micro tasks, and finally unveil the Promise/A+ specification.

First Promise

Back when I first started working with Promise, I thought it was “cool” to be able to encapsulate the Ajax process. At the time, the Promise seemed to be an elegant asynchronous wrapper, eliminating the need to write highly coupled callbacks.

Here’s an example of a temporary ajax wrapper:

function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]';
}

function serialize(params) {
    let result = ' ';
    if (isObject(params)) {
      Object.keys(params).forEach((key) = > {
        let val = encodeURIComponent(params[key]);
        result += `${key}=${val}& `;
      });
    }
    return result;
}

const defaultHeaders = {
  "Content-Type": "application/x-www-form-urlencoded"
}

// Ajax simple encapsulation
function request(options) {
  return new Promise((resolve, reject) = > {
    const { method, url, params, headers } = options
    const xhr = new XMLHttpRequest();
    if (method === 'GET' || method === 'DELETE') {
      // GET and DELETE are usually passed using queryString
      const requestURL = url + '? ' + serialize(params)
      xhr.open(method, requestURL, true);
    } else {
      xhr.open(method, url, true);
    }
    // Set the request header
    const mergedHeaders = Object.assign({}, defaultHeaders, headers)
    Object.keys(mergedHeaders).forEach(key= > {
      xhr.setRequestHeader(key, mergedHeaders[key]);
    })
    // Status monitoring
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
    xhr.onerror = function(e) {
      reject(e)
    }
    // Process the body data and send the request
    const data = method === 'POST' || method === 'PUT' ? serialize(params) : nullxhr.send(data); })}const options = {
  method: 'GET'.url: '/user/page'.params: {
    pageNo: 1.pageSize: 10}}// Call the interface in the form of Promise
request(options).then(res= > {
  // The request succeeded
}, fail= > {
  // The request failed
})
Copy the code

The above code encapsulates the main process of Ajax, while many other details and scenarios can’t be covered in dozens of lines of code. But as we can see, the core of what Promise encapsulates is:

  • Encapsulate a function that wraps the code containing the asynchronous procedure in the executor that builds the Promise. The encapsulated function finally needs to return the Promise instance.
  • Promise has three states: Pending, depressing, and Rejected. whileresolve().reject()Is a trigger for state transition.
  • Determine the condition for the state transition. In this case, we assume that the request succeeds (executes) when the Ajax response has a status code of 200resolve()), otherwise the request fails (executereject()).

Ps: In actual services, in addition to the HTTP status code, we also determine the internal error code (the status code agreed on the front and back ends of the service system).

In fact, with solutions like Axios, it’s not easy to wrap Ajax in your own, to discourage the repetition of this basic and important wheel, not to mention some scenarios that are hard to think through. Of course, if time permits, you can learn the source implementation.

Macro and micro tasks

To understand the Promise/A+ specification, we need to go back to its roots. Promises are closely related to microtasks, so it’s important to have A basic understanding of both macro and microtasks.

For a long time, I didn’t pay much attention to macro tasks and microtasks. There was even a time when I thought setTimeout(fn, 0) was very useful for manipulating dynamically generated DOM elements, but I didn’t know what was behind it, it was essentially related to Task.

var button = document.createElement('button');
button.innerText = 'New Input field'
document.body.append(button)

button.onmousedown = function() {
  var input = document.createElement('input');
  document.body.appendChild(input);
  setTimeout(function() {
    input.focus();
  }, 0)}Copy the code

Focus () has no effect without using setTimeout 0.

So, what are macro tasks and micro tasks? Let’s take our time to find out.

See Inside Look at Modern Web Browser for a multi-process architecture. The most closely related front end is the Renderer Process, in which Javascript runs in the Main Thread.

Renderer: Controls anything inside of the tab where a website is displayed.

The rendering process controls everything about the page displayed in the Tab page. It can be understood that the rendering process is dedicated to a specific web page.

We know that Javascript can interact directly with an interface. Imagine if Javascript were multithreaded, and each thread could manipulate the DOM, who would be the final interface? There is obviously a contradiction. Therefore, there is one important reason why Javascript chooses to use the single-threaded model: to ensure strong consistency in the user interface.

In order to ensure consistency and smoothness of interface interaction, Javascript execution and page rendering are executed alternately in the Main Thread (in some cases, the browser will skip the rendering step because it does not need to perform interface rendering for performance reasons). At present, the screen refresh rate of most devices is 60 times/second, and a frame is about 16.67ms. Within this frame, it is necessary to complete the execution of Javascript and the rendering of the interface (if necessary). The residual shadow effect of human eyes is used to make the user feel that the interface interaction is very smooth.

Take a look at the basic process in frame 1, quoted from aerotwist.com/blog/the-an…

PS: requestIdleCallback is an idle callback. At the end of frame 1, if there is time left, requestIdleCallback is called. Do not modify the DOM in the requestIdleCallback or read Layout information that triggers Forced Synchronized Layout, which may cause performance and experience problems. See Using requestIdleCallback for details.

As we know, there is only one Main Thread in the Render Process of a web page. In essence, Javascript tasks are executed sequentially in the execution stage, but the Javascript engine will divide the code into synchronous tasks and asynchronous tasks when parsing Javascript codes. The synchronization task is directly executed on the Main Thread. The asynchronous task enters the task queue and is associated with an asynchronous callback.

In a Web app, we write some Javascript code or reference some scripts to initialize the application. Within this initial code, the synchronized code is executed sequentially. During the execution of the synchronous code, events are listened for or asynchronous apis are registered (network specific, IO specific, etc.). These Event handlers and callbacks are asynchronous tasks, which are queued and processed in the following Event Loop.

Asynchronous tasks are divided into tasks and microtasks, each of which has a separate data structure and memory to maintain.

Here’s a simple example:

var a = 1;
console.log('a:', a)
var b = 2;
console.log('b:', b)
setTimeout(function task1(){
  console.log('task1:'.5)
  Promise.resolve(6).then(function microtask2(res){
    console.log('microtask2:', res)
  })
}, 0)
Promise.resolve(4).then(function microtask1(res){
  console.log('microtask1:', res)
})
var b = 3;
console.log('c:', c)
Copy the code

After the above code is executed, output in the console successively:

a: 1
b: 2
c: 3
microtask1: 4
task1: 5
microtask2: 6
Copy the code

It’s not hard to take a closer look, but it’s worth exploring the details of what’s going on behind the scenes. Let’s start by asking ourselves a few questions.

What are tasks and microtasks?

  • The Tasks:
    • setTimeout
    • setInterval
    • MessageChannel
    • I/0 (file, network) related apis
    • DOM event listening: browser environment
    • setImmediate: Node environment, IE seems to support it (see Caniuse data)
  • Microtasks:
    • requestAnimationFrame: Browser environment
    • MutationObserver: Browser environment
    • Promise.prototype.then.Promise.prototype.catch.Promise.prototype.finally
    • process.nextTick: the Node environment
    • queueMicrotask

Is requestAnimationFrame a microtask?

RequestAnimationFrame (rAF) is often used for animation because its callbacks are executed at the same rate as the browser’s screen refresh rate, which is generally referred to as 60FPS. Before rAF was widely used, we often used setTimeout for animation. However, setTimeout may not be timely scheduled when the main thread is busy, resulting in the lag phenomenon.

So is rAF a macro task or a micro task? In fact, many websites do not give a definition, including MDN also described very simple.

We might as well ask ourselves, is rAF a macro task? RAF can be used to replace timer animation. How can it be scheduled by Event Loop like timer task?

I asked myself again, is rAF a micromission? RAF is called just before the next browser redraw, which looks similar to the timing of microtasks, leading me to think rAF is a microtask when rAF is not a microtask. Why do you say that? Please run this code.

function recursionRaf() {
	requestAnimationFrame(() = > {
        console.log('the raf callback')
        recursionRaf()
    })
}
recursionRaf();
Copy the code

You will find that in the case of infinite recursion, the rAF callback executes normally and the browser interacts normally without blocking.

This would not be the case if rAF were micromissions. You can refer to the following section “What if a Microtask is created while it is executing?” .

Therefore, the TASK level of rAF is very high and has a separate queue maintenance. RAF and Javascript execute within 1 frame of the browser, and browser redraw is at the same Level. (In fact, you can see this in the previous picture of “Anatomy 1”.)

Task and Microtask have 1 queue each?

Initially, I thought that since the browser differentiated between Tasks and microtasks, it would simply arrange a queue for each Task. In fact, tasks are arranged in separate queues depending on the Task source. For example, Dom events belong to tasks, but there are many types of Dom events. In order to facilitate user agent to subdivide tasks and fine-arrange the processing priorities of tasks of different types, or even do some optimization work, a Task source must be used to distinguish tasks. Similarly, microTasks also have their own Microtask task source.

See a paragraph in the HTML standard for a detailed explanation:

Essentially, task sources are used within standards to separate logically-different types of tasks, which a user agent might wish to distinguish between. Task queues *are used by user agents to coalesce task sources Within a given event loop.

What is the consumption mechanism for Tasks and MicroTasks?

An event loop has one or more task queues. A task queue is a set of tasks.

Javascript is event-driven, so Event loops are at the heart of asynchronous task scheduling. Tasks are not a Queue, but a Set in terms of data structure. In each Event Loop, the first runnable Task (the first executable Task, not necessarily the first Task in order) is pulled into the Main Thread for execution, and then the Microtask queue is checked and all microtasks in the queue are executed.

Say more, is not as intuitive as a picture, please look!

When are tasks and microtasks queued?

Looking back, we’ve been talking about the concept of “asynchronous tasks being queued”, so when do tasks and microtasks actually queue? So let’s get this straight again. Asynchronous tasks have three key behaviors: registration, enqueueing, and callback execution. Registration is easy to understand; it means the task has been created; When a callback is executed, it means that the task has been picked up and executed by the main thread. However, when it comes to queuing, macro tasks and micro tasks behave differently.

The macro task is queued

For tasks, they will be queued when they are registered, but the status of the Task is not runnable yet, so it does not have the conditions to be picked up by the Event Loop.

Let’s start with a Dom event as an example.

document.body.addEventListener('click'.function(e) {
    console.log('Clicked', e)
})
Copy the code

When the addEventListener line is executed, the Task is registered, indicating that a user has clicked on the Task associated with the event and entered the Task queue. So when does this macro task become runnable? Of course, after the user clicks and the signal is transmitted to the Main Thread of the Render Process of the browser, the macro task becomes runnable state and can be picked up by the Event Loop to enter the Main Thread for execution.

Here’s another example, by the way, to explain why setTimeout 0 is delayed.

SetTimeout (function() {console.log(' I am a macro task registered by setTimeout ')}, 0)Copy the code

When the setTimeout line is executed, the corresponding macro task is registered, and the Main Thread tells the timer Thread, “Give me a message after 0 milliseconds.” The timer Thread receives the message, waits for 0 milliseconds, and immediately sends a message to the Main Thread saying, “0 milliseconds has passed on my end.” After the Main Thread receives the reply message, it sets the status of the corresponding macro task to Runnable, and the macro task can be picked up by the Event Loop.

It can be seen that after such a process of communication between threads, even if the timer is delayed by 0 ms, the callback is not executed after 0 ms in the real sense, because the communication process takes time. There is an argument on the web that setTimeout 0 is at least 4ms in response time, which is valid, but also conditional.

HTML Living Standard: If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

For this statement, I think I have an idea, different browsers will implement the specification in different details, the specific communication process is not clear, whether it is 4ms, the key is to understand what is going on behind the scenes.

Microtasks are queued

We mentioned earlier that after executing a Task, if the Microtask queue is not empty, all the microtasks in the Microtask queue will be fetched and executed. In my opinion, microTasks are not put into the Microtask queue at registration, because the Event Loop does not determine the status of the Microtask when it processes the Microtask queue. On the other hand, if a Microtask is queued at registration, it will be executed before the Microtask becomes runnable, which is obviously not reasonable. My point is that microTasks enter the Microtask queue only when they become runnable.

So let’s analyze when microTasks become runnable, starting with promises.

var promise1 = new Promise((resolve, reject) = > {
    resolve(1);
})
promise1.then(res= > {
    console.log('PromisE1 microtask executed')})Copy the code

Readers, my first question is, when will Promise’s microtasks be signed up? New Promise? Or when? Take a guess!

The answer is… when the then is executed. (And, of course, the.catch case, just for this example.)

So when does the Promise microtask state become runnable? This is a pity. Yes, the Promise state will be shifted. In this case, the Promise state will be shifted from pending to depressing when resolve(1) is implemented. After resolve(1), the Promise Microtask is queued and will be executed in the Event Loop.

Based on this example, let’s make it harder.

var promise1 = new Promise((resolve, reject) = > {
    setTimeout(() = > {
        resolve(1);
    }, 0);
});
promise1.then(res= > {
    console.log('PromisE1 microtask executed');
});
Copy the code

In this example, the Promise microtask is not registered and enqueued in the same Event Loop. How can I put it? In the first Event Loop, the microtask is registered via.then, but we can see that when new Promise is made, a setTimeout is executed, which is equivalent to registering a macro task. Resolve (1) must be executed when the macro task is executed. Obviously, there is at least one Event Loop between the two.

If you can analyze the Promise microtask process, you should know how to analyze the ObserverMutation microtask process, which is not covered here.

What if a Microtask is created during its execution?

As we know, an Event Loop executes at most one runnable Task, but executes all microtasks in the Microtask queue. If a new Microtask is created while executing the Microtask, will the new Microtask be executed in the next Event Loop? The answer is no. Microtasks can add new microtasks to the queue and complete all of the microtasks before the next task starts and the current Event Loop ends. Be careful not to create microtasks recursively, or you’ll get stuck in an endless loop.

Here is a bad example.

// bad case
function recursionMicrotask() {
	Promise.resolve().then(() = > {
		recursionMicrotask()
	})
}
recursionMicrotask();
Copy the code

Please do not try, otherwise the page will be stuck! (The browser cannot render because the Microtask is holding the Main Thread.)

Why distinguish between Tasks and microtasks?

This is a very important question. Why not just do the browser rendering step after executing the Task, but add the Microtask step to the process? Actually, that was solved in the previous question. An Event Loop consumes only one macro task, while the microtask queue has a “continue on” mechanism when consumed, which gives the developer more imagination and more control over the code.

Do a few problems to warm up?

Before the impact of Promise/A+ specification, might as well use A few exercises to test their understanding of Promise.

Basic operation

function mutationCallback(mutationRecords, observer) {
    console.log('mt1')}const observer = new MutationObserver(mutationCallback)
observer.observe(document.body, { attributes: true })

Promise.resolve().then(() = > {
    console.log('mt2')
    setTimeout(() = > {
        console.log('t1')},0)
    document.body.setAttribute('test'."a")
}).then(() = > {
    console.log('mt3')})setTimeout(() = > {
    console.log('t2')},0)
Copy the code

I’m not going to analyze this, but the answer is mt2, MT1, MT3, T2, T1

Browser does not speak martial arts?

Promise.resolve().then(() = > {
    console.log(0);
    return Promise.resolve(4);
}).then((res) = > {
    console.log(res)
})

Promise.resolve().then(() = > {
    console.log(1);
}).then(() = > {
    console.log(2);
}).then(() = > {
    console.log(3);
}).then(() = > {
    console.log(5);
}).then(() = >{
    console.log(6);
})
Copy the code

This problem is said to be a byte internal outflow of a problem, to tell the truth, I just saw the time is also confused. In my Chrome tests, the answers are pretty regular: 0, 1, 2, 3, 4, 5, 6.

First output 0, then output 1, I can understand, why output 2 and 3 and then suddenly jump to 4, browser you don’t speak wu De ah!

emm… I’m wearing a mask of pain!

So what is the order of execution behind this? Look at it closely, and you’ll see that there are clues.

As usual, the first question, how many microtasks are generated in the execution of the code in this problem? Maybe a lot of people think it’s seven, but it’s actually eight.

Serial number Registration time An asynchronous callback
mt1 .then() console.log(0); return Promise.resolve(4);
mt2 .then(res) console.log(res)
mt3 .then() console.log(1);
mt4 .then() console.log(2);
mt5 .then() console.log(3);
mt6 .then() console.log(5);
mt7 .then() console.log(6);
mt8 return Promise.resolve(4)Implicitly register after execution and the Execution Context stack is cleared An implicit callback (not reflected in the code) to make MT2 runnable
  • The execution Context stack is empty and the states of MT1 and MT3 change to runnable. The JS engine assigns MT1 and MT3 to the Microtask queue (via HostEnqueuePromiseJob).
  • Perform a microtask checkpoint. Mt1 and MT3 Perform a microtask checkpoint. Mt1 and MT3 Perform a microtask checkpoint.
  • The MT1 callback goes into the Execution Context stack,0To return toPromise.resolve(4). Mt1 is out of the queue. This is a big pity. Because MT1 callback will return a fulfilled Promise, then the JS engine will arrange a job (Job is a concept in ECMA, which is equivalent to the concept of micro-task, here it is numbered MT8 first). The callback is to make MT2 become a big pity (The premise is that the current execution context stack is empty). So again, the MT3 callback is executed first.
  • Mt3 is executed in the Execution context stack. The execution context stack is empty and MT4 is executed in a runnable state.
  • Since MT4 is already runnable, the JS engine enqueues MT4, and the JS engine enqueues MT8.
  • The mt4 callback then enters the Execution Context stack, outputs 2, MT5 becomes runnable, and MT4 exits the queue. The JS engine schedules MT5 into the Microtask queue.
  • The mt8 callback is executed to make MT2 runnable and MT8 out of the queue. Mt2 is queued.
  • Mt5 callback executes, output 3, MT6 becomes runnable, mt5 exits the queue. Mt6 into the queue.
  • Mt2 callback executes, outputs 4, mt2 exits the queue.
  • Mt6 callback executes, output 5, MT7 becomes runnable, mt6 exits the queue. Mt7 is queued.
  • Mt7 callback execution, output 6, MT7 out of the queue. Execution complete! In general, the output results are as follows: 0, 1, 2, 3, 4, 5, 6.

If you have any questions about this implementation, please read down on the Promise/A+ specification and ECMAScript262 for more information about promises.

In my tests on the Edge browser, the results are: 0, 1, 2, 4, 3, 5, 6. As you can see, the browsers agree on the main process for implementing promises, but there are some minor differences. In practice, we just need to avoid this problem.

Realize the Promise/A +

Now that we’re warmed up, it’s time to face the big boss: the Promise/A+ specification. The specification enumerates more than 30 rules, large and small, and at first glance it is quite dizzy.

After reading the specification several times, I had A basic understanding that the key to implementing the Promise/A+ specification was to clarify A few core points.

The relationship between the link

Originally, I felt tired after writing thousands of words, so I thought I would quickly finish the last part with words. But in the middle of the last section, I felt THAT I could not continue to write. Pure words were too dry and could not be absorbed, which was quite unfriendly to readers who did not have enough knowledge of Promise. So, I thought I’d start with a graph to describe the Promise link.

First, Promise is an object, and the Promise/A+ specification is built around the prototypical Promise method.then().

  • .then()Is special in that it returns a new Promise instance in this successive call.then()In this case, a Promise chain is strung together, which again has some similarities to the prototype chain. “Shameless” to recommend another”Mind mapping front end” 6K words understanding Javascript objects, prototypes, inheritanceHahaha.
  • Another area of flexibility is that,p1.then(onFulfilled, onRejected)Return a new Promise instance, P2, whose state transition occurred after p1’s state transition (here)afterAfter asynchronous). This is a big pity or Rejected, which depends on the state of P2onFulfilledoronRejected, there is a relatively complex analysis process, namely the Promise Resolution Procedure algorithm described below.

I have drawn a simple sequence diagram here, poorly drawn, just to give the reader a basic impression.

There are many details that have not been mentioned (because there are so many details that it is quite complicated to draw them all, please see the source code attached at the end of this article for details).

nextTick

After reading the previous content, I believe we all have a concept that microtask is an asynchronous task, and we must have the ability to simulate the asynchronous callback of microtask in order to realize the whole asynchronous mechanism of Promise. There is also a message in the specification:

This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.

I choose micro tasks to implement asynchronous callback. If macro tasks are used to implement asynchronous callback, then macro tasks may be interspersed during the execution of the Promise micro task queue, which does not conform to the scheduling logic of the micro task queue. There is also compatibility between the Node environment, where process.nextTick callbacks are used to simulate microtask execution, and the browser environment, where MutationObserver is an option.

function nextTick(callback) {
  if (typeofprocess ! = ='undefined' && typeof process.nextTick === 'function') {
    process.nextTick(callback)
  } else {
    const observer = new MutationObserver(callback)
    const textNode = document.createTextNode('1')
    observer.observe(textNode, {
      characterData: true
    })
    textNode.data = '2'}}Copy the code

State transition

  • There are three states in the Promise instance, namely Pending, depressing, and Rejected. The initial state is Pending.

    const PROMISE_STATES = {
      PENDING: 'pending'.FULFILLED: 'fulfilled'.REJECTED: 'rejected'
    }
    
    class MyPromise {
      constructor(executor) {
        this.state = PROMISE_STATES.PENDING;
      }
      / /... Other code
    }
    Copy the code
  • Once a Promise state has been transferred, it cannot be transferred to another state.

    /** * encapsulates the Promise state transition process *@param {MyPromise} Promise The promise instance where the state transition occurs *@param {*} TargetState Target status *@param {*} This is a big pity. Value Indicates the value of the state transition, which may be fulfilled or the reason of rejected */
    function transition(promise, targetState, value) {
      if(promise.state === PROMISE_STATES.PENDING && targetState ! == PROMISE_STATES.PENDING) {// 2.1: State can only be changed from pending to another state. After the state is changed, the values of state and value do not change
        Object.defineProperty(promise, 'state', {
          configurable: false.writable: false.enumerable: true.value: targetState
        })
        / /... Other code}}Copy the code
  • State transitions are triggered by calls to resolve() or reject(). This is a big pity. When resolve() is called, the current Promise may not immediately become a Fulfilled state, because the value passed into the resolve(value) method may also be a Promise. At this time, the current Promise must track the state of the passed Promise. The entire process of determining the Promise state is implemented through the Promise Resolution Procedure algorithm, and the details are encapsulated in the resolvePromiseWithValue function in the following code. When reject() is called, the state of the current Promise is determined, which must be Rejected. In this case, the transition function (which encapsulates the state transfer details) transfers the Promise state and performs subsequent actions.

    // The resolve execution is a trigger on which to proceed
    function resolve(value) {
      resolvePromiseWithValue(this, value)
    }
    Reject (reject) indicates that the reject state can change to Rejected
    function reject(reason) {
      transition(this, PROMISE_STATES.REJECTED, reason)
    }
    
    class MyPromise {
      constructor(executor) {
        this.state = PROMISE_STATES.PENDING;
        this.fulfillQueue = [];
        this.rejectQueue = [];
        // Call executor immediately after constructing a Promise instance
        executor(resolve.bind(this), reject.bind(this))}}Copy the code

The chain track

So let’s say I have an instance of Promise, let’s call it P1. As promise1. Then (ondepressing, onRejected) will return a new Promise (we call it P2), and at the same time, a microtask MT1 will be registered, and this new P2 will track the state change of its associated P1.

When the state of P1 shifts, the microtask MT1 callback will be executed next. If the state is Fulfilled, onFulfilled will be executed; otherwise, onFulfilled will be executed. The results of microtask MT1 callback execution will be used to determine p2 status. The following is a big pity. Some of the key codes will be Fulfilled, where promise refers to p1 and chainedPromise refers to p2.

// Callbacks should be executed asynchronously, so nextTick is used
nextTick(() = > {
  // Then may be called multiple times, so asynchronous callbacks should be maintained using arrays
  promise.fulfillQueue.forEach(({ handler, chainedPromise }) = > {
    try {
      if (typeof handler === 'function') {
        const adoptedValue = handler(value)
        // The value returned by the asynchronous callback will determine the state of the derived Promise
        resolvePromiseWithValue(chainedPromise, adoptedValue)
      } else {
        // It is possible that then is called, but no call is returned as a parameter, in which case the derived Promise state directly adopts the state of its associated Promise.
        transition(chainedPromise, PROMISE_STATES.FULFILLED, promise.value)
      }
    } catch (error) {
      // If the callback throws an exception, the derived Promise status is changed to Rejected and error is used as reason
      transition(chainedPromise, PROMISE_STATES.REJECTED, error)
    }
  })
  // Finally empties the callback queue for the Promise associationpromise.fulfillQueue = []; })Copy the code

Promise Resolution Procedure algorithm

The Promise Resolution Procedure algorithm is an abstract execution Procedure, which is syntactic in the form of [[Resolve]](Promise, x). It accepts a Promise instance and a value x. To determine the state of this Promise instance. If you look directly at the specification, it will be a little difficult, here directly explain some details in human language.

2.3.1

If a promise and the value X refer to the same object, you should simply set the state of the Promise to Rejected and use a TypeError as a reject reason.

If promise and x refer to the same object, reject promise with a TypeError as the reason.

For example, the boss said that as long as the sales exceed 1 billion this year, the sales will exceed 1 billion. This is obviously a wrong sentence. You can’t make expectation itself a condition. The correct way to play it is that the boss says he will give a bonus of $10 million if his sales exceed $1 billion this year.

Code implementation:

if (promise === x) {
    // 2.3.1 As Promise adopts the mechanism of state, congruence judgment must be made here to prevent the occurrence of an infinite loop
    transition(promise, PROMISE_STATES.REJECTED, new TypeError('promise and x cannot refer to a same object.'))}Copy the code

2.3.2

If x is an instance of a Promise, the Promise should adopt the state of X.

2.3.2 If x is a promise, adopt its state [3.4]:

2.3.2.1 If X is pending, the promise must remain pending until X is depressing or rejected.

Unfortunately, If/when X is fulfilled, fulfill promise with the same value.

2.3.2.3 If/when x is rejected, reject promise with the same reason.

Xiao Wang asked the leader: “Will you give year-end bonuses this year? How much?” “The leader listened to the thought,” THIS matter I also in inquiry before, but have not decided, depends on the boss’s meaning. “, so the leadership said to Xiao Wang: “Will send, but to wait for the news!” .

Notice that at this time, the leader made a promise to Xiao Wang, but the status of the promise P2 is still pending. We need to see the status of the promise P1 given by the boss.

  • Possibility 1: After a few days, the boss said to the leader: “This year’s business can be done well, the year-end bonus will be 10 million yuan”. This is the depressing state of P1, and the value is 10 million. The leader took this, naturally with xiao Wang to honor the promise P2, so he said to Xiao Wang: “the year-end bonus can come down, is 10 million!” . At this time, the state of commitment P2 is fulfilled, and the value is also 10 million yuan. Xiao Wang at this time “villa near the sea”.

  • Possibility 2: after a few days, the boss was a little worried and said to the leader: “This year’s performance is not so good, the year-end bonus will not be distributed, next year, let’s have more points next year.” Xiao Wang, the company is in a special situation this year, we won’t give out the year-end bonus! This p2 also rejected……

Note that there are two big directions here in section 2.3.2 of the Promise A/+ specification, one for the undetermined state of X and one for the determined state of X. In code implementation, there is a trick, for undetermined state, you have to implement it as a subscription, and.then is a great way to do that.

else if (isPromise(x)) {
    // 2.3.2 If x is a Promise instance, its state is traced and adopted
    if(x.state ! == PROMISE_STATES.PENDING) {// Assume that the state of x has been shifted, then adopt its state directly
      transition(promise, x.state, x.state === PROMISE_STATES.FULFILLED ? x.value : x.reason)
    } else {
      // Assuming that the state of X is still pending, then only wait for the state of X to be determined before performing the promise state transition
      // The result of x state transition is uncertain, so we need to subscribe in both cases
      // The subscription action is done neatly with a.then
      x.then(value= > {
        // The x state will be fulfilled. Since the value passed by callback is of an uncertain type, the Promise Resolution Procedure algorithm needs to be continued
        resolvePromiseWithValue(promise, value, thenableValues)
      }, reason= > {
        // The x status changes to Rejected
        transition(promise, PROMISE_STATES.REJECTED, reason)
      })
    }
}
Copy the code

Many details za this article is not an analysis, write write nearly 10,000 words, the end of it, interested readers can directly open the source code to see (look down).

This is the effect drawing of running test cases. You can see that all 872 cases are passed.

The complete code

Here is A direct Javascript implementation of the Promise/A+ specification I wrote for your reference. If I have time later, I will consider a detailed analysis.

  • Github Warehouse: Promises – APlus – Robin
  • The source code
  • Annotated version of source code

defects

The implementation of my version of the Promise/A+ specification does not have the ability to detect an empty execution Context stack, so the details are A bit problematic. Can not match the way above “browser does not speak martial virtue?” The title of the scene.

hack

Later, I thought again and found that there was a way to solve this problem. Two Nextticks could be used to solve the problem. For details, see Promises – aplus-Robin hack.js.

// Use two nextTick hacks to ensure that the Execution Context stack is empty before scheduling microtasks
nextTick(() = > {
    // The first nextTick ensures that the Execution Context stack is empty
    nextTick(() = > {
        // The second nextTick ensures that new tasks are arranged in the form of microtasks
        transition(promise, x.state, x.state === PROMISE_STATES.FULFILLED ? x.value : x.reason)
    })
})
Copy the code

methodology

Whether implementing the Promise/A+ specification by hand or implementing other Native codes, the following points are essentially unavoidable:

  • The ability to understand exactly what Native Code implementations are, just as you understand what feature points a requirement is to implement and prioritize implementation.
  • For each function point or function description, code implementation, priority through the main flow.
  • Design rich enough test cases, regression tests, iterate, ensure scenario coverage, and ultimately build good code.

conclusion

See the end, I believe you are also tired, thank you readers for reading! I hope this article will give you some insight into macro and micro tasks. The Promise/A+ specification is generally opaque and unfriendly to beginners, so I recommend learning more about the Promise/A+ specification after you’ve had some real experience with promises. By learning and understanding the Promise/A+ specification implementation mechanism, you’ll have A better understanding of the Promise internals, which will help you design complex asynchronous processes and, at worst, improve your ability to debug and debug asynchronously.

Here are some more specifications and articles to look at:

  • Promises/A + specification
  • Event Loop Processing Model
  • tasks-microtasks-queues-and-schedules
  • Jobs and Host Operations to Enqueue Jobs

If you think this article is good, please like it and follow it. Thank you for your support. Also welcome to communicate with me directly, I am Laobaife, looking forward to common progress with you!

This article is participating in the “Nuggets 2021 Spring Recruitment Campaign”, click to see the details of the campaign.