最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - What is the order of microtasks in multiple Promise chains? - Stack Overflow

programmeradmin5浏览0评论

Out of an academic interest I am trying to understand the order of microtasks in case of multiple Promise chains.

I. Two Promise chains

These execute in a predictable "zipped" way:

Promise.resolve()
  .then(log('a1'))
  .then(log('a2'))
  .then(log('a3'));

Promise.resolve()
  .then(log('b1'))
  .then(log('b2'))
  .then(log('b3'));
// a1 b1 ; a2 b2 ; a3 b3
// Here and in other output listings
// manually inserted semicolons ";" help illustrate my understanding.

II. The first "a1" returns a Promise:

Promise.resolve()
  .then(() => {
    log('a1')();
    return Promise.resolve();
  })
  .then(log('a2'))
  .then(log('a3'));

Promise.resolve()
  .then(log('b1'))
  .then(log('b2'))
  .then(log('b3'));
// a1 b1 ; b2 ; b3 a2 ; a3

As I understand, a new Promise returned from a then() has introduced a single extra microtask to resolve that Promise; which has also shifted the "A" then() to the end of the "loop". This allowed both "b2" and "b3" to overtake.

III. Three Promise chains

Here's the working code to try running, along with the helper functions:

const output = [];

const makeChain = (key, n = 5, trueStep = false) => {
  let p = Promise.resolve();
  const arrow = isArrow => isArrow ? '->' : '';
  for (let i = 1; i <= n; i++) {
    const returnPromise = trueStep === i;
    const afterPromise = trueStep === i - 1;
    p = p.then(() => {
      output.push(`${arrow(afterPromise)}${key}${i}${arrow(returnPromise)}`);
      if (returnPromise) return Promise.resolve();      
    });
  }
  return p.catch(console.error);
};

// ----- cut line for this and next tests -----

Promise
  .all([
    makeChain('a', 3),
    makeChain('b', 3),
    makeChain('c', 3),
  ])
  .then(() => console.log(output.join(' ')));

// a1 b1 c1 ; a2 b2 c2 ; a3 b3 c3

So far so good: a "zip" again.

IV. Return Promise at "a1" step

(skipping the helper funciton)

Promise
  .all([
    makeChain('a', 3, 1),
    makeChain('b', 3),
    makeChain('c', 3),
  ])
  .then(() => console.log(output.join(' ')));
// a1-> b1 c1 ; b2 c2 ; b3 c3 ->a2 ; a3

Looks like the same behavior: "A1" introduced a single extra microtask + jumped to the tail of the "loop".

V. "c4" also returns a Promise:

Promise
  .all([
    makeChain('a', 7, 1),
    makeChain('b', 7),
    makeChain('c', 7, 4),
  ])
  .then(() => console.log(output.join(' ')));

// a1-> b1 c1 ; b2 c2 ; b3 c3 ->a2 ; b4 c4-> a3 ; b5 a4 ; b6 a5 b7 ->c5 ; a6 c6 ; a7 c7

Two questions

  1. In the last test I cannot explain the order after "dispatching" the "c4": why did the "b7" overtook the "c5" then()?

  2. Where can I read about the rules behind the order of the microtasks in the PromiseJobs queue?

Out of an academic interest I am trying to understand the order of microtasks in case of multiple Promise chains.

I. Two Promise chains

These execute in a predictable "zipped" way:

Promise.resolve()
  .then(log('a1'))
  .then(log('a2'))
  .then(log('a3'));

Promise.resolve()
  .then(log('b1'))
  .then(log('b2'))
  .then(log('b3'));
// a1 b1 ; a2 b2 ; a3 b3
// Here and in other output listings
// manually inserted semicolons ";" help illustrate my understanding.

II. The first "a1" returns a Promise:

Promise.resolve()
  .then(() => {
    log('a1')();
    return Promise.resolve();
  })
  .then(log('a2'))
  .then(log('a3'));

Promise.resolve()
  .then(log('b1'))
  .then(log('b2'))
  .then(log('b3'));
// a1 b1 ; b2 ; b3 a2 ; a3

As I understand, a new Promise returned from a then() has introduced a single extra microtask to resolve that Promise; which has also shifted the "A" then() to the end of the "loop". This allowed both "b2" and "b3" to overtake.

III. Three Promise chains

Here's the working code to try running, along with the helper functions:

const output = [];

const makeChain = (key, n = 5, trueStep = false) => {
  let p = Promise.resolve();
  const arrow = isArrow => isArrow ? '->' : '';
  for (let i = 1; i <= n; i++) {
    const returnPromise = trueStep === i;
    const afterPromise = trueStep === i - 1;
    p = p.then(() => {
      output.push(`${arrow(afterPromise)}${key}${i}${arrow(returnPromise)}`);
      if (returnPromise) return Promise.resolve();      
    });
  }
  return p.catch(console.error);
};

// ----- cut line for this and next tests -----

Promise
  .all([
    makeChain('a', 3),
    makeChain('b', 3),
    makeChain('c', 3),
  ])
  .then(() => console.log(output.join(' ')));

// a1 b1 c1 ; a2 b2 c2 ; a3 b3 c3

So far so good: a "zip" again.

IV. Return Promise at "a1" step

(skipping the helper funciton)

Promise
  .all([
    makeChain('a', 3, 1),
    makeChain('b', 3),
    makeChain('c', 3),
  ])
  .then(() => console.log(output.join(' ')));
// a1-> b1 c1 ; b2 c2 ; b3 c3 ->a2 ; a3

Looks like the same behavior: "A1" introduced a single extra microtask + jumped to the tail of the "loop".

V. "c4" also returns a Promise:

Promise
  .all([
    makeChain('a', 7, 1),
    makeChain('b', 7),
    makeChain('c', 7, 4),
  ])
  .then(() => console.log(output.join(' ')));

// a1-> b1 c1 ; b2 c2 ; b3 c3 ->a2 ; b4 c4-> a3 ; b5 a4 ; b6 a5 b7 ->c5 ; a6 c6 ; a7 c7

Two questions

  1. In the last test I cannot explain the order after "dispatching" the "c4": why did the "b7" overtook the "c5" then()?

  2. Where can I read about the rules behind the order of the microtasks in the PromiseJobs queue?

Share Improve this question asked Mar 21 at 19:22 SergeSerge 1,6132 gold badges24 silver badges47 bronze badges 4
  • "which has also shifted the "A" then() to the end of the "loop"." - not necessarily, if there's other microtasks being queued they may as well come behind that. Add some more to your "b" chain. – Bergi Commented Mar 21 at 19:35
  • 1 "Where can I read about the rules behind the order of the microtasks in the PromiseJobs queue?" - if you really have an academic interest, read the specification - which you already seem to know as you're using the spec term "PromiseJob". – Bergi Commented Mar 21 at 19:37
  • 1 I think you're confusing yourself with those semicolons and "loops" or "turns". There is only one FIFO queue, being processed until it's empty, that's it. – Bergi Commented Mar 21 at 19:41
  • Thank you @Bergi for the comments. I learned the PromiseJobs name from learn.javascript article on Microtasks javascript.info/microtask-queue#microtasks-queue and have only briefly skipped through the ECMAScript specs referred from there and from your suggestion. Specs are quite hard to read and didn't shine any light yet to me :-) I keep in mind the FIFO nature of the queue. Still trying to understand the order in which the jobs are getting there. As it is not obvious from the code. I also noted to myself, that the code should not ever rely on the order of microtasks. – Serge Commented Mar 21 at 19:49
Add a comment  | 

1 Answer 1

Reset to default 2

In principle, the order of Promise resolution is guaranteed in the specification (unlike other Jobs whose execution order is implementation defined):

9.5.5 HostEnqueuePromiseJob

[...] Jobs must run in the same order as the HostEnqueuePromiseJob invocations that scheduled them.

The key idea to answer your question is that there is not one job involved in resolving a promise, there are two:

  • The NewPromiseReactionJob - the usual job to run the promise handlers
  • The NewPromiseResolveThenableJob - a special job where I am not sure why it exists, but it causes the difference you observe

To see where the difference comes from, lets simplify the example to:

const settled = Promise.resolve();
settled.then(() => console.log("settled"));

Promise.resolve()
    .then(() => console.log("a1"))
    .then(() => console.log("a2"))
    .then(() => console.log("a3"))
    .then(() => console.log("a4"))
    .then(() => console.log("a5"))

Promise.resolve()
    .then(() => { console.log("b1"); return settled; })
    .then(() => console.log("b2"));

The key difference is that in one case, .then returns a Promise which adds three sequential jobs to the queue, whereas not returning a Promise from .then only schedules one job.

27.2.1.3.2 Promise Resolve Functions

8. If resolution is not an Object, then
   a. Perform FulfillPromise(promise, resolution). [<- One Job]
   b. Return undefined.

9.  Let then be Completion(Get(resolution, "then")).
11. Let thenAction be then.[[Value]].
13. Let thenJobCallback be HostMakeJobCallback(thenAction).
14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). [<- First Job of three]

The three sequential jobs are:

  • The NewPromiseResolveThenableJob, which calls .then(...) on the returned object (on the settled promise) - As the Promise is already settled, this directly schedules a PromiseReactionJob for the resolution of settled
  • The PromiseReactionJob of settled, which resolves the Promise returned by .then(...), which in turn schedules new PromiseReactionJobs
  • The PromiseReactionJob that prints b2

One easy way to look behind the curtain and see the ResolveThenableJob is by returning a Thenable (an object with a .then method) instead of a Promise:

const prom = Promise.resolve();
prom.then(() => { 
    console.log("promise reaction job 1")
    return { then(res) { console.log("then job"); res(); } };
}).then(() => console.log("promise reaction job 3"));
prom.then(() => console.log("promise reaction job 2"));

The existence of the concept of a Thenable is probably why this job exists in the first place.


For what it is worth, this three-job frenzy also existed for await, then it was optimized away in V8 and afterwards the spec was changed to make this optimization mandatory. So maybe nobody cared to optimize away the three jobs when returning a Promise from within .then(...).

发布评论

评论列表(0)

  1. 暂无评论