彻底搞定Javascript事件循环

2,303 阅读3分钟

前言

众所周知,Javascript是一门单线程、非阻塞的脚本语言。这表示Javascript只有一个主线程来执行所有任务,各种任务必须排队来执行。既然如此,为什么浏览器可以在进行耗时较长的任务同时还能响应用户界面交互呢?所谓的非阻塞又从何说起呢?下面我们来具体探究一下。

渲染进程与主线程

首先我们先来说下浏览器进程与Javascript主线程是什么关系?在浏览器中一个tab页面就是一个渲染进程(render process),一个渲染进程中包含多个线程,其中包括以下四种线程:

  • 主线程 Main thread
  • 工作线程 Worker thread
  • 排版线程 Compositor thread
  • 光栅线程 Raster thread

Javascript代码运行在主线程当中

宏任务与微任务

异步任务可以分为宏任务(macrotask)和微任务(microtask)两种。 宏任务包含以下几种:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O 操作
  • UI 渲染
  • MessageChannel
  • postMessage

微任务包含以下几种:

  • Promise
  • Object.observe
  • MutationObserver
  • process.nextTick(Node.js)
  • queueMicrotask

Javascript引擎根据以上两种类型将任务分到两个队列中。

事件循环机制

Javascript引擎在执行代码过程中,遇到同步任务会立即执行,遇到异步任务时暂时将异步任务挂起,交给浏览器内核去执行。在执行完所有同步任务时。由于浏览器会不停的检查任务队列中是否有任务等待执行,如果存在微任务,会先执行所有微任务,将微任务队列清空,再次取宏任务中第一个任务放到主线程执行,执行完成该任务会再次检查是否有微任务存,直到任务队列中的所有任务被执行完毕。如此循环往复,直到所有的异步任务被执行完成。当浏览器内核完成异步任务时,会将异步任务回调函数放入任务队列,等待下一次事件循环来临后将所有的任务队列中回调函数执行。

Event-Loop

实例

在搞清楚事件循环的机制后,解答以下的思考题将变得 so easy 🙂! 请思考以下代码执行后的打印结果:

setTimeout(()=>{
    console.log(1)
    Promise.resolve().then(()=>{
        console.log(5)
        setTimeout(()=>{
            console.log(7)
        },0)
    })
},0)

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

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

console.log(6)

正确答案是 6,3,1,5,2,4,7

为什么呢?下面我们来具体分析下:

  1. 主线程中的同步任务会先执行,所以第一个输出6
  2. 微任务先与宏任务执行,所以第二个输出3
  3. 微任务执行完成后该执行第一个setTimeout,所以输出1
  4. 第一个setTimeout完成后执行存在的微任务,所以输出5
  5. 第二个微任务完成后执行第二个setTimeout,所以输出2
  6. 当第二个微任务执行完毕后执行任务队列中的第三个setTimeout,所以输出4
  7. 之后执行任务队列中最后一个setTimeout,输出7

总结

看完这篇文章后,是不是觉得Javascript的事件循环其实很简单呢?😏 最后我们再次总结下这次所学习的内容:

  1. Javascript运行在主线程当中,任务需要排队执行
  2. 任务队列(task queue)是先进先出
  3. 微任务先与宏任务执行
  4. 微任务执行完后只执行一条宏任务,之后再次检查是否存在微任务
  5. 浏览器会暂时挂起未完成的异步任务,等待完成后将回调函数放入任务队列中等待下一次事件循环来临时执行