Javascript中的事件循环javascript
javascript是一门单线程的非阻塞的脚本语言。单线程,即js代码在执行的任什么时候候,都只有一个主线程来处理全部任务。非阻塞,只要指的是执行异步任务(如I/O事件)时,主线程会挂起这个任务,而后在异步任务返回结果的时候再按照必定规则执行相应的回调。java
Web worker 技术所实现的多线程技术也存在诸多限制。如,全部新线程都受到主线程的彻底控制,不能独立执行。这意味着这些‘线程’其实是主线程的子线程。另外,这些子线程没有执行I/O操做的权限,只能为主线程分担一些如计算等任务。因此严格来说,web worker并无改变javascript的单线程本质。node
执行栈与存储对象指针和基础类型变量的栈是不一样的。执行栈是指,当调用一个方法时,js会生成与这个方法对应的一个执行环境(context),即执行上下文。这个执行环境中包含:这个执行环境的私有做用域、上层做用域的指向,方法的参数,私有变量以及该做用域的this指向。由于js是单线程的,同一时间只能执行一个方法,也就是说,当一个方法被执行的时候,其余方法会被排队到一个单独的地方,即执行栈。web
当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈,而后从头开始执行。当执行一个方法时,js会向执行栈中添加这个方法的执行环境,而后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码执行完毕并返回结果后,js会退出当前执行环境并撤销该环境,回到上一个方法的执行环境,这个过程反复执行,知道执行栈中的代码所有执行完毕。chrome
案列1:api
function Func1 () {浏览器
console.log(1)多线程
function Func2 () {异步
console.log(2)ui
function Func3 () {
console.log(3)
}
Func3()
}
Func2()
}
Func1()
// 1 2 3
同步执行遵循先进后出的规则,在执行Func1时,会向执行栈加入该方法的执行环境,输出1,而后解析了Func2,执行时加入了Func2的执行环境,输出2,而后解析Func3并执行,输出3,Func3执行完毕后会撤销Func3的执行环境,接着是Func2执行完毕并撤销Func2的执行环境,最后撤销Func1的执行环境。该过程若没有终止,会无限进行直到栈溢出。
方法执行时,异步执行事件挂起加入与执行栈不一样的另外一个队列,即事件队列中,并继续执行执行栈中的其余任务。被放入事件队列不会当即执行其回调,而是等待当前执行栈中的全部任务执行完毕,在主线程出于闲置状态时,主线程会查找事件队列是否有任务。若是有,则会取第一个事件并将该事件的回调放入执行栈中执行,而后执行其中的同步代码,如此反复就是事件循环。
异步任务由于各任务的不一样和执行优先级的区别,分为 宏任务 (macro task) 和 微任务 (micro task)
属于宏任务的事件:setTimeout(), setInterval()
属于微任务的事件:new Promise(), new MutaionObserver()(已废除)
当执行栈为空时,主线程会优先查看微任务是否有事件。若是没有,就会执行宏任务中的第一个事件并将对应的回调加入当前执行栈中;若是有,就会依次执行微任务中事件对应的回调,直到微任务队列为空,而后再执行宏任务中的第一个事件对应的回调,如此反复,进入循环。同一次事件循环中,微任务永远优先宏任务执行。
案列2:
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
// 2 3 1
node环境下的事件循环
在node中,事件循环与浏览器中的略有不一样。node中的事件循环的实现是依靠的libuv引擎。node选用chrome的v8引擎做为解释器,v8引擎将js代码解析后会调用node api,而api则是由libuv引擎驱动,所以node中的事件循环是在libuv引擎中执行。
node中,同步代码执行完,会先清空微任务队列,轮询时会清空当前队列全部任务,才会切换到下一个队列,在切换下一个队列以前也会先清空微任务队列。
(来自:node官网)
node的事件循环顺序:
外部输入数据—>poll阶段—>检查阶段(check)—>关闭事件回调阶段(close callback)—>定时器检测执行阶段(timers)—>I/O事件回调阶段(I/O callbacks)—>idle,prepare—>poll…
setTimeout(() => {console.log('setTimeout')} , 0)
setImmediate(() => {console.log('immediate')})
默认状况下setTimeout()和 setImmediate()不知道哪个会先执行,node执行也须要准备时间。setTimeout()延迟时间设置为0,实际仍是有4ms的延迟,假设node准备时间在4ms内,定时器没有执行,poll阶段没有执行setTimeout(),会先执行check中的setImmediate(),等到下一轮询进入时,poll检测到定时器已到时,再执行timer中的setTimeout()
队列中有一个特殊的推迟任务执行的方法process.nextTick再此执行。咱们知道,每一次事件循环都是从微任务开始的,而且每一阶段都是按照事件循环顺序进行执行。而在每一次的队列切换以前,都会检查nextTick queue中是否有事件,如有则优先执行。
案列3:
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
// 结果一
// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2
// 结果二
// setTimeout2, nextTick, setImmediate1, setImmediate2, setTimeout1
产生上面两种结果的缘由,是node准备时间的差别。
案例4:
const fs = require('fs');
fs.readFile(__filename, () => {
setImmediate(() => {
console.log("setImmediate1");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
});
setTimeout(() => {
process.nextTick(() => console.log("nextTick"));
console.log("setTimeout2");
setImmediate(() => {
console.log("setImmediate2");
});
}, 0);
});
// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2
此时只会有一种结果,由于是在一个I/O事件的回调中,node准备已结束,setTimeout执行须要等待4ms,setImmediate则当即执行,又setTimeout2和setTimeout1在同一个timers队列中因此按顺序执行,以后须要切换到check队列执行setImmediate2,在切换以前会先检查nextTick队列并执行,所以最后输出nextTick,setImmediate2
注:欢迎你们监督指导,若有疑问或错误,请留言一块儿探讨~~