JS:详解Event-Loop

123 阅读3分钟

JS是一门单线程的语言,这意味着它同一时间只能执行一个任务。那么当JS遇到需要长时间执行的代码时,后面的代码是不是会一直处于等待被执行的状态呢?如果是这样的话,岂不是有可能会给用户带来非常不好的体验?然而事实是并不会这样,Event-Loop(事件循坏)能够很好的解决这个问题,要知道这个问题的答案就需要我们深入的了解去JS这门语言中代码是如何被执行的。

线程和进程的概念
  • 进程指的是cpu在运行指令和保存上下文所需的时间
  • 线程是进程中更小的单位,指的是一段指令执行所需的时间
以浏览器新开一个tab页为例

这个操作是一个进程,这个进程中会包含多个线程,如:

  • 渲染线程
  • http请求线程
  • js引擎线程

在这个进程中:

  • 线程之间是可以一起工作的
  • 但是渲染线程 和 js引擎线程 是互斥的
JS为什么会被设计成一门单线程语言
  • 节约内存
  • 没有锁的概念,节约了上下文切换的时间

JS中通过事件循环和异步编程来弥补单线程的限制

异步任务

异步任务分为宏任务和微任务

  • 宏任务:script ,setTimeout,setInterval,setI,setImmediate,I/O,UI-rendering
  • 微任务:promise.then(),MutaionObserver,process.nexTick()

Event-Loop(事件循环)

JS中事件循环的流程:

  1. 执行同步代码 (这属于宏任务)
  2. 当执行栈为空,查询是否有异步需要执行
  3. 执行微任务
  4. 如果有需要,会渲染页面
  5. 执行宏任务 (下一次event-loop的开始)

举个例子:

这段代码的最终会输出什么呢?

console.log(1);

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

console.log(3);
  1. 第1行代码执行,此时输出 1。
  2. 然后是一个setTimeout函数,该函数存在一个1000毫秒后执行的回调函数。但是这个回调不会立即执行,而是被放入事件队列中,等待当前执行栈清空并且指定的延迟时间过去之后才执行。
  3. 第16行代码执行,输出 3。
  4. 等待1000毫秒,然后开始执行setTimeout的回调函数。
  5. 在这个回调函数里,先执行第4行代码,输出 2。
  6. 创建 Promise 对象。在这个 Promise 的执行函数中,执行 console.log(4),输出 4,然后执行到第6行的resolve()此时表示这个Promise已成功完成。
  7. 在Promise的执行函数中,第8行有个setTimeout,它的回调是一个异步操作,所以这个回调会被放入事件队列中,等待当前执行栈清空才执行。
  8. 因为 Promise 的 resolve() 已经被调用,.then() 中注册的回调函数将被放入微任务队列,等待当前执行栈清空后立即执行,这发生在下一个宏任务即第8行的setTimeout之前。
  9. 当前执行栈清空,事件循环首先检查微任务队列,发现 .then() 中的回调函数,执行后输出 5。
  10. 最后,事件循环处理下一个宏任务队列中的任务,执行之前安排在 Promise 执行函数中的 setTimeout 的回调函数,输出 6。