前言
我们都知道JavaScript是一门单线程、非阻塞的脚本语言,目的是为了实现与浏览器交互。
这里我们提到了两点:一是单线程,二是非阻塞。
单线程是指JavaScript在执行的时候,有且只有一个主线程来处理所有的任务。
但是他为什么一定是单线程,而不是多线程的呢?
我们设想一下,如果JavaScript是多线程的,现在我们在浏览器中同时操作一个DOM,一个线程要求浏览器在这个DOM中添加节点,而另一个线程却要求浏览器删掉这个DOM节点,那这个时候浏览器就会很郁闷,他不知道应该以哪个线程为准。所以为了避免此类现象的发生,降低复杂度,JavaScript选择只用一个主线程来执行代码,以此来保证程序执行的一致性。
那非阻塞又是如何实现的呢?这就要说到我们今天的主角 Event Loop。
在了解 Event Loop 之前,我们先来熟悉一下执行栈和事件队列这两个概念。
执行栈
当我们调用一个方法的时候,js会生成一个与这个方法相对应的执行环境,也叫执行上下文,这个执行环境存在着这个方法的私有作用域、参数、this对象等等。因为js是单线程的,同一时间只能执行一个方法,所以当一系列的方法被依次调用的时候,js会先解析这些方法,把其中的同步任务按照执行顺序排队到一个地方,这个地方叫做执行栈。
事件队列
当我们发出一个ajax请求,他并不会立刻返回结果,为了防止浏览器出现假死或者空白,主线程会把这个异步任务挂起(pending),继续执行执行栈中的其他任务,等异步任务返回结果后,js会将这个异步任务按照执行顺序,加入到与执行栈不同的另一个队列,也就是事件队列。
浏览器中的Event Loop
-
主线程运行的时候会生成堆(heap)和栈(stack);
-
js从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;
-
当程序调用外部的API时,比如ajax、setTimeout等,会将此类异步任务按照执行顺序排列到事件队列中;
-
主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。
-
主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的... ...,这个过程被称为“Event Loop 事件循环”。
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0) ;
}, 0) ;
}, 0);
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0);
}, 0);
console.log('ok');
复制代码
脑阔痛?不要捉急,在此奉上小编贴心准备的GIF。
console.log(1);
setTimeout(() => {
console.log('setTimeout');
}, 0);
let promise = new Promise(resolve => {
console.log(3);
resolve();
}).then(data => {
console.log(100);
}).then(data => {
console.log(200);
});
console.log(2);
复制代码
Node中的Event Loop
-
timers:这个阶段执行定时器队列中的回调如 setTimeout 和 setInterval;
-
I/O callbacks:这个阶段执行几乎所有的回调。但是不包括close事件、定时器和setImmediate的回调;
-
idle, prepare:只在node内部使用;
-
poll:等待新的I/O事件,node在一些特殊情况下会阻塞在这里;
-
check:setImmediate回调在这个阶段执行;
-
close callbacks:只在node内部使用。
setImmediate(() => {
console.log(1);
process.nextTick(() => {
console.log(4);
});
});
process.nextTick(() => {
console.log(2);
setImmediate(() => {
console.log(3);
});
});复制代码
总结:
1. js解析方法时,将同步任务排队到执行栈中,异步任务排队到事件队列中。-
宏任务:setTimeout,setInterval,setImmediate,I/O,UI交互事件
-
微任务:process.nextTick,Promise.then
-
浏览器环境每次执行一个宏任务,再去检查微任务
-
node会清空当前所处阶段的队列,即执行所有task,再去检查微任务