JavaScript一门是单线程的脚本语言,为什么说它是单线程的呢?
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它是单线程,否则会带来很复杂的同步问题。举个例子,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程。但是,H5中的Web Works可以开启分线程。
Event Loop即事件循环,是指浏览器或Node的一种解决JavaScript单线程运行时不会阻塞的一种机制,是实现异步的一种方法。
在js中,任务被分为两类:
- 宏任务(任务)
MacroTask
script全部代码、setTimeout、setInterval、setImmediate(只有IE10支持)、I/O、UI Rendering。 - 微任务
MicroTask
Process.nextTick(Node独有)、Promise.then()、MutationObserver(监听dom节点更新完毕)
浏览器的Event Loop
执行过程:
- 首先执行script,script被称为全局任务,也属于macrotask;
- 当macrotask执行完以下,执行所有的微任务;
- 微任务全部执行完,再取任务队列中的一个宏任务执行。
看下面这个例子:
setTimeout(function(){
console.log(1) //宏任务
},0)
new Promise(function executor(resolve) {
console.log(2) //同步任务
for(var j = 0;j<100;j++){
j=99&&resolve()
}
console.log(3) //同步任务
}).then(function(){
console.log(4) //微任务
})
console.log(5) //同步任务
// 2 3 5 4 1
setTimeout(() => {
console.log('A');
}, 0);
var obj = {
func: function() {
setTimeout(function() {
console.log('B');
}, 0);
return new Promise(function(resolve) {
console.log('C');
resolve();
});
},
};
obj.func().then(function() {
console.log('D');
});
console.log('E');
// C D E A B
-
第一个 setTimeout 放到宏任务队列,此时宏任务队列为 [‘A’]
-
接着执行 obj 的 func 方法,将 setTimeout 放到宏任务队列,此时宏任务队列为 [‘A’, ‘B’]
-
函数返回一个 Promise,因为这是一个同步操作,所以先打印出 ‘C’
-
接着将 then 放到微任务队列,此时微任务队列为 [‘D’]
-
接着执行同步任务 console.log(‘E’);,打印出 ‘E’
-
因为微任务优先执行,所以先输出 ‘D’
-
最后依次输出 ‘A’ 和 ‘B’
当事件循环遇上async/await时,await 前面的代码是同步的,调用此函数时会直接执行;await 后面的代码 则会被放到 Promise 的 then() 方法里。
Node环境中的Event Loop
执行顺序:外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O事件回调阶段(I/O callbacks)–闲置阶段(idle, prepare)–>轮询阶段。。。
timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
idle, prepare 阶段:仅node内部使用
poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
check 阶段:执行 setImmediate() 的回调
close callbacks 阶段:执行 socket 的 close 事件回调
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
})
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
})
// time1 promise1 time2 promise2 (浏览器)
// time1 time2 promise1 promise2 (Node)
在Node端时,当程序进入timers阶段,会执行timer1的回调函数,打印timer1,并将promise.then回调放入microtask队列,同样的步骤执行timer2,打印timer2。
setTimeout(function () {
console.log(1);
});
console.log(2);
process.nextTick(() => {
console.log(3);
});
new Promise(function (resolve, rejected) {
console.log(4);
resolve()
}).then(res=>{
console.log(5);
})
setImmediate(function () {
console.log(6)
})
console.log('end');
// 2 4 end 3 5 1 6
首先执行同步任务中的2 4 end,然后是microTask队列中的process.nextTick:3、promise.then:5,最后是macroTask队列中的setTimeout:1、setImmediate:6,由于Timer优于Check阶段,所以先1后6。
注意: 在Node 11以后:Node的事件循环和浏览器的行为统一了,都是每执行一个宏任务就执行完微任务队列。