阿里云主机折上折
  • 微信号
Current Site:Index > Macro tasks and micro tasks

Macro tasks and micro tasks

Author:Chuan Chen 阅读数:14571人阅读 分类: Node.js

Concepts of Macro-tasks and Micro-tasks

Macro-tasks and micro-tasks are two types of task queues in JavaScript that determine the order of code execution. Macro-tasks include the entire script code, setTimeout, setInterval, I/O operations, UI rendering, etc. Micro-tasks include Promise.then, process.nextTick, MutationObserver, etc. Understanding their differences is crucial for mastering asynchronous programming in JavaScript.

Execution Order in the Event Loop

The JavaScript engine follows a specific order when executing code:

  1. Execute a macro-task (usually the entire script code)
  2. During execution, if micro-tasks are encountered, add them to the micro-task queue
  3. After the macro-task completes, immediately execute all micro-tasks
  4. Perform UI rendering (in browser environments)
  5. Start the next macro-task
console.log('script start'); // Macro-task

setTimeout(() => {
  console.log('setTimeout'); // Macro-task
}, 0);

Promise.resolve().then(() => {
  console.log('promise1'); // Micro-task
}).then(() => {
  console.log('promise2'); // Micro-task
});

console.log('script end'); // Macro-task

// Output order:
// script start
// script end
// promise1
// promise2
// setTimeout

process.nextTick in Node.js

In Node.js, micro-tasks created by process.nextTick have higher priority than those created by Promise.then:

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

// Output order:
// nextTick
// promise

Nesting of Macro-tasks and Micro-tasks

When new micro-tasks are generated within a micro-task, they continue to execute until the micro-task queue is empty:

function microtaskLoop() {
  Promise.resolve().then(() => {
    console.log('microtask');
    microtaskLoop(); // Infinite loop
  });
}

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

// Will continuously output 'microtask' and never execute 'timeout'

Differences Between Browsers and Node.js

The implementation of macro-tasks and micro-tasks varies across environments:

  1. Browser Environment:

    • Macro-tasks: setTimeout, setInterval, requestAnimationFrame, I/O, UI rendering
    • Micro-tasks: Promise.then, MutationObserver
  2. Node.js Environment:

    • Macro-tasks: setTimeout, setInterval, setImmediate, I/O
    • Micro-tasks: process.nextTick, Promise.then
// Execution order example in Node.js
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

// Possible output order:
// nextTick
// promise
// timeout
// immediate

Practical Application Scenarios

  1. Batch DOM Updates:
// Use micro-tasks to batch process DOM updates
function batchUpdate() {
  let pending = false;
  const callbacks = [];
  
  return function(callback) {
    callbacks.push(callback);
    if (!pending) {
      pending = true;
      Promise.resolve().then(() => {
        const copies = callbacks.slice();
        callbacks.length = 0;
        pending = false;
        copies.forEach(cb => cb());
      });
    }
  };
}

const update = batchUpdate();
update(() => console.log('Update 1'));
update(() => console.log('Update 2'));
  1. Priority Control:
// Ensure critical tasks are executed first
function criticalTask() {
  process.nextTick(() => {
    console.log('Critical task');
  });
}

function normalTask() {
  Promise.resolve().then(() => {
    console.log('Normal task');
  });
}

criticalTask();
normalTask();
// Output:
// Critical task
// Normal task

Performance Considerations

  1. Micro-task Accumulation: Too many micro-tasks can block the main thread
  2. Task Splitting: Long-running tasks should be split into multiple macro-tasks
  3. Priority Selection: Choose the appropriate task type based on the scenario
// Bad practice - Infinite micro-task loop
function badPractice() {
  Promise.resolve().then(badPractice);
}

// Good practice - Split using macro-tasks
function goodPractice() {
  // Process part of the work
  if (moreWork) {
    setTimeout(goodPractice, 0);
  }
}

Common Misconceptions

  1. Assuming setTimeout(fn, 0) executes immediately: It actually just adds the task to the macro-task queue as soon as possible
  2. Ignoring micro-task priority: Micro-tasks are executed entirely before the next macro-task
  3. Confusing implementations between Node.js and browsers: Especially the behavior of process.nextTick and setImmediate
// Misconception example
console.log('start');

setTimeout(() => {
  console.log('timeout 1');
  Promise.resolve().then(() => console.log('promise 1'));
}, 0);

setTimeout(() => {
  console.log('timeout 2');
  Promise.resolve().then(() => console.log('promise 2'));
}, 0);

console.log('end');

// Actual output:
// start
// end
// timeout 1
// promise 1
// timeout 2
// promise 2

Advanced Application Patterns

  1. Task Scheduler:
class TaskScheduler {
  constructor() {
    this.macroTasks = [];
    this.microTasks = [];
    this.isProcessing = false;
  }

  addMacroTask(task) {
    this.macroTasks.push(task);
    this.schedule();
  }

  addMicroTask(task) {
    this.microTasks.push(task);
    this.schedule();
  }

  schedule() {
    if (this.isProcessing) return;
    
    this.isProcessing = true;
    Promise.resolve().then(() => {
      // Execute all micro-tasks first
      while (this.microTasks.length) {
        const task = this.microTasks.shift();
        task();
      }
      
      // Then execute one macro-task
      if (this.macroTasks.length) {
        const task = this.macroTasks.shift();
        task();
      }
      
      this.isProcessing = false;
      if (this.microTasks.length || this.macroTasks.length) {
        this.schedule();
      }
    });
  }
}
  1. Asynchronous Queue Control:
async function processTasks(tasks, concurrency = 4) {
  const results = [];
  let index = 0;
  
  async function runNext() {
    if (index >= tasks.length) return;
    const currentIndex = index++;
    const task = tasks[currentIndex];
    results[currentIndex] = await task();
    await Promise.resolve(); // Micro-task checkpoint
    await runNext();
  }
  
  const workers = Array(concurrency).fill().map(runNext);
  await Promise.all(workers);
  return results;
}

Debugging Tips

  1. Use console.log to track execution order
  2. Leverage the Performance panel in Chrome DevTools
  3. Use the --trace-event-categories parameter in Node.js
// Debugging example
function debugFlow() {
  console.log('Synchronous code starts');
  
  setTimeout(() => {
    console.log('Macro-task 1');
    Promise.resolve().then(() => console.log('Micro-task from macro-task 1'));
  }, 0);
  
  Promise.resolve().then(() => {
    console.log('Micro-task 1');
    setTimeout(() => console.log('Macro-task from micro-task 1'), 0);
  });
  
  console.log('Synchronous code ends');
}

debugFlow();

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

如果侵犯了你的权益请来信告知我们删除。邮箱: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 ☕.