《复习计划二》你躲不过的 Event Loop 规则

整理复习准备面试 ing ……
? 如有错误 欢迎指正

什么是 Event Loop?

Event Loop 即时间循环,是浏览器/Node 解决 JavaScript 单线程运行不阻塞的机制,也就是异步的原理。

浏览器中的 Event Loop

从一道题说起

console.log('script start');

// 1-1 
setTimeout(() => {
  console.log('史莱姆是一只猫');
}, 2000);

// 2-1 
Promise.resolve()
.then(function() {
  // 2-2
  console.log('promise1');
}).then(function() {
  // 2-3
  console.log('promise2');
}).finally(() => {
  // 2-4
  console.log('finally')
});


// 3-1
async function foo() {
  await bar()
  // 3-2
  console.log('async1 end')
}
foo()

// 4-1
async function errorFunc () {
  try {
    // 4-2
    await Promise.reject('error!!!')
  } catch(e) {
    // 4-3
    console.log(e)
  }
  // 4-4
  console.log('async1');
  return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res)) // 4-5

// 5-1
function bar() {
  console.log('async2 end') 
}

// 6-1
console.log('script end');
复制代码

这个题并不难,答案也显而易见。

script start
async2 end
script end
promise1
async1 end
error!!!
async1
promise2
finally
async1 success
史莱姆是一只猫
复制代码

宏任务/微任务

我们用一张图来解析

image.png

主线程是一条加工线,待执行的任务就是流水线上的原料,需要一个个加工,event loop 便是加工的工人,能够直接被处理的原料会被依次处理,这就是同步任务(macrotask/宏任务),倘若需要先预处理的原料会被按照种类排序处理后,再次放回流水线,这就是异步任务(microtask/微任务)

常见宏任务script全部代码、setTimeoutsetIntervalsetImmediateI/OUI Rendering
常见微任务Process.nextTick(Node独有)、PromiseMutationObserver

执行过程

  • 主线程不断循环
  • 开始执行,同步任务创建执行上下文进入执行栈。
  • 同步任务执行过程中,会判断执行代码是否有同步/异步任务,有就分别放入各自的队列中。
  • 同步任务执行完毕,会来处理异步任务队列中的微任务,清空整队的微任务。
  • 微任务队列清空,下一个同步任务进入执行栈,重复上面的循环。

开始分析上面的题目

接着我们开始分析上面的题目

  • 首先,打印 script start
  • 运行到 1-1,发现是宏任务,放入队列
  • 运行到 2-1,Promise.resolve() 运行,后面的 then() 放入微任务队列
  • 运行到 3-1,foo() 函数,await bar(),打印 async2 end,后面放入 微任务队列

这里需要注意 await 的用法
await 表达式可以理解为异步等待获取结果,await 作为语法糖就是为了 简化 promise().then() 的写法
3-1 await bar() 后面的代码可以理解为

Promise.then((res) => res).then(() => {
  console.log('async1 end')
}))
复制代码
  • 运行到 4-1,errorFunc() 运行,

await Promise.reject('error!!!') 后面的代码可以理解为

Promise.then((res) => res).then(() => {
   console.log(e)
   console.log('async1');
}).then(() => Promise.resolve('async1 success'))
复制代码
  • 运行到 6-1,打印 script end,主线程运行完毕,开始运行微任务队列。
  • 打印 2-2 promise1,后面的 then() 放入微任务队列。
  • 打印 3-2 async1 end
  • 打印 4-3 error!!!
  • 打印 4-4 async1,后面的 then() 放入微任务队列。
  • 打印 2-3 promise2
  • 打印 2-4 finally
  • 打印 4-5 async1 success
  • 微任务队列清空,主线程拉取宏任务
  • 打印 1-1 史莱姆是一只猫

NodeJS Event Loop

Node.js 的运行机制

  • V8 引擎解析 JavaScript 脚本。
  • 解析后的代码,调用 Node API。
  • libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
  • V8 引擎再将结果返回给用户。

六个阶段

image.png

  • timers: 执行 setTimeoutsetInterval 中到期的 callback
  • pending callback: 上一轮循环中少数的 callback 会放在这一阶段执行。
  • idle, prepare: 仅在内部使用。
  • poll: 获取新的 I/O 事件,适当的条件下 node 将阻塞在这里。
  • check: 执行 setImmediate 的回调。
  • close callbacks: 执行 socketclose 事件回调。

Process.nextTick()

process.nextTick() 虽然它是异步 API 的一部分,但未在图中显示。这是因为 process.nextTick() 从技术上讲,它不是事件循环的一部分。

当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

题目

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)

Promise.resolve().then(function() {
  console.log('promise3')
})

function tick(callback) {
  process.nextTick(callback);
}

let cat

tick(() => {
  console.log('name', cat); // 1
});

cat = 1;

console.log('end')
复制代码

结果(node > v11)

start
end
name 1
promise3
timer1
promise1
复制代码

Node 与浏览器的 Event Loop 差异

image.png

image.png

  • Node 端,microtask 在事件循环的各个阶段之间执行
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享