浅谈浏览器事件循环——Event loop

746 阅读3分钟

一、引言

众所周知,JS是一门单线程异步非阻塞的编程语言。那单线程是如何做到异步的呢?

二、单线程和异步

在JS中,只有一个线程处理JS任务,这也意味着所有任务只能同步执行。如果遇到网络请求,那就必须等待请求结果返回,JS才能继续往下执行。这明显是不合情理的。所以浏览器引入了Event Loop机制来帮助处理这种长时间挂起的任务。

三、浏览器事件循环模型

本篇文章不涉及浏览器底层真实运行环境,只对浏览器的事件循环模型进行抽象,感性理解:

概念理解

把JS引擎想象成美国总统,他的职务是给国会通过的法案签名(执行任务),形成法律。由于他不能找人代签(单线程),所以法案(task)在他的办工作上(task queue)堆积如山。他一般会先抽出压在最下面的法案(按照队列顺序),签完之后在再从最底下抽出来开签,但是秘书和总统的关系比较好,很多重要的事情(microtask)都是由秘书交给总统处理,秘书只要看见总统手头签完名(调用栈为空),就直接把手上所有的法案交给总统,不用排队。总统有个习惯,每16.6ms(一般的屏幕渲染周期)他要喝一口伏特加(渲染),如果不喝,他就会暴躁(阻塞渲染),所以他手速非常快,一般能在几毫秒之内给所有法案签名,剩下的时间就能翘着二郎腿。

段子写完了,回到JS~

  1. 调用栈(call stack): JS引擎唯一工作线程,用于函数调用执行,所有的任务都要推入调用栈才能执行;
  2. WebApis: 浏览器提供的事件,如:DOM操作,AJAX请求,定时器等等
  3. 任务队列(task queue): 事件循环将完成的webApi事件按顺序排队形成队列,在调用栈为空时,队首的回调函数推入调用栈执行
  4. 微任务队列(microtask queue): 每次当一个任务执行完成且调用栈为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

事件循环流程 (未包含渲染)

  1. 执行当前调用栈中的task
  2. 执行microtask queue中所有 microtask,即使是中途加入的microtask,直到microtask queue为空;
  3. 执行task queue 中队首的task;如果调用栈为空,执行2;

image.png

举例说明

<script>
      function task1() {
        console.log('task1')
        task2()
        return
      }
      function task2() {
        console.log('task2')
        task3()
        return
      }
      function task3() {
        console.log('task3')
        return
      }
      document.body.addEventListener('click', () => {
        setTimeout(function handleClick(){
          console.log('handleClick called')
        })
      })
      new Promise((resolve) => {
        document.body.addEventListener('click', () => {
          resolve('promise')
        })
      }).then(function success(value) {
        console.log(value)
      })

      task1()
      document.body.click()
</script>
  1. 主函数main推入调用栈,代码从上往下执行,函数声明task1,task2,task3
  2. 添加bodyclick事件处理函数handleClick到WebAPIs,等待事件触发。
  3. 添加promise,当bodyclick事件触发时,调用resolve('promise');
  4. task1调用,task1推入调用栈执行,控制台输出'task1';
  5. task1内部调用task2,task2推入调用栈执行,控制台输出'task2';
  6. task2内部调用task3,task3推入调用栈执行,控制台输出'task3';
  7. task3执行完毕,返回,弹出调用栈;
  8. task2执行完毕,返回,弹出调用栈;
  9. task1执行完毕,返回,弹出调用栈;
  10. document.body.click()click事件触发,监听函数执行;
  11. handleClick推入task queue等待执行; resolve调用,success推入maricotask queue等待执行;
  12. main执行完毕,返回,弹出调用栈;调用栈清空;
  13. 事件循环机制检查调用栈为空,执行microtask queue中的回调函数success,控制台打印'promise';
  14. 检查microtask queue为空,执行task queue中的回调函数handleClick,控制台打印'handleClick called';