阿里云主机折上折
  • 微信号
Current Site:Index > The microtask queue feature of Promise

The microtask queue feature of Promise

Author:Chuan Chen 阅读数:22958人阅读 分类: JavaScript

The Relationship Between Promises and the Microtask Queue

The Promise object introduced in ECMAScript 6 is closely related to JavaScript's event loop mechanism, particularly in that its callbacks are placed in the microtask queue rather than the macrotask queue. This design ensures that Promise callbacks are executed earlier than macrotasks like setTimeout, even if they are created within the same event loop.

console.log('Script start');

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

Promise.resolve().then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

console.log('Script end');

// Output order:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout

Execution Mechanism of the Microtask Queue

The microtask queue is executed immediately after the current macrotask completes and before rendering. This means microtasks can block rendering because they are all executed before the browser has a chance to paint a new frame. Each microtask execution may generate new microtasks, and the engine will continue processing the microtask queue until it is empty.

function recursiveMicrotask(count = 0) {
  if (count >= 3) return;
  
  Promise.resolve().then(() => {
    console.log(`Microtask ${count}`);
    recursiveMicrotask(count + 1);
  });
}

recursiveMicrotask();

setTimeout(() => console.log('Macrotask'), 0);

// Output order:
// Microtask 0
// Microtask 1
// Microtask 2
// Macrotask

Promise Chains and Microtask Generation

Each then/catch/finally method of a Promise returns a new Promise, and the callbacks of these methods are scheduled as microtasks. When a Promise's state changes, all related callbacks are batched into the microtask queue.

const promise = new Promise((resolve) => {
  console.log('Executor');
  resolve('Initial');
});

promise
  .then((value) => {
    console.log('Then 1:', value);
    return 'Modified';
  })
  .then((value) => {
    console.log('Then 2:', value);
    throw new Error('Failure');
  })
  .catch((error) => {
    console.log('Catch:', error.message);
    return 'Recovered';
  })
  .finally(() => {
    console.log('Finally');
  });

console.log('Sync code');

// Output order:
// Executor
// Sync code
// Then 1: Initial
// Then 2: Modified
// Catch: Failure
// Finally

Interaction with async/await

Async functions are essentially based on Promises. The await expression pauses function execution and wraps the remaining code as a microtask. This makes the execution order of async/await code similar to Promise chains but with more intuitive syntax.

async function asyncFunc() {
  console.log('Async start');
  await Promise.resolve();
  console.log('After await');
}

console.log('Script start');
asyncFunc();
console.log('Script end');

// Output order:
// Script start
// Async start
// Script end
// After await

Microtask Applications in Browser APIs

Many modern browser APIs, such as MutationObserver and queueMicrotask, utilize the microtask queue. Understanding the microtask nature of Promises helps in correctly handling interactions with these APIs.

// Using queueMicrotask to directly schedule a microtask
queueMicrotask(() => {
  console.log('Microtask via queueMicrotask');
});

Promise.resolve().then(() => {
  console.log('Microtask via Promise');
});

// Output order:
// Microtask via Promise
// Microtask via queueMicrotask

Performance Considerations and Potential Issues

Excessive use of microtasks can lead to prolonged blocking of the main thread, affecting page responsiveness. Especially when processing large amounts of data, consider chunking tasks or using requestIdleCallback.

function processLargeData() {
  const data = Array(10000).fill().map((_, i) => i);
  
  // Bad practice: processing all data at once
  // Promise.resolve().then(() => {
  //   data.forEach(heavyProcessing);
  // });
  
  // Better approach: chunked processing
  function processChunk(start) {
    if (start >= data.length) return;
    
    const end = Math.min(start + 100, data.length);
    queueMicrotask(() => {
      for (let i = start; i < end; i++) {
        heavyProcessing(data[i]);
      }
      processChunk(end);
    });
  }
  
  processChunk(0);
}

function heavyProcessing(item) {
  // Simulate time-consuming operation
  for (let i = 0; i < 100000; i++);
}

Comparison with Other Asynchronous Patterns

Compared to scheduling methods like setImmediate, setTimeout(0), and requestAnimationFrame, Promise microtasks provide higher-priority execution timing, making them suitable for operations that need to execute as soon as possible without blocking UI updates.

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve().then(() => console.log('Promise'));

requestAnimationFrame(() => console.log('rAF'));

queueMicrotask(() => console.log('queueMicrotask'));

// Typical output order:
// Promise
// queueMicrotask
// rAF
// setTimeout

Differences in the Node.js Environment

In Node.js, process.nextTick has even higher priority than Promise microtasks. Additionally, Node.js has its own implementation of the microtask queue, which differs slightly from the browser environment.

process.nextTick(() => console.log('nextTick 1'));
Promise.resolve().then(() => console.log('Promise 1'));
process.nextTick(() => console.log('nextTick 2'));
Promise.resolve().then(() => console.log('Promise 2'));

// Node.js output order:
// nextTick 1
// nextTick 2
// Promise 1
// Promise 2

Analysis of Practical Use Cases

Leveraging the microtask nature of Promises enables efficient batch DOM operations, update scheduling in state management libraries, and more. Frameworks like Vue.js utilize this feature to optimize change detection.

// Simulating batch updates in a state management library
let isUpdating = false;
let pendingStates = [];

function setState(newState) {
  pendingStates.push(newState);
  
  if (!isUpdating) {
    isUpdating = true;
    Promise.resolve().then(() => {
      const states = [...pendingStates];
      pendingStates = [];
      isUpdating = false;
      applyStates(states);
    });
  }
}

function applyStates(states) {
  console.log('Applying states:', states);
  // In practice, this would merge states and update components
}

setState({ count: 1 });
setState({ count: 2 });
console.log('Sync code');

// Output order:
// Sync code
// Applying states: [ { count: 1 }, { count: 2 } ]

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.