阿里云主机折上折
  • 微信号
Current Site:Index > Single-threaded and event loop

Single-threaded and event loop

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

The Nature of Single-Threadedness

JavaScript is a single-threaded language, meaning it can only execute one task at a time. This design stems from its original role as a browser scripting language, avoiding the complexities introduced by multithreading, such as race conditions and deadlocks. The single-threaded model simplifies development but also presents performance challenges.

console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
console.log('End');
// Output order: Start -> End -> Timeout

This example demonstrates the execution order in a single-threaded environment. Even with setTimeout set to a delay of 0, the callback function will only execute after the current execution stack is cleared. This mechanism is a core manifestation of the event loop.

Call Stack and Task Queue

The call stack is the JavaScript engine's mechanism for tracking the order of function execution. When a function is called, it is pushed onto the call stack; once execution completes, it is popped off. Synchronous code is executed sequentially in the stack.

function first() {
  console.log('First');
  second();
}

function second() {
  console.log('Second');
}

first();
// Call stack changes:
// [first] -> [first, second] -> [first] -> []

Asynchronous operations like setTimeout or fetch place callback functions in the task queue. Task queues are divided into two types:

  • Macro task queue: Includes setTimeout, setInterval, I/O, etc.
  • Micro task queue: Includes Promise.then, MutationObserver, etc.

The Workflow of the Event Loop

The event loop continuously checks the call stack and task queues, processing tasks in a specific order:

  1. Execute all synchronous code in the current call stack.
  2. Check the micro task queue and execute all micro tasks.
  3. Execute one macro task.
  4. Repeat the process.
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
*/

This example clearly illustrates the characteristic where micro tasks take precedence over macro tasks. Promise callbacks, being micro tasks, execute immediately after the current stack is cleared, while setTimeout callbacks, as macro tasks, are processed in the next event loop iteration.

Blocking vs. Non-Blocking I/O

Although JavaScript is single-threaded, it achieves efficient concurrency through non-blocking I/O and the event loop. Web APIs in the browser environment (e.g., fetch, setTimeout) and I/O operations in Node.js are asynchronous and non-blocking.

// Simulate a time-consuming synchronous operation
function syncOperation() {
  const start = Date.now();
  while (Date.now() - start < 3000) {}
  console.log('Sync operation done');
}

// Asynchronous non-blocking operation
function asyncOperation() {
  setTimeout(() => {
    console.log('Async operation done');
  }, 3000);
}

console.log('Start');
syncOperation();  // Blocks for 3 seconds
asyncOperation(); // Does not block
console.log('End');

Priority of Macro Tasks vs. Micro Tasks

Understanding the execution order of macro and micro tasks is crucial for writing efficient code. Micro tasks execute immediately after the current macro task ends, while new macro tasks wait for the next event loop iteration.

// Example 1: Nested tasks
setTimeout(() => {
  console.log('macro 1');
  Promise.resolve().then(() => console.log('micro 1'));
}, 0);

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

/*
Possible output:
macro 1
micro 1
macro 2
micro 2
*/

// Example 2: Generating new micro tasks within a micro task
Promise.resolve().then(() => {
  console.log('micro 1');
  Promise.resolve().then(() => console.log('micro 2'));
});

/*
Output:
micro 1
micro 2
*/

Performance Considerations in Practice

Long-running synchronous code can block the event loop, causing the page to become unresponsive. Web Workers can be used to offload compute-intensive tasks.

// Main thread
const worker = new Worker('worker.js');
worker.postMessage({ data: largeArray });
worker.onmessage = (e) => {
  console.log('Result from worker:', e.data);
};

// worker.js
self.onmessage = (e) => {
  const result = processLargeData(e.data);
  self.postMessage(result);
};

For UI updates, requestAnimationFrame ensures smooth animations:

function animate() {
  // Update UI
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

Evolution of Asynchronous Programming Patterns

From callback hell to modern asynchronous patterns, JavaScript's approach to handling asynchrony has evolved:

  1. Callback functions:
fs.readFile('file.txt', (err, data) => {
  if (err) throw err;
  console.log(data);
});
  1. Promise chains:
fetch('/api/data')
  .then(response => response.json())
  .then(data => processData(data))
  .catch(error => handleError(error));
  1. Async/Await:
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return processData(data);
  } catch (error) {
    handleError(error);
  }
}

Differences in Node.js Event Loop

The Node.js event loop differs slightly from the browser's, with additional phases:

  1. Timers: Execute setTimeout and setInterval callbacks.
  2. Pending callbacks: Execute system operation callbacks.
  3. Idle, prepare: Internal use.
  4. Poll: Retrieve new I/O events.
  5. Check: Execute setImmediate callbacks.
  6. Close callbacks: Execute close event callbacks.
// Node.js-specific example
setImmediate(() => {
  console.log('immediate');
});

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

// Output order may vary depending on context

Common Pitfalls and Misconceptions

  1. Misunderstanding setTimeout(fn, 0) as immediate execution:
// It actually executes as soon as possible but still waits for the current stack to clear
setTimeout(() => console.log('timeout'), 0);
heavyCalculation(); // This delays the timeout output
  1. Closure issues in loops:
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0);
}
// Outputs five 5s instead of 0-4
// Solution: Use let or additional closures
  1. Unhandled Promise rejections:
function riskyOperation() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) reject('Error');
    else resolve('Success');
  });
}

// Always add a catch handler
riskyOperation().catch(console.error);

Debugging and Performance Analysis

Chrome DevTools offers powerful event loop debugging capabilities:

  1. The Performance panel records the entire execution process.
  2. The Console shows unhandled Promise rejections.
  3. The queueMicrotask API directly adds micro tasks.
// Measure code execution time
console.time('operation');
expensiveOperation();
console.timeEnd('operation');

// Track micro tasks
queueMicrotask(() => {
  console.log('Microtask executed');
});

Advanced Use Cases

  1. Implementing a custom scheduler:
class TaskScheduler {
  constructor() {
    this.queue = [];
    this.isProcessing = false;
  }
  
  addTask(task) {
    this.queue.push(task);
    if (!this.isProcessing) this.processQueue();
  }
  
  processQueue() {
    this.isProcessing = true;
    queueMicrotask(() => {
      const task = this.queue.shift();
      if (task) task();
      if (this.queue.length) this.processQueue();
      else this.isProcessing = false;
    });
  }
}
  1. Optimizing batched DOM updates:
function batchDOMUpdates(updates) {
  Promise.resolve().then(() => {
    document.body.style.display = 'none';
    updates.forEach(update => update());
    document.body.style.display = '';
  });
}
  1. Implementing React-like scheduling:
const taskQueue = [];
let isPerformingWork = false;

function scheduleTask(task) {
  taskQueue.push(task);
  if (!isPerformingWork) {
    isPerformingWork = true;
    requestIdleCallback(performWork);
  }
}

function performWork(deadline) {
  while (deadline.timeRemaining() > 0 && taskQueue.length) {
    const task = taskQueue.shift();
    task();
  }
  
  if (taskQueue.length) {
    requestIdleCallback(performWork);
  } else {
    isPerformingWork = false;
  }
}

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

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

上一篇:cookie操作

下一篇:回调函数模式

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 ☕.