重学浏览器 Event loop

268 阅读4分钟

前言 作为前端er面试没有被问过Event loop,那么可能是还没入门的前端。似乎感觉人人都学Event Loop,人人都在谈论Event loop, 但是又没有多少人真正懂Event loop。前端真的太难了。

最近重学了一遍Event loop, 感觉受益良多,才有此文。

Event loop是什么

我们知道浏览器单线程的,也就是说同一时刻只能处理一个任务,如果遇到前面的任务耗时比较长,那么后面的任务只能等待(只针对同步的代码)。js代码分为同步代码和异步代码,主线程会执行完所有的同步代码,再去执行异步代码(定时器, ajax, Dom操纵等),异步代码执行完毕之后会把当前的回调函数放到任务队列,等待主线程来执行。 执行完同步任务主线程会一直轮询任务队列是否还有,这就是event loop的运行机制。

6052168a67a8d3445b2a75bae5f28d4.png

微任务和宏任务

宏任务(macrotask)可以理解每次js执行栈的代码都是一个宏任务, 浏览器为了能够使得JS内部macrotask与DOM任务能够有序的执行,会在一个macrotask执行结束后,在下一个macrotask 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

宏任务包含

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

微任务(microtask)会在渲染之前执行,所以microtask的执行优先级比macrotask要高, 一个macrotask执行完毕之后,回把执行当前macrotask产生的microtask执行完。

微任务包含:

Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

为什么 setTimeout不准

下面这份代码相当于是 webAPi setTimeout 1秒之后把回调函数foo丢到任务队列里, 等待主线程有空来执行, 主线程循环一次tick的时间大约是4ms。如果把定时器的第二个参数设为0,代表立刻把回调函数foo丢到任务队列,等待主线程执行。 敲黑板了, 所以setTimeout为什么不准的原因是setTimeout的回调函数在任务队列等待主线程执行的这段时间是额外多出的时间。

setTimeout(function foo() {}, 1000)

(实战环节)分析宏任务和微任务的面试题

分析流程

  1. 从上到下先执行同步代码, 然后开始执行异步代码
  2. 微任务优先于宏任务执行
  3. 执行一次宏任务就会清空微任务队列
面试题1
console.log('start')

setTimeout(function foo() {
  console.log('s1')
  Promise.resolve().then(() => {
    console.log('p1')
  })
  Promise.resolve().then(() => {
    console.log('p2')
  })
})

setTimeout(function bar() {
  console.log('s2')
  Promise.resolve().then(() => {
    console.log('p3')
  })
  Promise.resolve().then(() => {
    console.log('p4')
  })
})

console.log('end');

打印结果: start, end, s1, p1, p2, s2, p3, p4

分析步骤:

  1. 首先执行同步代码,输出 start, end
  2. 执行第一个宏任务foo, 输出s1
  3. 执行第一个宏任务foo就会清空微任务: 输出 p1, p2
  4. 执行第二个宏任务 bar, 输出s1
  5. 执行第二个宏任务bar就会清空微任务: 输出 p3, p4

Tips: 有些同学可能会认为 p1,p2, p3, p4是一起输出的。在执行foo的时候, 还没有执行bar, 所以p3, p4还没有被推入微任务队列, 只有执行bar之后, p3, p4才会被推入微任务队列。也就是说每一次执行完宏任务就会立刻检查微任务队列

面试题2
console.log('start')
setTimeout(() => {
  console.log('s1')
  Promise.resolve().then(() => {
    console.log('p1')
  })
  Promise.resolve().then(() => {
    console.log('p2')
  })
})

Promise.resolve().then(() => {
  console.log('p3');
  setTimeout(() => {
    console.log('s2')
  })
  setTimeout(() => {
    console.log('s3')
  })
})
console.log('end');

打印结果: start, end, p3, s1, p1, p2, s2, s3

分析步骤:

  1. 首先执行完同步代码,此时宏任务队列有[s1], 微任务队列有[p3],输出: start, end。
  2. 优先执行微任务p3, 此时宏任务队列有[s1, s2, s3], 微任务队列有为空, 输出: p3。
  3. 执行s1, 此时宏任务队列有[s2, s3], 微任务队列有[p1, p2], 输出: s1。
  4. 清空微任务, 此时宏任务队列有[s2, s3], 微任务队列有为空 输出: p1, p2。
  5. 执行完宏任务, 此时宏任务队列为空, 微任务队列有为空 输出: s2, s3。

结尾

转眼毕业一年多留了,技术上感觉有些浮躁,想通过在掘金写写文章,沉淀一下技术。文章如有不当之处,还望大家当指出, 共同探讨。