js事件循环机制(EventLoop)那些事

718 阅读5分钟

从一道面试题开始

EventLoop 面试题

这是一道经典的面试题,主要考查的就是我们今天的主角事件循环,大家可以先细细品,记下你想到的答案,继续往下读.

js的事件循环

首先我们都知道js是一个单线程的语言,在每个运行瞬间最多只能运行一个js语句.但是在实际的开发中,我们发现由于各种原因,有些程序执行时会很耗时,假如只通过代码从上往下顺序执行, 那么在耗时程序后面的程序就会一直等待着,例如当你打开一个视频页面, 我们难道要等着整个视频都加载完成才能点击播放观看吗?显然答案是否定的.这时候机智的程序猿(媛)发现如果通过cpu高速切换js任务调度,我们就可以创造出表面上的任务同时执行.让你在缓冲视频的同时也能观看已经缓冲完成视频片段.我们把这种比较简单需要顺序执行的任务称为同步任务(Synchronous Tasks),把相对比较耗时等待的任务称为异步任务(Asynchronous Tasks).

在js的事件循环中,我们又把异步任务分为宏任务(macrotasks)微任务(microtasks).

  • 宏任务: setTimeout,setInterval,io事件等....
  • 微任务: Promise.then, process.nextTick,MutationObserver

js执行时,js引擎会先把脚本至上而下解析,如果是同步任务则放入主线程执行栈中运行,如果是异步任务则放入异步队列中执行 ,当异步任务执行结束后,js引擎会根据异步任务的种类把异步任务的回调处理任务移入相应的宏任务队列或微任务队列中去.

EventLoop

如图所示,在js事件循环中,每次开始时,js引擎会先判断当前的宏任务列表是否为空,如果存在有宏任务则把宏任务第一个移入主线程执行栈中,然后执行主线程执行栈的任务最后在检查微任务队列是否有任务需要执行,这样便完成了一个事件循环.

回归题目

知道了事件循环机制后我们重新回到刚刚的那道面试题.

在第一个事件循环中,程序先检查宏任务队列,发现队列中为空,便进入主线程执行栈,执行了一个main函数:

  1. 函数一开始先打印出main start
  2. 然后遇到第一个setTimeout的宏任务,便把setTimeout放入异步队列中去执行,由于setTimeout传入的延迟时间是0,(这里我们默认为瞬间完成,其实最小时延接近4ms),所以把第一个setTimeout的回调任务放入宏任务队列中;
  3. 接着执行process.nextTick, process.nextTick是一个瞬间微任务,便把他放入异步队列后执行结束后将回调放入微任务中.
  4. 之后便是一个Promise,执行Promise时打印出second Promise,遇到Promise.then则将其回调函数移入微任务中.
  5. 最后打印出main end

至此第一个事件循环中的主线程执行栈的任务全部执行结束,接着进入微任务,发现此时微任务队列存在有两个任务:

  1. 执行process.nextTick回调函数,打印next tick function

  2. 执行代码中最后一个Promise.then的回调函数second Promise then function,并执行一个延时为0的setTimeout,同理把setTimeout的回调函数放入宏任务中去.

    这样一个事件循环就结束了.

接着是第二个任务循环. js引擎检查宏任务队列,发现存在有两setTimeout的任务, 按照队列顺序先把第一个setTimeout放入主线程执行栈中执行打印first setTimeout function并执行Promise,打印first Promise,将Promise.then放入微任务队列中,结束主线程执行栈,接着执行微任务队列打印first Promise then function,至此第二个事件循环结束.

最后执行第三个事件循环, 检查宏任务时发现只剩下一个setTimeout,执行打印second setTimeout function,此时所有任务都执行完毕.

所以最后的结果是:

  1. main start
  2. second Promise
  3. main end
  4. next tick function
  5. second Promise then function
  6. first setTimeout function
  7. first Promise
  8. first Promise then function
  9. second setTimeout function

关于setTimeout、setInterval

setTimeout和setInterval是延时后将回调函数放入下一轮的宏任务队列中去,所以如果存在执行完当前的剩下的同步任务和微任务所需的时间大于setTimeout和setInterval延时时间便会触发回调函数时延变长.这样也解释了为什么setInterval中回调函数运行所需时间超过setInterval延迟时间时会产生没间隔触发回调函数.

从一道面试题结束

故事的最后给大家留一道思考题,至于答案........

async function async1(): Promise<void> {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2(): Promise<void> {
  console.log('async2');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeOut');
}, 0);

async1();

new Promise<void>((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

最后的最后

感谢大家的阅读,希望大家看了有所收获,文笔粗糙勿喷,如有差错,请斧正.