preface

I recently read “Analyze the details of Promise implementation, Starting with A Promise Interview question that gave Me sleepless nights,” but still couldn’t figure out why the “order of microtasks in JS Promises” didn’t match the handwritten Promise A+ version.

Therefore, I decided to clarify the definition of Promise in JS from the perspective of ECMA specification, and clarify the principles and conclusions.

If you have limited time, read only chapter 1. The first chapter clearly explains the registration and execution of Promise microtasks in JS by drawing execution and principle summary.

The following chapters record my process of reading ECMA in detail, and solve my doubts through standardization. Readers can also refer to the specification for answers to other questions later on.

Finally, this article also prepared another question, can test the reader’s learning achievement, don’t forget to try ~

Promise the interview questions

What is the output of the following code?

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

The answer

The above code output is: 0 1 2 3 4 5 6.

If you think the output is 0, 1, 2, 4, 3, 5, 6, that’s a classic error. This is what happens when you write Promise A+ by hand in the article “Delving into the details of how A Promise is implemented, starting with the Interview question that gave Me Sleepless nights.”

Named Promise

For the sake of later description, let’s first name all promises generated in the code.

The reason for the classic error

If you just look at the first part of the interview code, how many microtasks did the code generate?

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

The first microtask is to perform the callback to Promise1.then (), which is pseudocode like this.

function job1() {
  const cb = () = > {
    console.log(0)
    return Promise.resolve(4)}const promise3 = cb()

  // Generate the second microtask
  resolvePromise2(promise3)
}
Copy the code

Because the argument to resolvePromise2 is a Promise object, the second microtask is generated.

The second microtask is at the heart of this interview question, and the reason we get the classic mistake is because we ignored it.

The second microtask relates Promise2 to PromisE3, and its pseudocode is as follows.

function job2() {
  // Generate the third microtask
  promise3.then(resolvePromise2, rejectPromise2)
}
Copy the code

This is very depressing. This will be fulfilled gradually, which will be fulfilled gradually. This is very depressing.

The pseudocode for the third microtask is as follows.

function job3() {
  // Generate the fourth microtask
  resolvePromise2(4)}Copy the code

Because PromisE2 is in a fulfilled state and has a then callback function, the fourth microtask will be generated.

The fourth microtask is to call the callback function for promise2.then().

function job4() {
  const cb = res= > {
    console.log(res)
  }

  const result = cb()
  / / the result is undefined
  resolvePromise4(result)
}
Copy the code

Because Promise4 has no then callbacks, no new microtasks are generated.

So if you just look at the first part of the interview code, the code generates four microtasks.

Drawing understanding

In the image below will involve PromiseReactionJob and PromiseResolveThenableJob two nouns, they say the task type, readers can ignore them for the time being.

1. The first round of execution

2. Perform two microtasks

3. Perform two microtasks

4. Perform two microtasks

5. Perform two microtasks

6. Perform a microtask

Summarize and memorize the conclusions

  1. promise.then()Is added to the microtask queue when the promise’s state is not pending. This microtask is called in the specificationPromiseReactionJobThe name Reaction meansthen(onFulfilled, onRejected)The callback parameters onFulfilled and onRejected on the call.
  2. ifpromise1.then()The return value of the callback function is a Promise object (let’s call it Promise2), and a new microtask is generated. The contents of this microtask are callspromise2.then(resolvePromise1, rejectPromise1)To associate Promise1 with Promise2. It’s called in the specificationPromiseResolveThenableJob.

Next, we will read the ECMA specification to clarify the interview questions step by step. You are advised to visit the ECMA website and follow this article to navigate the specification.

1. The first round of execution

Promise.resolve() and promise.then() are called in the first round of code execution. This is gradually fulfilled, which may be fulfilled or pending when promise.then() is called. So we can realize ledpromise.then () and pendingpromise.then ().

Let’s look at the three types of function calls in terms of the ECMA specification.

Promise.resolve(x)

Promise.resolve(x) Refer to the official link.

The third step is to execute PromiseResolve(C, x).

PromiseResolve(C, x)

See the official link.

Since the argument x we passed in is undefined, we only need to look at steps 3 and 4.

The third step generates a new Instance record of PromiseCapability via NewPromiseCapability(C).

The fourth step is to call the resolve method of promiseCapability.

NewPromiseCapability(C)

See the official link.

Within the specification, a promise is associated with its resolve and reject methods using the PromiseCapability type.

This function eventually returns the object as follows.

const promiseCapability = {
  Promise: promise,
  Resolve: resolve,
  Reject: reject,
}
Copy the code

The GetCapabilitiesExecutor Functions algorithm was mentioned in step 4, and I won’t discuss it here. For ease of understanding, the NewPromiseCapability(C) algorithm can be written as the following pseudocode.

// Ignore parameter C and consider C to be a Promise
function NewPromiseCapability() {
  / / the third step
  const promiseCapability = {
    Promise: undefined.Resolve: undefined.Reject: undefined,}// Step 4 and Step 5 create a function
  const executor = (resolve, reject) = > {
    executor.Capability.Resolve = resolve
    executor.Capability.Reject = resolve
  }

  / / step 6
  executor.Capability = promiseCapability

  / / step 7
  const promise = new Promise(executor)

  / / step 10
  promiseCapability.promise = promise

  // Step 11
  return promiseCapability
}
Copy the code

resolve()

In PromiseResolve (C, x). The fourth step performed promiseCapability Resolve (x), and the Resolve method how to define?

From NewPromiseCapability (C) we know promiseCapability. Resolve is Promise constructor calls when Resolve parameters.

Let’s look at the definition of the Promise constructor.

Steps 8 and 9 are what we are looking for, and we continue into the CreateResolvingFunctions(Promise) method.

In the second step, the Promise Resolve function is the Resolve () algorithm we are looking for.

Since we call promise.resolve () with undefined, we go to step 8, and call FulfillPromise(Promise, value).

FulfillPromise(promise, value)

See the official link.

Step 6: set the state to fulfilled.

Step 7, call the TriggerPromiseReactions method. In TriggerPromiseReactions, since the current promise’s reactions are an empty array, undefined is returned directly.

conclusion

Promise. Resolve () returns a Promise which is in the state of fulfilled.

fulfilledPromise.then()

This is very depressing. A. fulfilled b. fulfilled C. fulfilled D. fulfilled

We need to know how the promise.then method is defined.

Promise.prototype.then(onFulfilled, onRejected)

See the official link.

The fourth step creates a new Promise object, which is the return value from the call to THEN ().

The fifth step executes the PerformPromiseThen() method.

PerformPromiseThen(promise, onFulfilled, onRejected)

See the official link.

This is very depressing, because the current state of promise is fulfilled, so we can skip all the other steps and only look at the ninth step.

In step 9.b, create a microtask by calling NewPromiseReactionJob(). In step 9.c, the microtask is added to the microtask queue.

NewPromiseReactionJob(reaction, promise)

See the official link.

In the definition of NewPromiseReactionJob(), we only care about {Job: Job} in the return value. You can think of a job as a callback function that is put into a microtask queue and then taken out of the microtask queue and executed at some future time.

The content of the microtask consists of two parts.

  1. Step F is executedhandler(argument)The handler ispromise.then(cb)The cb.
  2. Execute in step i.iresolve()promise.then()The returned PROMISE object is resolved.

conclusion

A call to.then(cb) with a promise state of fulfilled will generate a microtask. Make sure to resolve the promise that is returned by.then(). Make sure to resolve the promise that is returned by.then().

Write the pseudocode as follows.

const fulfilledPromise = Promise.resolve()
const promise2 = fulfilledPromise.then(onFulfilled)

function job() {
  / / the first f
  const result = onFulfilled()

  The first step i.i / /
  resolvePromise2(result)
}
Copy the code

pendingPromise.then()

Except for Promise1, Promise3, and Promise5, all promisers are generated via.then(), and their state is pending.

In the PerformPromiseThen() definition, find the steps that match the scenario.

conclusion

When a promise with pending state calls the THEN (CB) method, it saves CB to the [[PromiseFulfillReactions]] array of promises.

Result after execution

  1. The status of PROMISe1 and Promise5 is fulfill, so the command is executed.then(cb)The microtask is generated when.
  2. Promise2, 7, 8, 9.then()The callback exists[[PromiseFulfillReactions]]In the array.

2. Perform two microtasks

These two microtasks are generated from fulfillPromise with calls to.then(). Its pseudocode is shown in the job below.

const promise = Promise.resolve()
const promise2 = promise.then(onFulfilled)

function job() {
  const result = onFulfilled()
  resolvePromise2(result)
}
Copy the code

Let’s move on to the definition of the resolve method.

Steps 8 through 12 mean that FulfillPromise() is performed when result.then isn’t called.

Step 13 mean, as a result, then can be invoked, is executed NewPromiseResolveThenableJob generate micro task ().

An object is called a thenable object if its.then property can be called.

The first case is when result is not a thenable object in the resolve(result) call, and the second case is when result is a thenable object.

resolve(nonThenable)

According to steps 8 through 12, the FulfillPromise() method is fulfilled when the value of resolve is not a thenable object.

FulfillPromise()

See the official link.

Step 7 triggers TriggerPromiseReactions(reactions).

The reactions argument is an array of promises [[PromiseFulfillReactions]], and each item in the array is the CB of.then(cb) calls.

TriggerPromiseReactions()

See the official link.

In this method, a microtask is generated for each.then(cb) call.

conclusion

When calling pendingPromise.resolve(nonThenable), the array of pendingPromise.[[PromiseFulfillReactions]] is traversed, A PromiseReactionJob is created for each item.

resolve(thenable)

According to “Promise Resolve Functions provides” step 13, when Resolve value is thenable object, performs NewPromiseResolveThenableJob () to generate a new task.

NewPromiseResolveThenableJob()

See the official link.

Looking only at step B, the pseudocode for the microtask is as follows.

function job() {
  thenable.then(resolveOtherPromise, rejectOtherPromise)
}
Copy the code

Why

Why do you need to execute the thenable.then() method in a microtask?

The official explanation is that thenable.then() is called to ensure that the synchronized code is executed.

I guess the reason for this is that thenable can be implemented for any object the developer implements, so it’s not necessarily a Promise instance. If the call to.then() has side effects (such as console.log), it is more intuitive for the developer to defer the side effects until after synchronization.

Here is the official explanation of the original article.

This Job uses the supplied thenable and its then method to resolve the given promise. This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.

conclusion

When called pendingPromise.resolve(thenable), a microtask is generated whose purpose is to associate thenable with the pendingPromise through thenable’s.then() method.

Result after execution

  1. becausepromise1.then(cb)Callback cb the return value is the Promise of object, so according to the specification generates PromiseResolveThenableJob micro tasks.
  2. becausepromise5.then(cb)The return value of the callback is undefinedresolvePromise6(undefined)Then change the state of Promise6 from pending to fulfill. After Promise6 is fulfilled, the[[PromiseFulfillReactions]]Each item in the list generates a microtask.

3. Perform two microtasks

PromiseResolveThenableJob

The pseudocode in this microtask is:

function job() {
  promise4.then(resolvePromise2, rejectPromise2)
}
Copy the code

Since promise4 is currently in the state of fulfill, promise4.then() generates a microtask based on the results of the first round of execution.

PromiseReactionJob

The content of this microtask is the same as the microtask generated by resolve(nonThenable) and will not be discussed here.

The execution result

4. Perform two microtasks

Both microtasks are PromisereactionJobs.

The execution result

5. Perform two microtasks

Both microtasks are PromisereactionJobs.

Because [[PromiseFulfillReactions]] of PROMISe4 is an empty array, no new microtasks are generated.

The execution result

6. Perform a microtask

The final microtask is also a PromiseReactionJob.

Because [[PromiseFulfillReactions]] of promiseA is an empty array, no new microtasks are generated.

The execution result

Another topic

Let’s do one more problem at the end to see what we’ve learned.

Promise.resolve()
  .then(() = > {
    console.log("a")
    return Promise.resolve().then(() = > {
      console.log("b")
      return "c"
    })
  })
  .then(res= > {
    console.log(res)
  })

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

A 1 B 2 C 4 5

Because the following code generates two microtasks.

.then(() = > {
  console.log("a")
  return Promise.resolve().then(() = > {
    console.log("b")
    return "c"})})Copy the code

The first microtask is the PromiseReactionJob, whose contents are the callback CB of promise.resolve ().then(cb).

The second is PromiseResolveThenableJob micro tasks.


, recruiting

The writer is in Chengdu – Bytedance – private cloud direction, the main technology stack is React + Node.js. Team expansion speed is fast, the technical atmosphere within the group is active. Public cloud private cloud is just starting, there are many technical challenges, the future is foreseeable.

Interested can resume through this link: job.toutiao.com/s/e69g1rQ

You can also add my wechat moonball_cxy, chat together, make a friend.

Original is not easy, don’t forget to encourage oh ❤️