您现在的位置是:网站首页 > 单线程与事件循环文章详情

单线程与事件循环

单线程的本质

JavaScript 是单线程语言,这意味着它一次只能执行一个任务。这种设计源于其最初作为浏览器脚本语言的定位,需要与 DOM 操作紧密配合。单线程避免了多线程环境下的竞态条件和锁机制等复杂问题,但也带来了阻塞的风险。

console.log('Start');
for(let i=0; i<1000000000; i++) {} // 长时间循环
console.log('End');
// 在这段代码中,'End'会等待循环完成后才输出

事件循环机制

为了处理异步操作而不阻塞主线程,JavaScript 引入了事件循环模型。这个模型由调用栈、任务队列和微任务队列组成:

  1. 调用栈:存储同步任务的执行上下文
  2. 任务队列(宏任务队列):存放 setTimeout、setInterval、I/O 等异步回调
  3. 微任务队列:存放 Promise.then、MutationObserver 等微任务
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
*/

执行顺序详解

事件循环的具体工作流程可以分为以下几个阶段:

  1. 执行全局同步代码(属于宏任务)
  2. 检查微任务队列并执行所有微任务
  3. 渲染页面(如果需要)
  4. 从宏任务队列取出一个任务执行
  5. 重复上述过程
setTimeout(() => console.log('timeout1'), 0);
setTimeout(() => {
  console.log('timeout2');
  Promise.resolve().then(() => console.log('promise inside timeout'));
}, 0);
Promise.resolve().then(() => console.log('promise1'));
Promise.resolve().then(() => console.log('promise2'));
/*
输出顺序:
promise1
promise2
timeout1
timeout2
promise inside timeout
*/

浏览器与Node.js的差异

虽然都采用事件循环,但浏览器和Node.js的实现存在差异:

浏览器环境

  • 微任务在渲染前执行
  • 每个宏任务后都会检查微任务队列

Node.js环境

  • 分为多个阶段(timers、pending callbacks等)
  • 微任务在阶段切换时执行
  • process.nextTick优先级高于Promise
// Node.js中的示例
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
/*
典型输出顺序:
nextTick
promise
timeout
immediate
*/

常见的异步模式

理解事件循环有助于编写高效的异步代码:

  1. Promise链式调用
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve('data'), 1000);
  });
}

fetchData()
  .then(data => {
    console.log(data);
    return data.toUpperCase();
  })
  .then(modified => {
    console.log(modified);
  });
  1. async/await语法糖
async function process() {
  try {
    const data = await fetchData();
    const result = await processData(data);
    console.log(result);
  } catch (error) {
    console.error(error);
  }
}
  1. 事件发射器模式
const EventEmitter = require('events');
const emitter = new EventEmitter();

emitter.on('data', (data) => {
  console.log('Received:', data);
});

setTimeout(() => {
  emitter.emit('data', 'sample data');
}, 500);

性能优化实践

合理利用事件循环特性可以提升应用性能:

  1. 长任务拆分
// 不佳的做法
function processLargeArray(array) {
  for(let i=0; i<array.length; i++) {
    // 耗时处理
  }
}

// 改进方案
async function processLargeArray(array) {
  const CHUNK_SIZE = 100;
  for(let i=0; i<array.length; i+=CHUNK_SIZE) {
    const chunk = array.slice(i, i+CHUNK_SIZE);
    await new Promise(resolve => 
      requestAnimationFrame(() => {
        processChunk(chunk);
        resolve();
      })
    );
  }
}
  1. 优先使用微任务
// 需要立即执行但不阻塞渲染的操作
function saveState() {
  Promise.resolve().then(() => {
    localStorage.setItem('appState', JSON.stringify(state));
  });
}
  1. 合理使用Web Workers
// 主线程
const worker = new Worker('worker.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

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

常见的陷阱与误区

  1. 误认为setTimeout(fn,0)会立即执行
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// timeout不会最先执行
  1. 忽略微任务的递归风险
function recursivePromise() {
  Promise.resolve().then(() => {
    console.log('微任务执行');
    recursivePromise(); // 会导致无限微任务循环
  });
}
  1. 混淆宏任务与微任务的执行时机
setTimeout(() => console.log('timeout1'), 0);
Promise.resolve().then(() => {
  console.log('promise1');
  setTimeout(() => console.log('timeout2'), 0);
});
/*
promise1
timeout1
timeout2
*/

实际应用场景

  1. 用户输入防抖
let timeoutId;
input.addEventListener('input', () => {
  clearTimeout(timeoutId);
  timeoutId = setTimeout(() => {
    // 实际处理逻辑
  }, 300);
});
  1. 动画队列管理
function animate(element) {
  return new Promise(resolve => {
    element.classList.add('fade-in');
    element.addEventListener('animationend', resolve, {once: true});
  });
}

async function runAnimations() {
  await animate(document.querySelector('.box1'));
  await animate(document.querySelector('.box2'));
}
  1. 数据批量处理
const batchSize = 100;
let currentIndex = 0;

function processBatch() {
  const end = Math.min(currentIndex + batchSize, data.length);
  while(currentIndex < end) {
    processItem(data[currentIndex++]);
  }
  
  if(currentIndex < data.length) {
    setTimeout(processBatch, 0);
  }
}

深入理解任务优先级

浏览器环境中不同任务的优先级:

  1. 用户交互事件(点击、滚动等)具有最高优先级
  2. 渲染相关任务(requestAnimationFrame)
  3. 微任务(Promise回调)
  4. 宏任务(setTimeout、setInterval)
button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('Microtask from click'));
  setTimeout(() => console.log('Timeout from click'), 0);
});

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

// 点击按钮后的输出顺序可能:
// Microtask from click
// Regular timeout
// Timeout from click

Node.js中的特殊案例

Node.js的事件循环包含更多阶段:

  1. timers:执行setTimeout和setInterval回调
  2. pending callbacks:执行系统操作回调
  3. idle/prepare:内部使用
  4. poll:检索新的I/O事件
  5. check:执行setImmediate回调
  6. close callbacks:执行关闭事件回调
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);

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

调试技巧

  1. 使用performance.mark跟踪任务
performance.mark('start-task');
// 执行一些操作
performance.mark('end-task');
performance.measure('task-duration', 'start-task', 'end-task');
console.log(performance.getEntriesByName('task-duration'));
  1. 监控事件循环延迟
let last = Date.now();
function monitor() {
  const now = Date.now();
  const delay = now - last - 1000; // 预期1秒间隔
  console.log('Event loop delay:', delay);
  last = now;
  setTimeout(monitor, 1000);
}
monitor();
  1. 使用Chrome DevTools的Performance面板
  • 记录运行时性能
  • 分析任务分布和长任务
  • 查看主线程活动

上一篇: cookie操作

下一篇: 回调函数模式

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步