javascript是一门单线程语言,按照语句出现的顺序执行的。
执行和运行
JavaScript
的执行和运行是两个不同概念的,执行,一般依赖于环境,比如 node
、浏览器、Ringo
等, JavaScript 在不同环境下的执行机制可能并不相同。而Event Loop
就是 JavaScript
的一种执行方式。而运行,是指JavaScript 的解析引擎。这是统一的。
事件循环(Event Loop)
JavaScript
有一个主线程 main thread
,和调用栈 call-stack
也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
JavaScript
的 Event Loop
是伴随着整个源码文件生命周期的,只要当前 JavaScript
在运行中,内部的这个循环就会不断地循环下去,去寻找 queue
里面能执行的 task
。
任务队列(task queue)
task
,就是任务的意思,可以理解为每一个语句就是一个任务。
queue
,就是FIFO
的队列。
Task Queue
就是承载任务的队列。而 JavaScript
的 Event Loop
就是会不断地查找这个 queue
,看有没有 task
可以运行。
同步任务(SyncTask)、异步任务(AsyncTask)
同步任务:就是主线程来执行的时候立即就能执行的代码
异步任务:就是先去执行别的 task,等所有task执行完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行
setTimeout(() => {
task()
},3000)
sleep(100000)
执行上面的代码,执行task()的时间远远大于3秒。
看看上述代码的执行
task()
进入Event Table并注册,计时开始。- 执行
sleep
函数,继续计时。 - 3秒到了,计时事件
timeout
完成,task()
进入Event Queue,等待sleep
执行完成。 sleep
执行完毕,task()
从Event Queue进入主线程执行。此时计时时间将大于3秒
setInterval()的执行效果同理。
再看一个ajax请求
ajax({
url:www.xxx.com,
data:prams,
success:() => {
console.log('请求成功!');
},
error:()=>{
console.log('请求失败~');
}
})
console.log('这是一个同步任务');
- ajax 请求首先进入到
Event Table
,分别注册了onError
和onSuccess
回调函数。 - 主线程执行同步任务:
console.log('这是一个同步任务');
- 主线程任务执行完毕,看
Event Queue
是否有待执行的 task,这里是不断地检查,只要主线程的task queue
没有任务执行了,主线程就一直在这等着 - ajax 执行完毕,将回调函数
push
到Event Queue
。(步骤 3、4 没有先后顺序而言) - 主线程“终于”等到了
Event Queue
里有task
可以执行了,执行对应的回调任务。 - 如此往复。
宏任务(MacroTask)、微任务(MicroTask)
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout
和setInterval
会进入相同的Event Queue。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 这段代码作为宏任务,进入主线程。
- 先遇到
setTimeout
,将其回调函数注册后分发到宏任务Event Queue。 - 接下来遇到了
Promise
,new Promise
立即执行,then
函数分发到微任务Event Queue。 - 遇到
console.log()
,立即执行。 - 整体代码script作为第一个宏任务执行结束,查询微任务,执行在微任务Event Queue中的
then
。 - 第一轮事件循环结束,开始第二轮循环,从宏任务Event Queue开始。遇到宏任务Event Queue中
setTimeout
对应的回调函数,立即执行。 - 结束。
promise、process.nextTick与async/await
Promise中的异步体现在then
和catch
中,所以写在Promise中的代码是被当做同步任务立即执行的。
await实际上是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码;
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
第一轮事件循环流程:
- 整体script作为第一个宏任务进入主线程,遇到
console.log
,输出1。 - 遇到
setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。 - 遇到
process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。 - 遇到
Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。 - 又遇到了
setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
-
上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
-
我们发现了
process1
和then1
两个微任务。 - 执行
process1
,输出6。 - 执行
then1
,输出8。
第一轮时间循环结束,输出1,7,6,8。
第二轮时间循环从setTimeout1
宏任务开始:
首先输出2。接下来遇到了process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
- 第二轮事件循环宏任务结束,我们发现有
process2
和then2
两个微任务可以执行。 - 输出3。
- 输出5。
第二轮事件循环结束,第二轮输出2,4,3,5。
第三轮事件循环开始,此时只剩setTimeout2了,执行。
- 直接输出9。
- 将
process.nextTick()
分发到微任务Event Queue中。记为process3
。 - 直接执行
new Promise
,输出11。 - 将
then
分发到微任务Event Queue中,记为then3
。
宏任务Event Queue | 微任务Event Queue |
---|---|
process3 | |
then3 |
- 第三轮事件循环宏任务执行结束,执行两个微任务
process3
和then3
。 - 输出10。
- 输出12。
- 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
PS:此处是JavaScript执行环境下的执行过程
Node 环境下的 Event Loop
Node.js的运行机制
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户。
Node中的Event Loop
是基于libuv
实现的,而libuv
是 Node 的新跨平台抽象层,libuv
使用异步,事件驱动的编程方式,核心是提供i/o
的事件循环和异步回调。libuv
的API
包含有时间,非阻塞的网络,异步文件操作,子进程等等。
区分:nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。
Event Loop运作机制
上述的五个阶段都是按照先进先出的规则执行回调函数。按顺序执行每个阶段的回调函数队列,直至队列为空或是该阶段执行的回调函数达到该阶段所允许一次执行回调函数的最大限制后,才会将操作权移交给下一阶段。
- timers:执行
setTimeout()
和setInterval()
中到期的callback。 - I/O callback:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks
- idle, prepare:仅node内部使用
- poll: 最为重要的阶段,获取新的I/O事件,在适当的条件下会阻塞在这个阶段
- check: 执行
setImmediate
的callback - close callbacks: 执行
close
事件的callback,例如socket.on('close'[,fn])
、http.server.on('close, fn)
在每次运行事件循环之间,node.j检查是否有正在等待的异步i/o调用、timers等。如果没有,就清除并结束(退出程序),例如:执行一个程序,仅有一句话(var a= ‘hello’;),处理完目标代码后,不会进入evetloop,而是直接结束程序。
PS:上面六个阶段都不包括 process.nextTick()
重要的三阶段:
timers,定时器阶段:
执行定时任务(setTimeOut(), setInterval())
poll 轮询阶段:
处理到期的定时器任务,然后(因为最开始阶段队列为空,一旦队列为空,就会检查是否有到期的定时器任务)
处理队列任务,直到队列空,或达到上限
如果队列为空:如果setImmediate,终止轮询阶段,进入检查阶段执行。如果没setImmediate,查看有没有定时器任务到期,有的话就到timers阶段,执行回调函数.
check 检查阶段
轮询阶段空闲,且有setImmediate的时候,进入检查阶段
不重要二阶段
- I/O callbacks阶段:处理I/O异常错误
- close callbacks阶段: 处理各种close事件回调
从event loop机制的角度上区分process.nextTick()与setImmediate()
setImmediate()
和 setTimeout()
非常的相似,区别取决于谁调用了它。
setImmediate
在 poll 阶段后执行,即check 阶段setTimeout
在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段
计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是从主模块中调用的,则时序将受到进程性能的限制。
例如,如果我们运行以下不在I / O
周期(即主模块)内的脚本,则两个计时器的执行顺序是不确定的,因为它受进程性能的约束:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
如果在一个I/O
周期内移动这两个调用,则始终首先执行立即回调:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
所以与setTimeout()
相比,使用setImmediate()
的主要优点是,如果在I / O
周期内安排了任何计时器,则setImmediate()
将始终在任何计时器之前执行,而与存在多少计时器无关。
nextTick queue
process.nextTick()
从技术上讲不是Event Loop的一部分。 相反,无论当前事件循环的当前阶段如何,都将在当前操作完成之后处理nextTickQueue。
如果存在 nextTickQueue
,就会清空队列中的所有回调函数,并且优先于其他 microtask
执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
Node与浏览器的 Event Loop 差异
浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。
在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。