JavaScript EventLoop(事件循环)

123 阅读4分钟

浏览器进程

浏览器是一个多进程的应用程序

    1. 每一个标签页都是一个进程,且互相不影响
    1. 浏览器也有一个主进程:UI(用户界面)
    1. 每一个标签页也都是多进程的:渲染进程、网络进程、GUP进程、第三方插件进程等。

渲染进程

渲染进程包含多个线程

    1. GUI渲染线程
    1. JS引擎线程,与页面渲染时互斥
    1. 事件触发线程,即EventLoop
    1. 定时器监听线程(setTimeout)、HTTP网络请求(ajax)、事件监听线程(click) 也都是单独的线程

EventLoop

EventLoop解决的问题是:JS执行可能会调用异步方法,这些方法是如何调度执行的

JS是单线程运行的,所以大部分代码是同步的,但也有部分异步操作代码,包括了异步微任务和异步宏任务:

异步微任务

  1. Promise.then/catch/finally
  2. async/ await
  3. MutationObserver
  4. IntersectionObserver
  5. requestAnimationFrame
  6. queueMicrotask 手动创建一个异步微任务
  7. process.nextTick(Node)

异步宏任务

  1. setTimeout/setInterval
  2. 事件绑定
  3. XMLHttpRequest/Fetch
  4. MessageChannel

JS的异步操作原理是:借用浏览器的多线程机制,再基于EventLoop(事件循环)机制,实现的单线程异步。

EventLoop机制详解

Snipaste_2023-02-23_09-46-22.png

eventloop3.png

浏览器加载页面,除了开辟堆栈内存外,还会创建两个队列:

    1. WebAPI: 任务监听队列
    1. EventQueue: 事件/任务队列

JS执行时,从上到下执行,如果遇到异步代码,会把异步任务放到任务监听队列中,当异步任务被检测到为可执行时,不是立即执行,而是放到EventQueue中排队等待执行

    1. 根据是微任务还是宏任务,放到不同队列中
    1. 按照队列先进先出的原则,谁先进来的,谁就排在队列的最前面

等到同步代码执行完成,此时主线程空闲下来,此时就会到EventQueue中把正在排队的异步任务取出执行:

    1. 异步微任务优先级较高,只要有可执行的微任务,就先执行。
    1. 同样级别的任务(微任务还是宏任务),谁在队列的前面,谁先执行。
    1. 根据1,每执行完一个宏任务,都会先清空微任务队列,再取出一个宏任务继续执行。

注意:这里的执行是把任务拿到执行栈中执行,而且是交给主线程执行。(所以只要拿出来的这个任务没有执行完,永远不会再去拿其他任务)

例子分析

例子1

console.log('script-start')

new Promise((resolve, reject) => {
  console.log('promise1')
  setTimeout(() => {
    resolve('ok-1')
  })
}).then((value) => {
  console.log(value)
})

setTimeout(() => {
  console.log('setTimeout')
})

new Promise((resolve, reject) => {
  resolve('ok-2')
}).then((value) => {
  console.log(value)
})

console.log('script-end')

分析:

  1. console.log('script-start'): 同步代码,打印(script-start)
  2. 遇到一个promise,执行函数的内容是同步代码,打印(promise1),遇到 一个setTimeout,放到WebAPI队列,由于,没有设置延时时间,setTimeout会被放到EventQueue队列中的宏任务队列(宏任务1)。而then由于此时还不知道promise的状态(因为setTimeout里面的内容还没有执行,promise的状态还未改变),所以是进入WebAPI任务队列中。
  3. 遇到 一个setTimeout,放到WebAPI队列,由于,没有设置延时时间,setTimeout会被放到EventQueue队列中的宏任务队列(宏任务1)
  4. 遇到一个promise,执行函数的内容是同步代码,promise状态已改变,then方法里面的回调函数放到微任务队列中(微任务1)。
  5. console.log('script-start'): 同步代码,打印(script-end)

此时,同步代码执行完,主线程空闲下来,此时就会到EventQueue中把正在排队的异步任务取出执行

  1. 发现有微任务,先清空,执行了微任务1,打印(ok-2)
  2. 取出一个宏任务,宏任务1被执行,第一个promise的状态被改变了,then方法放到微任务中
  3. 发现此时有微任务,执行微任务,打印(ok-1)
  4. 取出一个宏任务,宏任务2被执行,打印(setTimeout)

所以打印结果是:

script-start
promise1
script-end
ok-2
ok-1
setTimeout

例子2

async function async1() {
  console.log('async1 start');
  await async2(); 
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function () {
  console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});
console.log('script end');
/**
 * script start
 * async1 start
 * async2
 * promise1
 * script end
 * async1 end
 * promise2
 * script end
*/