前端面试:EventLoop三剑客

1,659 阅读5分钟

引言

它是前端面试中的常客,用于考察对js程序运行机制的理解,其同时也是理解异步问题的重要基础。相关文章数不胜数。本文仅为一篇学习记录

1.剑客一:程序任务

在各种程序语言,任务的含义差别不大。JS是单线程语言,它的许多任务是在主线中执行,例如页面渲染,各种交互事件,资源请求等。

  • 宏任务(MacroTask):在一个js文件里,代码自上而下执行,任务被存储在先进先出的队列里。每个任务设置了一个标志,判断是否执行完毕,用于退出队列。简单例子
funcation a(){}
const b=setTimeout(()=>{},)
funcation c(){}
const nomalqueue=[a,c]//普通任务
const delayqueue=[b]//延迟任务
const taskqueue=[*,nomalqueue,delayqueue]
/*需要注意的是
*整个js文件是一个宏任务*/
bool running = true;
void MainTherad(taskqueue){
  for(item of taskqueue){
    //执行队列中的任务
    Task task = task_queue.takeTask(nomalqueue);
    ProcessTask(task);
    //执行延迟队列中的任务
    ProcessDelayTask(delayqueue)
    if(!running) //如果设置了退出标志,那么直接退出线程循环
        break; 
  }
}

其中包含了两种任务队列,除了上述提到的任务队列, 还有一个延迟队列,它专门处理诸如setTimeout/setInterval这样的定时器回调任务。

  • 微任务(MicroTask):假如在函数a中也存在setTimeout,那么这个队列需要如何设计?这就引出了微任务概念。引入微任务的初衷是为了解决异步回调的问题。想一想,对于异步回调的处理有两种方式:1.将异步回调进行宏任务队列的入队操作;2.异步回调放到当前宏任务的末尾。JS选择了第二种,在每一个宏任务中定义一个微任务队列,当该宏任务执行完成,会检查其中的微任务队列,如果为空则直接执行下一个宏任务,如果不为空,则依次执行微任务,执行完成才去执行下一个宏任务。在这种情况下,函数a里的微任务队列里有一个setTimeout微任务。 常见的微任务有MutationObservePromise.then(或.reject) 以及以 Promise 为基础开发的其他技术(比如fetch API), 还包括 js的垃圾回收过程。

剑客二:浏览器

以近年的字节面试题为例:

function getJson() {
    return new Promise((resolve, reject) => {
      setTimeout(function() {
        console.log(2);
        resolve(2)
      }, 2000)
    })
  }
  
  async function testAsync() {
    await getJson()
    console.log(3);
    }
  testAsync()
  //执行结果 2 3

当我们用EventLoop的角度来审视它时:

1.刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈

2.getJson和testAsync依次放入宏任务队列

3.setTimeout 被放入getJson的微任务队列

4.await 后的输出进入tsetAsync微任务队列

所以只有上个宏任务的所有任务完成后,下一次事件循环才开始,剩下的任务才执行

可以总结下浏览器的事件循环流程为:

  • 一开始整段脚本作为第一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
  • 执行队首新的宏任务,回到第二步,依此循环,直到宏任务和微任务队列都为空

剑客三:Node

经典名图网上随便找...比如

1.node执行重要阶段(定时器回调,轮询,检测)

  • 回调(timer):在此阶段程序检测定时器函数,如setTimeout,setInterval.根据设定的时间,到了时间就执行回调函数,就如同 午时已到,刀起头落 。
  • 轮询(poll):当一些异步任务完成后,需要通知主线程开始下个任务。通过'data'、 'connect'等事件使得事件循环到达 poll 阶段。到达了这个阶段后:
  • 如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到timer阶段。
  • 如果没有定时器, 会去看回调函数队列。如果队列不为空,拿出队列中的方法依次执行如果队列为空,检查是否有 setImmdiate 的回调;有则前往check阶段;没有则继续等待,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段。
  • 检查(check) 。这是一个比较简单的阶段,直接执行 setImmdiate 的回调,setImmdiate用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的其他语句后,就立刻执行这个回调函数,
  1. 其他阶段
  • I/O异常阶段:顾名思义就是在请求I/O等异步事件的响应失败,程序进入回调进入从阶段
  • 空闲,预备状态
  • 关闭事件回调阶段:如果一个 socket 或句柄被突然关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。
  1. 小例子
setTimeout(()=>{
    console.log('A')
    Promise.resolve().then(function() {
        console.log('B')
    })
}, 1)
setTimeout(()=>{
    console.log('C')
    Promise.resolve().then(function() {
        console.log('D')
    })
}, 2)
//A B C D

4.nodejs 和 浏览器关于eventLoop的主要区别 两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的。更详细请见:废人指路

总结

  • 先抓住宏任务与微任务执行顺序这个关键问题
  • 多加记忆
  • 考虑异步问题时,模拟程序的任务队列会有所帮助

本人大三,正寻实习,与君共勉。寥有拙作,万望指正