-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
浏览器和NodeJS中不同的Event Loop #234
Comments
不错 |
你的demo肯定没有自己执行过,或者自己对着代码写一遍,因为你里面没有promise2这个东西,你把promise3和promise4,变成了promise2和promise3.。。。。。 |
我要研究一下 |
Node V11.X版本执行和浏览器执行保持一致 |
你好,『因此node作者推荐我们尽量使用setImmediate,因为它只在check阶段执行,不至于导致其他异步回调无法被执行到』 请问这句话应该如何理解呢? 也就是说, |
@dujuncheng 去查了一下源码,nodejs/node@460ee75#diff-e7ef4821107f4cae3bd0fea4dec350bf 用新版的话可以不用考虑这么多了吧 |
不好意思,很久没关注这个issue,现在回复。 |
可以看下你的Node版本哦,>=v11.0.0 的EL有所更新,可以参考下这个文章 New Changes to the Timers and Microtasks in Node v11.0.0 ( and above) |
目录
在文章开始之前,我们先来做道题目
下文中对这段代码会再做分析
答案戳
js是单线程的,EL机制实现异步
browsing contexts
EL在HTML规范中的定义
为了协调事件、用户交互、脚本、UI渲染、网络请求等行为,用户引擎必须使用Event Loop。EL包含两类:基于browsing contexts,基于worker。二者独立。
本文讨论的浏览器中的EL基于browsing contexts
图解Event Loop
task
一个EL中有 一个或多个 task队列。来自不同任务源的task会放入不同的task队列中:比如,用户代理会为鼠标键盘事件分配一个task队列,为其他的事件分配另外的队列。
task执行顺序是由进入队列的时间决定的,先进队列的先被执行。
典型的任务源有以下几种(Generic task sources):
DOM操作任务源:响应DOM操作
用户交互任务源:对用户交互作出反应,例如键盘或鼠标输入。响应用户操作的事件(例如click)必须使用task队列
网络任务源:响应网络活动
history traversal任务源:当调用history.back()等类似的api时,将任务插进task队列
task在网上也被成为
macrotask
可能是为了和microtask
做对照。但是规范中并不是这么描述任务的。除了上述task来源,常见的来源还有 数据库操作、
setTimeout/setInterval
等,可以概括为以下几种Microtask
一个EL中只有一个microtask队列,通常下面几种任务被认为是microtaskpromise
的then
和catch
才是microtask,本身其内部的代码并不是)EL循环过程
一个EL只要存在,就会不断执行下边的步骤:
简化一下上面的步骤,可以用下面的伪代码描述EL循环过程:
掌握了吗?在浏览器中运行文章开头的代码
运行结果:
过程分析:
node (version<=10)
个人理解,它与浏览器中的轮询机制(一个task,所有microtasks;一个task,所有microtasks…)最大的不同是,node轮询有phase(阶段)的概念,不同的任务在不同阶段执行,进入下一阶段之前执行process.nextTick() 和 microtasks。(以下概念性描述和例子均是对于<=10的node版本而言,node11在EL的处理上与浏览器趋同,可参考这篇文档 New Changes to the Timers and Microtasks in Node v11.0.0 ( and above)
Node事件轮询中的几个阶段
timers
在这个阶段检查是否有到达阈值的timer(setTimeout/setInterval),有的话就执行他们的回调
但timer设定的阈值不是执行回调的确切时间(只是最短的间隔时间),node内核调度机制和其他的回调函数会推迟它的执行
由poll阶段来控制什么时候执行timers callbacks
I/O callbacks
处理异步事件的回调,比如网络I/O,比如文件读取I/O。当这些I/O动作都结束的时候,在这个阶段会触发它们的回调。
idle, prepare内部使用,忽略poll
获取新的I/O事件,node会在适当的情况下阻塞在这里
为防止poll phase 耗尽 event loop,libuv 也有一个最大值(基于系统),会在超过最大值之后停止轮询更多的事件
由于其它各个阶段的操作都有可能导致新的事件发生,并使得内核向poll queue中添加事件,所以在poll阶段处理事件的时候可能还会有新的事件产生,最终,长时间的调用回调函数将会导致定时器过期,所以在poll阶段与定时器会有"合作"
poll阶段主要的两个功能:
进入poll阶段,timer的设定有下面两种情况:
setImmediate() callback
, event loop将结束poll阶段进入check阶段,并执行check queue (check queue是 setImmediate设定的)check
setImmediate
调度,EL会马上进入check phaseclose callbacks
看个例子~
循环过程
process.nextTick()
优先级:
nextTick
>microtask
|setTimeout/setInterval
>setImmediate
setTimeout 和 setImmediate 的区别
setImmediate
一旦当前poll阶段结束(poll queue为空或执行任务到达上限)就执行一次脚本setTimeout
设定一个最短的调度该脚本的时间阈值不在同一个I/O cycle中的时候,回调的调度顺序是不被保证的
在同一个I/O cycle中,
immediate
总比timeout
更早被调度process.nextTick()
process.nextTick()
不是Node的EL中的一部分(虽然它也是异步API),但是,任意阶段的操作结束之后nextTickQueue
就会被处理。nextTickQueue & microtasks
日常应用中经常会将 promise、`process.nextTick`、nextTickQueue、microtask 混为一谈,其实真正注册为 microtask 的任务的目前只有 promise。但是问题来了,v8 目前是没有暴露 `runMicrotasks` ,也就是说我们目前还没有办法通过内核的 API 执行 microtask queue 的任务。Node.js 最终选择的实现方法是将 microtask queue 的任务通过一个
runMicrotasks
对象暴露给上游,然后通过 nextTick 方法把它们推进了 nextTickQueue,也就是说最终 microtask queue 的任务变成了 nextTickQueue 的任务,所以我们用promise.then
和process.nextTick
可以实现相同的效果。process.nextTick() 和 setImmediate()
process.nextTick()
在当前循环阶段结束之前触发setImmediate()
在下一个事件循环中的check阶段触发通过
process.nextTick()
触发的回调也会在进入下一阶段前被执行结束,这会允许用户递归调用process.nextTick()
造成I/O被榨干,使EL不能进入poll阶段因此node作者推荐我们尽量使用setImmediate,因为它只在check阶段执行,不至于导致其他异步回调无法被执行到
掌握了吗:在浏览器中运行文章开头的代码
运行结果(执行demo的node版本为v8.*)
过程分析:
拓展
web worker
[【转向Javascript系列】深入理解Web Worker](http://www.alloyteam.com/2015/11/deep-in-web-worker/) [Web Worker浅识](http://note.youdao.com/noteshare?id=eb287f54753e456315c28cc9f1b17741)简单介绍:web worker是HTML5标准的一部分,将浏览器js线程分为主线程和worker线程,在主线程中,通过 new Worker()创建一个worker实例,参数是一个js文件。主线程和worker之间的通信是通过postMessage/onMessage的形式来做的,彼此发送数据,接受数据
MutationObserver
MutationObserver - Web API 接口 | MDN
执行栈
javaScript是单线程,也就是说只有一个主线程。主线程有一个栈,每一个函数执行的时候,都会生成新的execution context(执行上下文),执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中, running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。
简单的例子
执行栈的变化
更新渲染
在上面说到的浏览器的Event Loop 循环过程中,执行完microtask队列里的任务,有可能会渲染更新。这取决于在当下渲染是否“获益”。更新渲染在HTML的规范中包括了十二个步骤,大致是做5件事情:
1-4. 判断 document 在此时间点渲染是否会『获益』,是否有效。浏览器只需保证 60Hz 的刷新率即可(在机器负荷重时还会降低刷新率),若 eventloop 频率过高,一帧以内的多次dom变动浏览器不会立即响应,即使渲染了浏览器也无法及时展示。所以并不是每轮 eventloop 都会执行 UI Render。
5-9. 执行各种渲染所需工作,如 触发 resize、scroll 事件、建立媒体查询、运行 CSS 动画等等
10. 执行 request animation frame callbacks
11. 执行 IntersectionObserver callback
12. 渲染 UI
名词解释:
requestAnimationFrame是js绘制动画的API,注册callback,浏览器更新渲染时触发animate,animate触发cb
IntersectionObserver API 会注册一个回调方法,当期望监听的元素进入/退出另一个元素/浏览器视窗,或者是两个元素交集的部分大小发生变化,该回调就会被执行
参考
The text was updated successfully, but these errors were encountered: