未找到匹配的笔记

1.29 - Event Loop、宏任务与微任务

JavaScript面试

JavaScript Event Loop、宏任务与微任务深度解析

一、为什么需要 Event Loop?

1.1 JavaScript 的单线程特性

JavaScript 是单线程语言,这意味着同一时间只能执行一个任务。但这带来了一个问题:

// 如果没有异步机制,这段代码会阻塞后续所有代码
const data = fetch('https://api.example.com/data'); // 假设需要3秒
console.log('这行代码要等3秒才能执行');

为了解决这个问题,JavaScript 引入了异步机制Event Loop(事件循环)。

1.2 运行时环境的组成

JavaScript 运行时由以下几个部分组成:

  • 调用栈(Call Stack): 执行同步代码
  • Web APIs: 浏览器提供的异步API(setTimeout、DOM事件、fetch等)
  • 任务队列(Task Queue): 存放待执行的回调函数
  • 微任务队列(Microtask Queue): 存放优先级更高的回调
  • Event Loop: 协调者,负责调度执行

二、Event Loop 运行机制

2.1 核心执行流程

1. 执行同步代码(调用栈中的代码)
2. 调用栈清空后,检查微任务队列
3. 执行所有微任务(直到微任务队列清空)
4. 执行一个宏任务
5. 再次检查微任务队列,执行所有微任务
6. 渲染更新(如果需要)
7. 回到步骤4,循环往复

2.2 图解 Event Loop

┌───────────────────────────┐
│     Call Stack (调用栈)    │
│   执行同步代码              │
└───────────┬───────────────┘


┌───────────────────────────┐
│   Microtask Queue         │
│   (微任务队列)              │
│   - Promise.then          │
│   - MutationObserver      │
│   - queueMicrotask        │
└───────────┬───────────────┘


┌───────────────────────────┐
│   Macrotask Queue         │
│   (宏任务队列)              │
│   - setTimeout            │
│   - setInterval           │
│   - setImmediate(Node)    │
│   - I/O                   │
│   - UI rendering          │
└───────────────────────────┘

三、宏任务(Macrotask)

3.1 什么是宏任务?

宏任务是由宿主环境(浏览器/Node.js)发起的任务,每次 Event Loop 只执行一个宏任务。

3.2 常见的宏任务

// 1. setTimeout
setTimeout(() => {
  console.log('宏任务: setTimeout');
}, 0);

// 2. setInterval
const intervalId = setInterval(() => {
  console.log('宏任务: setInterval');
}, 1000);

// 3. setImmediate (Node.js 环境)
setImmediate(() => {
  console.log('宏任务: setImmediate');
});

// 4. I/O 操作
fs.readFile('./file.txt', () => {
  console.log('宏任务: 文件读取完成');
});

// 5. UI rendering (浏览器环境)
requestAnimationFrame(() => {
  console.log('宏任务: 页面渲染');
});

// 6. MessageChannel
const channel = new MessageChannel();
channel.port1.onmessage = () => {
  console.log('宏任务: MessageChannel');
};
channel.port2.postMessage('');

3.3 宏任务的特点

  • 每次 Event Loop 只取出一个宏任务执行
  • 执行完一个宏任务后,会清空所有微任务
  • setTimeout(fn, 0) 并不是立即执行,而是在下一轮 Event Loop 中执行

四、微任务(Microtask)

4.1 什么是微任务?

微任务是由 JavaScript 引擎发起的任务,在当前宏任务执行完后立即执行,会清空整个微任务队列。

4.2 常见的微任务

// 1. Promise.then/catch/finally
Promise.resolve().then(() => {
  console.log('微任务: Promise.then');
});

// 2. async/await (本质是 Promise)
async function asyncFunc() {
  await Promise.resolve();
  console.log('微任务: async/await');
}
asyncFunc();

// 3. queueMicrotask
queueMicrotask(() => {
  console.log('微任务: queueMicrotask');
});

// 4. MutationObserver
const observer = new MutationObserver(() => {
  console.log('微任务: MutationObserver');
});
observer.observe(document.body, { attributes: true });
document.body.setAttribute('data-test', 'value');

// 5. process.nextTick (Node.js,优先级最高)
process.nextTick(() => {
  console.log('微任务: process.nextTick');
});

4.3 微任务的特点

  • 在当前宏任务执行完后立即执行
  • 会执行所有微任务,直到微任务队列清空
  • 微任务执行过程中产生的新微任务也会在当前循环中执行
  • 优先级高于宏任务

五、经典面试题解析

5.1 基础题 - 执行顺序

console.log('1');

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

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

console.log('4');

// 输出: 1 4 3 2

执行流程分析:

1. 执行同步代码: console.log('1') → 输出 1
2. 遇到 setTimeout,放入宏任务队列: [setTimeout]
3. 遇到 Promise.then,放入微任务队列: [then]
4. 执行同步代码: console.log('4') → 输出 4
5. 调用栈清空,执行微任务: console.log('3') → 输出 3
6. 微任务清空,执行下一个宏任务: console.log('2') → 输出 2

5.2 进阶题 - 嵌套异步

console.log('start');

setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('promise1');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('promise2');
  setTimeout(() => {
    console.log('timeout2');
  }, 0);
});

console.log('end');

// 输出: start end promise2 timeout1 promise1 timeout2

执行流程分析:

第一轮 Event Loop:
1. 同步代码: start, end
2. 微任务队列: [promise2]
3. 宏任务队列: [timeout1]

执行微任务 promise2:
- 输出 promise2
- 添加宏任务 timeout2 到队列: [timeout1, timeout2]

第二轮 Event Loop:
执行宏任务 timeout1:
- 输出 timeout1
- 添加微任务 promise1 到队列: [promise1]
- 执行微任务 promise1: 输出 promise1

第三轮 Event Loop:
执行宏任务 timeout2:
- 输出 timeout2

5.3 高级题 - async/await

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

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

async1();

new Promise((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

// 输出: script start, async1 start, async2, promise1, script end, async1 end, promise2, setTimeout

关键理解点:

// await async2() 等价于:
Promise.resolve(async2()).then(() => {
  console.log('async1 end');
});

// 所以 'async1 end' 是微任务

5.4 陷阱题 - 微任务产生新微任务

Promise.resolve().then(() => {
  console.log('promise1');
  Promise.resolve().then(() => {
    console.log('promise2');
  });
}).then(() => {
  console.log('promise3');
});

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

// 输出: promise1 promise4 promise2 promise3

微任务队列变化:

初始微任务队列: [then1, then4]

执行 then1:
- 输出 promise1
- 添加 then2 到队列: [then4, then2]
- 返回新 Promise,添加 then3 到队列: [then4, then2, then3]

执行 then4:
- 输出 promise4

执行 then2:
- 输出 promise2

执行 then3:
- 输出 promise3

六、Node.js 中的 Event Loop

6.1 Node.js Event Loop 的六个阶段

   ┌───────────────────────────┐
┌─>│           timers          │  执行 setTimeout/setInterval 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │  执行延迟到下一轮的 I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │  内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           poll            │  检索新的 I/O 事件
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │           check           │  执行 setImmediate 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │  关闭回调,如 socket.on('close')
   └───────────────────────────┘

6.2 Node.js 微任务优先级

// Node.js 中微任务执行顺序
process.nextTick(() => {
  console.log('nextTick');
});

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

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

// 输出: nextTick, Promise, queueMicrotask
// process.nextTick 优先级最高

6.3 setImmediate vs setTimeout

// 在 I/O 回调中,setImmediate 总是先于 setTimeout 执行
const fs = require('fs');

fs.readFile('./file.txt', () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  
  setImmediate(() => {
    console.log('setImmediate');
  });
});

// 输出: setImmediate, setTimeout

七、实战应用场景

7.1 性能优化 - 大数据渲染

// 错误示范:一次性渲染10000条数据,页面卡顿
function renderBadly(data) {
  data.forEach(item => {
    const div = document.createElement('div');
    div.textContent = item;
    document.body.appendChild(div);
  });
}

// 优化方案:利用 Event Loop 分批渲染
function renderOptimized(data, batchSize = 100) {
  let index = 0;
  
  function renderBatch() {
    const batch = data.slice(index, index + batchSize);
    batch.forEach(item => {
      const div = document.createElement('div');
      div.textContent = item;
      document.body.appendChild(div);
    });
    
    index += batchSize;
    
    if (index < data.length) {
      // 使用 setTimeout 让出控制权,避免长时间占用主线程
      setTimeout(renderBatch, 0);
    }
  }
  
  renderBatch();
}

7.2 防抖与节流的微任务实现

// 使用微任务实现更精准的防抖
function debounceWithMicrotask(fn, delay) {
  let timer = null;
  let pending = false;
  
  return function(...args) {
    if (timer) clearTimeout(timer);
    
    if (!pending) {
      pending = true;
      queueMicrotask(() => {
        pending = false;
      });
    }
    
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// 使用
const handleInput = debounceWithMicrotask((e) => {
  console.log('搜索:', e.target.value);
}, 500);

7.3 Promise 串行执行

// 利用微任务特性实现 Promise 串行执行
async function serialExecute(tasks) {
  const results = [];
  
  for (const task of tasks) {
    // await 确保每个任务按顺序执行
    const result = await task();
    results.push(result);
  }
  
  return results;
}

// 使用
const tasks = [
  () => fetch('/api/1'),
  () => fetch('/api/2'),
  () => fetch('/api/3')
];

serialExecute(tasks).then(results => {
  console.log('所有请求完成', results);
});

7.4 竞态条件处理

// 利用闭包和微任务解决搜索请求竞态问题
function createSearchHandler() {
  let latestRequestId = 0;
  
  return async function search(keyword) {
    const requestId = ++latestRequestId;
    
    try {
      const result = await fetch(`/api/search?q=${keyword}`);
      
      // 只处理最新的请求结果
      if (requestId === latestRequestId) {
        return result.json();
      }
    } catch (error) {
      if (requestId === latestRequestId) {
        throw error;
      }
    }
  };
}

const search = createSearchHandler();

八、常见误区与陷阱

8.1 误区1: setTimeout(fn, 0) 会立即执行

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

// 输出: 1 3 2
// setTimeout 总是在下一轮 Event Loop 执行

8.2 误区2: Promise.then 是同步的

console.log('1');

new Promise((resolve) => {
  console.log('2'); // Promise 构造函数是同步的
  resolve();
}).then(() => {
  console.log('3'); // then 是微任务
});

console.log('4');

// 输出: 1 2 4 3

8.3 误区3: async 函数会等待内部所有代码执行

async function test() {
  console.log('1');
  
  setTimeout(() => console.log('2'), 0);
  
  await Promise.resolve();
  
  console.log('3');
}

test();
console.log('4');

// 输出: 1 4 3 2
// await 后面的代码是微任务,会在同步代码之后、宏任务之前执行

8.4 陷阱: 死循环微任务

// 危险代码:会导致页面卡死
function infiniteMicrotasks() {
  Promise.resolve().then(() => {
    infiniteMicrotasks();
  });
}

infiniteMicrotasks(); // 微任务队列永远无法清空,宏任务无法执行

九、面试高频考点总结

9.1 执行顺序判断技巧

  1. 第一步: 找出所有同步代码,按顺序执行
  2. 第二步: 标记所有微任务(Promise.then、async/await、queueMicrotask)
  3. 第三步: 标记所有宏任务(setTimeout、setInterval)
  4. 第四步: 按照”同步 → 微任务 → 宏任务 → 微任务…”的循环执行

9.2 必背知识点

  • Event Loop 是协调 JavaScript 单线程执行异步任务的机制
  • 每次 Event Loop 执行一个宏任务,但会清空所有微任务
  • 微任务优先级高于宏任务
  • Promise 构造函数是同步的,then/catch/finally 是微任务
  • async/await 本质是 Promise 的语法糖,await 后面的代码是微任务
  • Node.js 中 process.nextTick 优先级最高

9.3 实战输出题

// 综合练习题
console.log('1');

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

new Promise((resolve) => {
  console.log('4');
  resolve();
}).then(() => {
  console.log('5');
  setTimeout(() => {
    console.log('6');
  }, 0);
});

setTimeout(() => {
  console.log('7');
  Promise.resolve().then(() => {
    console.log('8');
  });
}, 0);

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

console.log('10');

// 输出: 1 4 10 5 9 2 3 7 8 6

十、工作中的最佳实践

10.1 合理使用异步

// ✅ 好的实践
async function fetchUserData(userId) {
  try {
    const user = await fetch(`/api/users/${userId}`);
    const orders = await fetch(`/api/orders?userId=${userId}`);
    return { user, orders };
  } catch (error) {
    console.error('获取数据失败', error);
  }
}

// ❌ 不好的实践:过度使用 await 导致串行执行
async function fetchUserDataBad(userId) {
  const user = await fetch(`/api/users/${userId}`);
  const orders = await fetch(`/api/orders?userId=${userId}`); // 等待上一个完成
  // 应该使用 Promise.all 并行执行
}

10.2 避免回调地狱

// ❌ 回调地狱
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      console.log(c);
    });
  });
});

// ✅ 使用 async/await
async function fetchData() {
  const a = await getData();
  const b = await getMoreData(a);
  const c = await getEvenMoreData(b);
  console.log(c);
}

10.3 性能监控

// 监控 Event Loop 阻塞
let lastTime = Date.now();

setInterval(() => {
  const now = Date.now();
  const delay = now - lastTime - 1000;
  
  if (delay > 100) {
    console.warn(`Event Loop 被阻塞了 ${delay}ms`);
  }
  
  lastTime = now;
}, 1000);

总结

Event Loop 是 JavaScript 异步编程的核心机制,理解它的运行原理对于写出高性能、无 bug 的代码至关重要。记住以下关键点:

  1. JavaScript 是单线程,通过 Event Loop 实现异步
  2. 微任务优先级高于宏任务,会在当前宏任务执行完后立即清空
  3. 每次 Event Loop 只执行一个宏任务,但会清空所有微任务
  4. Promise 构造函数是同步的,then/catch/finally 是微任务
  5. await 后面的代码是微任务,会在当前同步代码执行完后执行