代码已经关联到github: 链接地址 文章有更新也会优先在这,觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
浏览器为什么有事件循环
javascript 从诞生之日起就是一门单线程的非阻塞的脚本语言。
- 单线程:解析和执行
JavaScript
代码的线程只有一个主线程 - 非阻塞:当我们的
Javascript
代码运行一个异步任务的时候(像Ajax
从网络读取数据等),主线程会挂起这个任务,然后异步任务返回结果的时候再根据特定的结果去执行相应的回调函数。
也正是javascript的这两个特点,需要一种执行同步任务、且在异步任务及时按需执行的机制,这就是事件循环。
Event Loop(事件循环)
事件循环是用来协调事件、用户交互、脚本、渲染、网络的一种浏览器内部机制。一般我们所指的事件循环是指渲染过程中的脚本执行的机制,也就是浏览器一个渲染进程内主线程所控制的循环。
task quenes(任务队列)
一个Event loop
有一个或多个task quenes
。task quenes
是一组tasks
的集合。
任务队列是集合,而不是队列,因为事件循环处理模型的第一步是从所选队列中获取第一个可运行的任务,而不是将第一个任务出队。
task and microtask(宏任务和微任务)
任务队列里的任务也分为不同的类型,常见的有宏任务
和微任务
,与宏任务取第一个可运行的任务不同,微任务所处的是一个先进先出的队列。
(为什么要区分宏任务和微任务:在下一次 Event loop 之前进行插队)
一个Event Loop,Microtask 是在 Macrotask 之后调用,Microtask 会在下一个Event Loop 之前执行调用完,并且其中会将 Microtask 执行当中新注册的 Microtask 一并调用执行完,然后才开始下一次 Event loop,所以如果有新的 Macrotask 就需要一直等待,等到上一个 Event loop 当中 Microtask 被清空为止。由此可见, 我们可以在下一次 Event loop 之前进行插队。
作者:evan
链接:https://www.zhihu.com/question/316514618/answer/1311354630
宏任务(macro-task)
常见的 :setTimeout
、setInterval
、setImmediate
、 script
(整体代码)、 I/O
操作、UI 交互事件、postMessage
、requestAnimationFrame
等。
微任务(micro-task)
没有明确指定,但是通常认为 :new Promise().then(callback)
、MutationObserve
、await
和 process.nextTick(NodeJS
等。
Event Loop 处理过程
执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
Event Loop
的循环过程简略版如下:
- 从
task quene
取出一个可执行的宏任务,放入到执行栈中去执行,如果没有可选的宏任务,则直接跳到微任务处理步骤 - 执行一个宏任务完成后,就需要检测微任务队列有没有需要执行的任务,有的话,全部执行,注意中间添加的微任务也会继续执行,直到所有的微任务执行完
- 设置
hasARenderingOpportunity
为 false,该参数主要是判断当前界面是否有渲染机会,浏览器会根据硬件(如刷新率)和其他因素(页面性能和页面不可见等)。 - 更新渲染
- 处理document中的对象,标记需要渲染的对象,排除不需要渲染和不受影响的对象。
- 处理完后,如果document中需渲染的对象不为空,设置
hasARenderingOpportunity
为 true,浏览器会在合适的时机去重新渲染。
如果无需渲染,则后续的渲染相关步骤都不会触发。
- 对于需要渲染的文档,窗口大小变化则触发
resize
事件 - 对于需要渲染的文档,窗口滚动则触发
scroll
事件 - 对于需要渲染的文档,媒体查询变化
matchMedia
- 对于需要渲染的文档,执行动画帧回调
requestAnimationFrame
- 对于需要渲染的文档,执行
IntersectionObserver
回调 - 对于需要渲染的文档,重新渲染界面
- 如果当前
task queues
里没有task
且microtask queue
是空的,同时渲染时机变量hasARenderingOpportunity
为 false ,去执行 idle period(requestIdleCallback
) - 返回第一步
下图(源)是chrome对于事件循环处理的完整逻辑:
![image.png](https://img-blog.csdnimg.cn/img_convert/0c8fd1f258d65d41ede3e83df3e86a05.png#clientId=u5c80bfa4-5cb8-4&from=paste&height=791&id=uff6f0407&margin=[object Object]&name=image.png&originHeight=1582&originWidth=2282&originalType=binary&ratio=1&size=1727023&status=done&style=none&taskId=u23102343-59d2-4089-a07f-5f1e11a892f&width=1141)
从规范中可以注意到,有两个函数比较特殊:一个是算作宏任务的requestAnimationFrame(rAF)
,另外一个是requestIdleCallback(rIC)
。
requestAnimationFrame
rAF
是官方推荐的用来做一些流畅动画所应该使用的 API,其特点为:
- 在界面重新渲染前调用
- 由系统决定回调的时机
比如一般显示器刷新率为60HZ,则每秒最多绘制60此,也就是1000ms / 60 ≈ 16.7ms
执行一次
- 页面不可见时,不执行动画任务
requestIdleCallback
在闲时进行事件的处理,可以用来处理一些优先级低但是耗性能的任务。其特点为:
- 在界面渲染完毕且有空闲的时机调用
- 不可见时,执行频率会降低到10秒一次甚至更低
- 第二个参数
timeout
,可以指定该任务在该时间后一定会执行,不会再等待空闲。 - 如果浏览器未产生任何交互和渲染,该函数的触发时间间隔大概为
50ms
//可在稳定的浏览器界面不断输入该代码查看间隔
requestIdleCallback((deadline)=>{
console.log(deadline.timeRemaining())
})
NodeJS的事件循环顺序
Node11 之后一些特性已经向浏览器看齐了,两者最主要的区别是:
- 浏览器中的宏任务执行完毕后,会执行所有微任务;
- 而Node中除了宏任务与微任务,还有其他任务(
setImmediate
、IO的延迟回调
和process.nextTick
等),所以Node的事件循环是多阶段的,一旦一个阶段执行完毕就立刻执行对应的微任务队列。(如果我们只看Node宏任务与微任务,则与浏览器一致,执行完宏任务就执行微任务)
Node的不同阶段及事件循环
timers
:此阶段执行由setTimeout
和setInterval
设置的回调。pending callbacks
:执行推迟到下一个循环迭代的 I/O 回调。idle, prepare
, :仅在内部使用。poll
:取出新完成的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调,计时器调度的回调和setImmediate
之外,几乎所有这些回调)。适当时Node 将在此处阻塞。check
:在这里调用setImmediate
回调。close callbacks
:一些关闭回调,例如 socket.on(‘close’, …)。
事件循环如下图(源):
Node11前后差异
Node11及之后会在宏任务执行完毕后会直接执行微任务;而Node11之前的可能出现例外,比如timer
阶段第一个定时器执行完毕了,第二个定时器也恰好到时间,这时候会优先执行定时器,check
阶段有多个setImmediate
也是如此,也就是说Node11之前,在某个阶段存在的可执行事件都会优先执行,而后才执行微任务。