事件循环 Event Loop
定义
事件循环是单线程
的 JavaScript 在处理异步
事件时进行的一种循环过程
。
背景
JavaScript 是以单线程的方式运行的
- 同一时刻只能执行特定的任务,而浏览器是多线程的
- JavaScript 为了避免复杂性,而实现单线程执行
可这样简单理解 JS 的单线程:
- 从前到后,一行行执行
- 如果某行执行报错,则停止后续代码执行 (同步阻塞)
- 先把同步代码执行完,再执行异步
做了什么
Event Loop 解决了 JavaScript 作为单线程语言时的并发性问题,其执行过程如下:
- 同步代码,放到调用栈,依次执行完
- 期间若有异步代码,标记并放到任务队列等待时机执行
- 无同步代码(调用栈为空),检查任务队列:
- 如果调用栈为空(同步代码执行完)Event Loop 开始工作
- 轮询查找调用队列,若有待执行事件则移动到调用栈执行
- 只要有空闲就不断轮询查找
- 重复以上步骤形成事件循环
这只是把事件循环这个概念说了,很多重点还没浮出水面。答到这,面试官肯定不会满意,所以你还需要了解以下概念,顶住下一轮深挖。
需要了解的几个概念
主线程 Main thread
所有的同步任务都是在主线程里执行的。
主线程用于浏览器处理用户事件和页面绘制等。默认情况下,浏览器在一个线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。
- 同步任务: 指在主线程上排队等待执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 异步任务: 只有引擎认为某个异步任务可以执行了,该任务(采用回调函数的形式)才会进入主线程执行
宏任务 macro task
指的是浏览器在执行代码的过程中会调度的任务,比如事件循环中的每一次迭代、setTimeout 和 setInterval 等。 宏任务会在浏览器完成当前同步任务之后执行。
宏任务(Macrotasks)是一些较大粒度的任务:
- 所有同步任务
- script
待执行脚本
- I/O,如文件读写,数据库数据读写等
- setTimeout、setInterval
- setImmediate
Node环境
- requestAnimationFrame
- 事件监听,回调函数等
- UI render
- ...
微任务 microtask
本质就是一个待调用的 function,当创建该微任务的函数执行之后,并且只有当 Javascript 调用栈为空,而控制权尚未返还给被用户代理用来驱动脚本执行环境的事件循环之前,该微任务才会被执行。
微任务(Microtasks)是一些较小粒度、高优先级的任务
- Promise
- async / await
- Generator 函数
- mutationObserver
html5 API
- process.nextTick
Node环境
- ...
展开来说
事件循环是单线程的 JavaScript 在处理异步事件时进行的一种循环过程,具体来讲,对于异步事件它会先加入到事件队列中挂起,等主线程空闲时会去执行事件队列中的事件。
在同一轮任务队列中,同一个微任务产生的微任务会放在这一轮微任务的后面,产生的宏任务会放在这一轮的宏任务后面。 在同一轮任务队列中,同一个宏任务产生的微任务会马上执行,产生的宏任务会放在这一轮的宏任务后面
主线程任务——>微任务——>宏任务
如果宏任务里还有微任就继续执行宏任务里的微任务,如果宏任务中的微任务中还有宏任务就在依次进行
主线程任务——>微任务——>宏任务——>宏任务里的微任务——>宏任务里的微任务中的宏任务——>直到任务全部完成
它不停检查 Call Stack 中是否有任务(也叫栈帧)需要执行,如果没有,就检查 Event Queue,从中弹出一个任务,放入 Call Stack 中,如此往复循环。
梳理:事件循环流程
- 主线程读取 JavaScript 代码,形成相应的堆和执行栈。
- 当主线程遇到异步任务时,将其委托给对应的异步进程(如 Web API)处理。
- 异步任务完成后,将相应的回调函数推入任务队列。
- 主线程执行完同步任务后,检查任务队列,如果有任务,则按照先进先出的原则将任务推入主线程执行。
- 重复执行以上步骤,形成事件循环。
梳理:任务队列执行过程
首先,必须要明确,在 JavaScript 中,所有任务都在主线程上执行。任务执行过程分为同步任务和异步任务两个阶段。异步任务的处理经历两个主要阶段:Event Table(事件表)
和 Event Queue(事件队列)
。
Event Table 存储了宏任务的相关信息,包括事件监听和相应的回调函数。当特定类型的事件发生时,对应的回调函数被添加到事件队列中,等待执行。例如,你可以通过 addEventListener 来将事件监听器注册到事件表上:
任务队列的执行流程可概括为:
- 同步任务在主线程排队执行,异步任务在事件队列排队等待进入主线程执行。
- 遇到宏任务则推进宏任务队列,遇到微任务则推进微任务队列。
- 执行宏任务,执行完毕后检查当前层的微任务并执行。
- 继续执行下一个宏任务,执行对应层次的微任务,直至全部执行完毕。
这个流程确保了异步任务能够在适当的时机插入执行,保持程序的高效性和响应性。
其他
为什么先微后宏
微任务会在执行任何其他事件处理,或渲染,或执行任何其他宏任务之前完成。
这很重要,因为它确保了微任务之间的应用程序环境基本相同(没有鼠标坐标更改,没有新的网络数据等)。
如果我们想要异步执行(在当前代码之后)一个函数,但是要在更改被渲染或新事件被处理之前执行,那么我们可以使用 queueMicrotask
来对其进行安排(schedule)
Node.js 和 浏览器 eventLoop 的区别
两者最主要的区别在于:
- 浏览器中的微任务是在每个相应的宏任务中执行的
- Node.js 中的微任务是在不同阶段之间执行的
执行顺序
微任务队列优先于宏任务队列执行;
微任务队列上创建的宏任务会被后添加到当前宏任务队列的尾端;
微任务队列中创建的微任务会被添加到微任务队列的尾端;
只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行;
只有运行完 await 语句,才把 await 语句后面的全部代码加入到微任务行列;
在遇到 await promise 时,必须等 await promise 函数执行完毕才能对 await 语句后面的全部代码加入到微任务中;
在等待 await Promise.then 微任务时:
- 运行其他同步代码;
- 等到同步代码运行完,开始运行 await promise.then 微任务;
- await promise.then 微任务完成后,把 await 语句后面的全部代码加入到微任务行列;
参考资料
- 关于 JavaScript 单线程的一些事 ——JChehe
- 一看就懂的事件循环机制(event loop)——藤原托漆
- 深入 JS 执行原理:一文搞定 EventLoop、宏任务、微任务——程序员Sunday