运行时与事件循环

325 阅读4分钟

参考:mdn-微任务与Javascript运行时环境
推荐:JavaScript 事件循环:从起源到浏览器再到 Node.js(淘系前端订阅号发布的一篇文章,从事件循环的起源开始解说且事件循环对事件的调度解说的很清楚,我看了以后对事件循环的理解有深入了一点,回来更新了我的博客)

执行上下文

在整理学习包时,发布了执行上下文相关的内容参考

javascript运行时

js运行时相关的一些基本概念

在执行时js代码时,javascript运行时实际上维护了一组执行js代码的代理
每个代理由执行上下文集合、执行上下文栈、主线程、一组可能创建用户执行worker的额外的线程集合、一个任务队列、一个微任务队列。
除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其他的组成部分对该代理是唯一的。

事件循环

每个代理是由事件循环驱动的,事件循环负责收集事件(包括用户事件和其他非用户事件等)、对任务进行排序以便在合适的时候执行回调。
事件循环本质上是浏览器用于协调用户交互(鼠标、键盘)、脚本(如javascript)、渲染(如html DOM、css样式)、网络行为等一种机制。

事件循环排序分为两个队列:

  • Task Queue:外部队列/宏任务队列主要是浏览器协调各类事件的队列。

    • DOM操作(页面渲染)
    • 用户交互(鼠标、键盘)
    • 网络请求
    • 定时器
    • History Api操作

在每次迭代开始之后加入到队列中的任务在下一次迭代开始后才会执行

  • Microtask Queue:内部任务队列/微任务队列,即JavaScript语言内部的事件队伍,在html标准中并没有规定这个队列的事件源,通常认为有以下几种:
    • Promise的成功(then)和失败(catch)
    • MutationObserver
    • Object.observe(已废弃)

当一个(宏)任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。即:微任务可以添加新的微任务到队列中,并在下一个任务开始执行前且当前事件循环结束之前执行完所有的微任务。

总结事件循环的规律

运行一段js代码时,事件循环去这样调度这些事件的:

  1. 从宏任务队列中取出一个可执行的任务,如果有则执行,没有就往下。
  2. 从微任务队列中按顺序取出所有的任务去执行,执行完了或者没有就玩下。
  3. 浏览器渲染。

其实叫任务队列不大准确,应该是有序集合,队列在开发的认知中是先进先出的,但宏任务的执行顺序并不一定按照加入到任务集合中的顺序,比如setTimeout即使是排在最前面没有满足条件也不会执行的。
为什么先是从宏任务队列中取出一个可执行任务,再去清空微任务队列,而实际中SetTimeOut0的代码其实在promise.then之后执行的?
不知道大家有没有这个疑问,我之前是一直不理解的。后来看到了mdn文档的这句话就解惑啦。
在每次迭代开始之后加入到(宏)队列中的(宏)任务在下一次迭代开始后才会执行。
在迭代开始加入到微任务队列的任务会在下一次迭代开始之前清空队列也就是执行所有微任务。 640.webp

有三种事件循环

  • Window事件循环:
    驱动所有的同源窗口。

    这里的同源是指由同一个窗口打开的多个子窗口、同一个窗口中的iframe等。
    窗口可能运行在相同的事件循环中。

  • Worker事件循环:
    用于驱动worker的代理
    包括所有种类的worker:最基本的web worker、shared worker、service worker。
    Worker被放在一个或多个独立于“主代码”的代理中,故浏览器用一个或多个事件循环来处理给定类型的所有worker。
  • Worklet事件循环:
    用于驱动Worklet代理。包含Worklet、AudioWorklet、PaintWorklet。

线程

代码和浏览器的用户界面运行在同一个线程中,共享同一个事件循环,如果代码进行复杂的运算或者存在bug可能造成阻塞或者死循环都会导致性能降低,影响用户体验。
使用web worker可以让主线程另起新的线程来运行脚本,可以缓解阻塞问题。