阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 单线程与事件循环

单线程与事件循环

作者:陈川 阅读数:2553人阅读 分类: JavaScript

单线程的本质

JavaScript 是单线程语言,这意味着它一次只能执行一个任务。这种设计源于其最初作为浏览器脚本语言的定位,避免多线程带来的复杂性,比如竞态条件和死锁问题。单线程模型简化了开发,但同时也带来了性能挑战。

console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
console.log('End');
// 输出顺序:Start -> End -> Timeout

这个例子展示了单线程的执行顺序。即使setTimeout的延迟设为0,回调函数仍然会在当前执行栈清空后才会执行。这种机制就是事件循环的核心表现。

调用栈与任务队列

调用栈是 JavaScript 引擎追踪函数执行顺序的机制。当一个函数被调用时,它会被推入调用栈;执行完毕后,从栈中弹出。同步代码按照顺序依次入栈执行。

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

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

first();
// 调用栈变化:
// [first] -> [first, second] -> [first] -> []

异步操作如setTimeoutfetch会将回调函数放入任务队列。任务队列分为两种:

  • 宏任务队列:包含setTimeoutsetInterval、I/O等
  • 微任务队列:包含Promise.thenMutationObserver

事件循环的工作流程

事件循环持续检查调用栈和任务队列,按照特定顺序处理任务:

  1. 执行当前调用栈中的所有同步代码
  2. 检查微任务队列并执行所有微任务
  3. 执行一个宏任务
  4. 重复上述过程
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');

/*
输出顺序:
Script start
Script end
Promise 1
Promise 2
setTimeout
*/

这个例子清晰展示了微任务优先于宏任务执行的特性。Promise回调属于微任务,会在当前执行栈清空后立即执行,而setTimeout回调作为宏任务会在下一轮事件循环中处理。

阻塞与非阻塞I/O

虽然JavaScript是单线程,但通过非阻塞I/O和事件循环实现了高效并发。浏览器环境中的Web API(如fetchsetTimeout)和Node.js中的I/O操作都是异步非阻塞的。

// 模拟耗时同步操作
function syncOperation() {
  const start = Date.now();
  while (Date.now() - start < 3000) {}
  console.log('Sync operation done');
}

// 异步非阻塞操作
function asyncOperation() {
  setTimeout(() => {
    console.log('Async operation done');
  }, 3000);
}

console.log('Start');
syncOperation();  // 阻塞3秒
asyncOperation(); // 不阻塞
console.log('End');

宏任务与微任务的优先级

理解宏任务和微任务的执行顺序对编写高效代码至关重要。微任务会在当前宏任务结束后立即执行,而新的宏任务要等到下一次事件循环。

// 示例1:嵌套任务
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);

/*
可能输出:
macro 1
micro 1
macro 2
micro 2
*/

// 示例2:微任务中产生新微任务
Promise.resolve().then(() => {
  console.log('micro 1');
  Promise.resolve().then(() => console.log('micro 2'));
});

/*
输出:
micro 1
micro 2
*/

实际应用中的性能考量

长时间运行的同步代码会阻塞事件循环,导致页面无响应。合理使用Web Worker可以将计算密集型任务分流。

// 主线程
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);
};

对于UI更新,可以使用requestAnimationFrame来确保动画流畅:

function animate() {
  // 更新UI
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

异步编程模式演进

从回调地狱到现代异步模式,JavaScript的异步处理方式不断进化:

  1. 回调函数:
fs.readFile('file.txt', (err, data) => {
  if (err) throw err;
  console.log(data);
});
  1. Promise链:
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);
  }
}

Node.js中的事件循环差异

Node.js的事件循环与浏览器略有不同,包含更多阶段:

  1. timers:执行setTimeoutsetInterval回调
  2. pending callbacks:执行系统操作的回调
  3. idle, prepare:内部使用
  4. poll:检索新的I/O事件
  5. check:执行setImmediate回调
  6. close callbacks:执行关闭事件的回调
// Node.js特定示例
setImmediate(() => {
  console.log('immediate');
});

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

// 输出顺序可能不同,取决于上下文

常见误区与陷阱

  1. 误认为setTimeout(fn, 0)会立即执行:
// 实际上只是尽快执行,但仍需等待当前栈清空
setTimeout(() => console.log('timeout'), 0);
heavyCalculation(); // 这会延迟timeout输出
  1. 在循环中创建闭包的问题:
for (var i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出五个5,而不是0-4
// 解决方案:使用let或额外闭包
  1. 未处理的Promise拒绝:
function riskyOperation() {
  return new Promise((resolve, reject) => {
    if (Math.random() > 0.5) reject('Error');
    else resolve('Success');
  });
}

// 应该总是添加catch处理
riskyOperation().catch(console.error);

调试与性能分析

Chrome DevTools提供了强大的事件循环调试能力:

  1. Performance面板可以记录完整的执行过程
  2. Console中查看未处理的Promise拒绝
  3. 使用queueMicrotaskAPI直接添加微任务
// 测量代码执行时间
console.time('operation');
expensiveOperation();
console.timeEnd('operation');

// 跟踪微任务
queueMicrotask(() => {
  console.log('Microtask executed');
});

高级应用场景

  1. 实现自定义调度器:
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. 批量DOM更新优化:
function batchDOMUpdates(updates) {
  Promise.resolve().then(() => {
    document.body.style.display = 'none';
    updates.forEach(update => update());
    document.body.style.display = '';
  });
}
  1. 实现类似React的调度机制:
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操作

下一篇:回调函数模式

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌