浏览器中的Event Loop

301 阅读3分钟

JavaScript 语言的一大特点是单线程。

线程和进程

  • 线程是 cpu 调度的最小单位(线程是进程上的一次程序运行单位,一个进程可以有多个线程)。
  • 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)。

浏览器是多进程的

每开一个标签页系统就创建一个独立的进程,一个进程中可能包含如渲染线程、JS 引擎线程、HTTP 请求线程等。
注意:由于浏览器的优化机制某些进程可能会被合并。

为什么JavaScript是单线程的

如果 JavaScript 是多线程的,假定 procces1 和 procces2 同时对同一个 DOM 作出操作,浏览器应该以哪一个为准,所以 JavaScript 只能为单线程。

事件循环

任务分为同步任务和异步任务,同步任务会进入主线程,异步任务则会进入事件队列 ( Event Queue )。主线程中的任务执行完毕后会从事件队列中取出放入执行栈。

任务除了分为同步任务和异步任务,还分为微任务和宏任务。

  • 微任务(microtask)包括:process.nextTick,Promise,MutationObserver
  • 宏任务(macrotask)包括:主代码script,setTimeout,setInterval

事件循环的执行顺序为

  • 事件循环开始,script 代码块做为宏任务进入主线程执行
  • 同步任务移入主线程执行,异步任务进入事件队列
  • 主线程执行完成后,取出事件队列中的微任务执行
  • 微任务执行完毕后,事件循环结束
  • 取出事件队列中的宏任务,开始新一轮事件循环
  • ......

示例:

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

console.log('4')

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

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

分析:

  • 第一轮事件循环:

    • script 作为宏任务进入主线程
    • 遇到 setTimeout,注册其回调函数作为宏任务移入事件队列,记为 setTimeout1
    • 遇到 console.log,输出'4'
    • 遇到 Promise,输出'5',注册其回调函数作为微任务进入事件队列,记为 then
    • 遇到 setTimeout,注册其回调函数作为宏任务移入事件队列,记为 setTimeout2
    • 宏任务执行完毕,执行事件队列中的微任务 then,输出'6'

    第一轮事件循环结束输出'4'、5'、'6'

  • 第二轮事件循环:

    • 执行宏任 setTimeout1
    • 遇到 Promise,输出'1',注册其回调函数作为微任务进入事件队列,记为 then
    • 遇到 console.log,输出'3'
    • 宏任务执行完毕,执行事件队列中的微任务 then,输出'2'

    第二轮事件循环结束输出'1'、'3'、'2'

  • 第三轮事件循环:

    • 遇到 Promise,输出'7',注册其回调函数作为微任务进入事件队列,记为 then
    • 遇到 console.log,输出'9'
    • 宏任务执行完毕,执行事件队列中的微任务 then,输出'8'

    第三轮事件循环结束输出'7'、'9'、'8'

    代码执行完毕输出结果为'4'、5'、'6'、'1'、'3'、'2'、'7'、'9'、'8'

参考:前端面试之道