Open ccforward opened 7 years ago
great!
好奇,文中的图片 左边的编辑器师怎么做的
@zhanglun 左边其实就是 sublime 截的图
好文 正好时间循环机制不是很了解
写的很详细,赞。 希望以后加点,复杂的一些流程流程中转的话,node是如何处理的~
学习了事件循环
为什么在 Cycle2 中 setInterval 和 setTimeout 同样是 macrotask,但是先执行 setInterval 呢?
“microtasks 队列清空” 这个过程应该在每个cycle的结尾,更加严格讲是每个task执行完毕后从task队列中移除。
这个sublime主题很好看,可以搜到么
@Aaaaaaaty 主题是我自己改的颜色
@ccforward 啊这样 谢谢啦 文章好nice 持续关注中~
我有一个问题, Promise.then 里的 callback 是 microtask 吗? 这个不应该是 macrotask 吗?
@mrcodehang 是microtask,可以参考这篇文章,promise都应该是microtask,在一个次循环后串行打印结果,只是有的环境将promise callback解释为macrotask而不是microtask导致打印顺序出了问题。至于为什么环境之间对它的解释不一样,这似乎涉及到ECMA和HTML之间的一些标准界定了=。=毕竟厂商们都是自己按照标准来撸一套环境
@ccforward 謝謝樓主分享如此詳盡的比較與分析。
我了解到 macrotask === task !== microtask
,但有個問題想請教一下樓主:
目前我除了可以在這份文件 找到 macro-task
這個名詞以外,似乎在其他正式文件 (如 WHATWG) 裡頭,還是以 task
稱呼之,所以想請教下, macrotask
這個名詞的起源為何呢?
这段代码在chrome console中执行,第二个setInterval输出了两次,是什么情况?
请教下作者 async/await 属不属于microtasks
@lawpachi async/await 相当于是 Promise。应该是属于 microtasks
手动夸一夸作者,整理的很赞~
这个例子很方便理解!有个疑问:第9步是否属于Cycle 5?
我想请教,分为两个任务队列的原因。
@justahole 并不能理解成只有两个任务队列,而是两种类型的任务队列。
恩,我知道,但是这样设计的目的是啥
掬一捧 notifications@github.com 于 2019年3月22日周五 下午6:19写道:
@justahole https://github.com/justahole 并不能理解成只有两个任务队列,而是两种类型的任务队列。
- microtask 类型:会在一次事件循环中清空掉该队列中所有的任务。
- macrotask 类型:在一次事件循环中只会执行该队列中最前面的那个任务,剩下的任务在后续的事件循环中进行。
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ccforward/cc/issues/47#issuecomment-475566036, or mute the thread https://github.com/notifications/unsubscribe-auth/AaBDq_F-DvqD0Hz5f5RhOpKu_UHpid43ks5vZK5FgaJpZM4Kosox .
Node.js 事件循环一: 浅析
多数的网站不需要大量计算,程序花费的时间主要集中在磁盘 I/O 和网络 I/O 上面
SSD读取很快,但和CPU处理指令的速度比起来也不在一个数量级上,而且网络上一个数据包来回的时间更慢:
一个数据包来回的延迟平均320ms(我网速慢,ping国内网站会更快),这段时间内一个普通 cpu 执行几千万个周期应该没问题
因此异步IO就要发挥作用了,比如用多线程,如果用 Java 去读一个文件,这是一个阻塞的操作,在等待数据返回的过程中什么也干不了,因此就开一个新的线程来处理文件读取,读取操作结束后再去通知主线程。
这样虽然行得通,但是代码写起来比较麻烦。像 Node.js V8 这种无法开一个线程的怎么办?
先看下面函数执行过程
栈 Stack
当我们调用一个函数,它的地址、参数、局部变量都会压入到一个 stack 中
函数
fire
首先被调用fire
调用sumSqrt
函数 参数为3和4之后调用
square
参数为 x, x==3当
square
执行结束返回时,从 stack 中弹出,并将返回值赋值给 s1s1加入到 sumSqrt 的 stack frame 中
以同样的方式调用下一个
square
函数在下一行的表达式中计算出 s1+s2 并赋值给 sum
之后调用
Math.sqrt
参数为sum现在就剩下
sumSqrt
函数返回计算结果了返回值赋值给 result
在 console 中打印出 result
最终
fire
没有任何返回值 从stack中弹出 stack也清空了当函数执行完毕后本地变量会从 stack 中弹出,这只有在使用 numbers string boolean 这种基本数据类型时才会发生。而对象、数组的值是存在于 heap(堆) 中的,stack 只存放了他们对应的指针。
当函数之行结束从 stack 中弹出来时,只有对象的指针被弹出,而真正的值依然存在 heap 中,然后由垃圾回收器自动的清理回收。
事件循环
通过一个例子来了解函数的执行顺序
请求
http://localhost:5000/
后打印出虽然 V8 是单线程的,但底层的 C++ API 却不是。这意味着当我们执行一些非阻塞的操作,Node会调用一些代码,与引擎里的js代码同时执行。一旦这个隐藏的线程收到了等待的返回值或者抛出一个异常,之前提供的回调函数就会执行。
上面的说的Node调用的一些代码其实就是 libuv,一个开源的跨平台的异步 I/O 。最初就是为 Node.js 开发的,现在很多项目都在用
任务队列
javascript 是单线程事件驱动的语言,那我们可以给时间添加监听器,当事件触发时,监听器就能执行回调函数。
当我们去调用
setTimeout
http.get
fs.readFile
, Node.js 会把这些定时器、http、IO操作发送给另一个线程以保证V8继续执行我们的代码。然而我们只有一个主线程和一个 call-stack ,这样当一个读取文件的操作还在执行时,有一个网络请求request过来,那这时他的回调就需要等stack变空才能执行。
回调函数正在等待轮到自己执行所排的队就被称为任务队列(或者事件队列、消息队列)。每当主线程完成前一个任务,回调函数就会在一个无限循环圈里被调用,因此这个圈被称为事件循环。
我们前面那个获取文章的例子的执行顺序就会如下:
request
事件触发getArticle
getArticle
压入(push) stackfetchArticle
被调用 同时压入 stackMath.floor
和Math.random
被调用压入 stack 然后再 弹出(pop), 从 aids 里面取出的一个值被赋值给变量 aidsuperagent.get
被执行,参数为'http://news-at.zhihu.com/api/4/news/${aid}'
,并且回调函数注册给了end
事件http://news-at.zhihu.com/api/4/news/${aid}
的HTTP请求被发送到后台线程,然后函数继续往下执行'Now is fetching an article'
打印在 console 中。 函数fetchArticle
返回print
函数被调用,'Print something'
打印在 console 中getArticle
返回,并从 stack 中弹出, stack 为空http://news-at.zhihu.com/api/4/news/${aid}
发送相应信息end
事件被触发end
事件的匿名回调函数被执行,这个匿名函数和他闭包中的所有变量压入 stack,这意味着这个匿名函数可以访问并修改express
,superagent
,app
,aids
,req
,res
,aid
的值以及之前所有已经定义的函数res.send()
伴随着 200 或 500 的状态码被执行,但同时又被放入到后台线程中,因此 响应流 不会阻塞我们函数的执行。匿名函数也被 pop 出 stack。Microtasks Macrotasks
任务队列不止一个,还有 microtasks 和 macrotasks
microtasks:
macrotasks:
这两个的详细区别下一篇再写,先看一段代码
理解了node的事件循环还是比较容易得出答案的:
根据 WHATVG 的说明,在一个事件循环的周期(cycle)中一个 (macro)task 应该从 macrotask 队列开始执行。当这个 macrotask 结束后,所有的 microtasks 将在同一个 cycle 中执行。在 microtasks 执行时还可以加入更多的 microtask,然后一个一个的执行,直到 microtask 队列清空。
规范理解起来有点晦涩,来看下上面的例子
Cycle 1
1)
setInterval
被列为 task2)
setTimeout 1
被列为 task3)
Promise.resolve 1
中两个then
被列为 microtask4) stack 清空 microtasks 执行
任务队列:
setInterval
setTimeout 1
Cycle 2
5) microtasks 队列清空
setInteval
的回调可以执行。另一个setInterval
被列为 task , 位于setTimeout 1
后面任务队列:
setTimeout 1
setInterval
Cycle 3
6) microtask 队列清空,
setTimeout 1
的回调可以执行,promise 3
和promise 4
被列为 microtasks7)
promise 3
和promise 4
执行。setTimeout 2
被列为 task任务队列
setInterval
setTimeout 2
Cycle 4
8) microtask 队列清空
setInteval
的回调可以执行。然后另一个setInterval
被列为 task ,位于setTimeout 2
后面任务队列:
setTimeout 2
setInterval
9)
setTimeout 2
的回调执行,promise 5
和promise 6
被列为 microtasks现在
promise 5
和promise 6
的回调应该执行,并且 clear 掉interval
。 但有的时候不知道为什么setInterval
还会在执行一遍,变成下面结果但是把上面的代码放入 chrome console 中执行却没有问题。这一点还要再根据不同的 node版本 查一下。
Last
这篇只是对 事件循环 的浅析和总览,后面再继续深入的研究。