事件循环:
javascript是一门单线程的非阻塞的脚本语言。js为什么是单线程的语言?是因为js的执行引擎只有一个线程,不会在执行期间开启新的线程,而非浏览器是单线程的。浏览器是多线程的。
Event Loop是javascript的执行机制,也是js实现异步的一种方法。
单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务,如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。保证了程序执行的一致性。不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的
而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
****事件循环**:js主线程有一个执行栈,所有js代码都会在执行栈里运行,当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,如果碰到一些异步代码(比如setTimeout,ajax,promise.then以及用户点击等操作),浏览器就会把这些代码交给异步线程去执行,主线程继续执行栈中的代码,当异步线程里的代码执行完成后该线程就会将它的异步回调函数放到任务队列(又称事件队列,消息队列)中等待执行。当主线程执行完栈中的所有代码后,它就会检查任务队列是否有任务要执行,如果有任务要执行就将该任务放到执行栈中去执行。如果当前任务队列为空就会一直等待任务到来。
**执行宏任务执行过程中出现了微任务,会去执行微任务。**执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。
常见宏任务有:
script
(可以理解为外层同步代码)、ajax请求回调setTimeout/setInterval
setImmediate
(Node.js)
常见微任务有:
Promise
、await(promise的语法糖,返回一个promise对象)process.nextTick
(Node.js)MutaionObserver
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。Node的Event Loop是阶段的转移过程。
六个阶段:
timers: 执行setTimeout
和setInterval
的回调
pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
idle, prepare: 仅系统内部使用
poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。(等待)
check: setImmediate
在这里执行
close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)
每个阶段都有一个自己的先进先出的队列,只有当这个队列的事件执行完或者达到该阶段的上限时,才会进入下一个阶段。在每次事件循环之间,Node.js都会检查它是否在等待任何一个I/O或者定时器,如果没有的话,程序就关闭退出了。
setImmediate
和setTimeout
除了第一次的启动不确定之外,正常情况下处于poll阶段时,setImmediate
会比定时器先执行:
console.log('outer');
setTimeout(() => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
}, 0);
外层是一个
setTimeout
,所以执行他的回调的时候已经在timers
阶段了处理里面的
setTimeout
,因为本次循环的timers
正在执行,所以他的回调其实加到了下个timers
阶段处理里面的
setImmediate
,将它的回调加入check
阶段的队列外层
timers
阶段执行完,进入pending callbacks
,idle, prepare
,poll
,这几个队列都是空的,所以继续往下到了
check
阶段,发现了setImmediate
的回调,拿出来执行然后是
close callbacks
,队列是空的,跳过又是
timers
阶段,执行我们的console
理解成正常情况下,先执行check,再执行timer。
console.log('outer');
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
我们来运行下看看效果:
好像是setTimeout
先输出来,我们多运行几次看看:
两者之间的出现的顺序不一定。node.js里面setTimeout(fn, 0)
会被强制改为setTimeout(fn, 1)
,这在官方文档中有说明。(说到这里顺便提下,HTML 5里面setTimeout
最小的时间限制是4ms)。
- 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
- 遇到
setTimeout
,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times
阶段- 遇到
setImmediate
塞入check
阶段- 同步代码执行完毕,进入Event Loop
- 先进入
times
阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout
条件,执行回调,如果没过1毫秒,跳过- 跳过空的阶段,进入check阶段,执行
setImmediate
回调
通过上述流程的梳理,我们发现关键就在这个1毫秒,如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout
执行,如果1毫秒还没到,就先执行了setImmediate
。每次我们运行脚本时,机器状态可能不一样,导致运行时有1毫秒的差距,一会儿setTimeout
先执行,一会儿setImmediate
先执行。**但是这种情况只会发生在还没进入timers
阶段的时候。**像我们第一个例子那样,因为已经在timers
阶段,所以里面的setTimeout
只能等下个循环了,所以setImmediate
肯定先执行。
同理的还有其他poll
阶段的API也是这样的,
var fs = require(‘fs’)
fs.readFile(__filename, () => {
setTimeout(() => {
console.log(‘setTimeout’);
}, 0);
setImmediate(() => {
console.log(‘setImmediate’);
});
});
这里setTimeout
和setImmediate
在readFile
的回调里面,由于readFile
回调是I/O操作,他本身就在poll
阶段,所以他里面的定时器只能进入下个timers
阶段,但是setImmediate
却可以在接下来的check
阶段运行,所以setImmediate
肯定先运行,他运行完后,去检查timers
,才会运行setTimeout
。
process.nextTick()
process.nextTick()
是一个特殊的异步API,他不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick()
,这个执行完后才会继续Event Loop。(在当前阶段结束,准备跳到下一个阶段之前去执行process.nexttick()函数)
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(() => {
console.log('nextTick 2');
});
});
process.nextTick(() => {
console.log('nextTick 1');
});
});
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hKqJ2bko-1602035479932)(C:\Users\shao\AppData\Roaming\Typora\typora-user-images\image-20201006204427255.png)]
我们代码基本都在readFile
回调里面,他自己执行时,已经在poll
阶段
遇到setTimeout(fn, 0)
,其实是setTimeout(fn, 1)
,塞入后面的timers
阶段
遇到setImmediate
,塞入后面的check
阶段
遇到nextTick
,立马执行,输出’nextTick 1’
到了check
阶段,输出’setImmediate’,又遇到个nextTick
,立马输出’nextTick 2’
到了下个timers
阶段,输出’setTimeout’
https://juejin.im/post/6844904100195205133#heading-11
settimeout的理解
setTimeout
这个函数,是经过指定时间后,把要执行的任务加入到Event Queue中,又因为是单线程任务要一个一个执行,如果事件队列中前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于设定的时间。
setTimeout(fn,0)
的含义是,不用等待进入时间宏队列中,栈为空就马上执行。
setInterval
是循环的执行。对于执行顺序来说,setInterval
会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于setInterval(fn,ms)
来说,我们已经知道不是每过ms
秒会执行一次fn
,而是每过ms
秒,会有fn
进入Event Queue。一旦**setInterval
的回调函数fn
执行时间超过了延迟时间ms
,那么就完全看不出来有时间间隔了**。
node.js中的事件循环