javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。因此一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!javascript
既然js是单线程,后一个任务会等前一个任务执行完成后才会执行,若是前一个任务执行时间过长后面的任务一直得不到执行,就会引发阻塞。那么问题来了,假如咱们想浏览新闻,可是新闻包含的超清图片加载很慢,难道咱们的网页要一直卡着直到图片彻底显示出来?所以咱们会将任务分为两类:html
当咱们打开网站时,网页的渲染过程就是一大堆同步任务,好比页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。具体逻辑见下面的导图: java
文字描述node
上图中Event Queue 包括 macro task queue 和 micro task queue,下一小节咱们会详细解释一下。 上代码咱们体会一下这个流程:网络
console.log('1');
setTimeout(function () {
console.log('timeout');
});
console.log('2');
复制代码
上面的代码解释多线程
console.log('1');
和console.log('2');
是同步任务会放到主线程中,setTimeout
声明的回调函数会放到Event Table。主线程内的任务(console.log('1');console.log('2');
)执行完毕为空,会去Event Queue读取console.log('timeout');
,进入主线程执行。因此执行的结果为1 2 timeout
。下面两张图为Event Loop 和 macro-task 及 micro-task的关系异步
导图解释ide
看到这么多的定义和导图,咱们来段代码屡一下:函数
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')
})
复制代码
macro task Queue | macro task Queue |
---|---|
setTimeout | process1 |
- | then1 |
macro task Queue | macro task Queue |
---|---|
- | process2 |
- | then2 |
1 7 6 8 2 4 3 5
。下图为node中Event Loop的执行顺序的简略图 oop
note
1.poll阶段的功能
2.poll阶段的处理流程
下面我用if else的方式描述一下poll阶段的处理逻辑,以下:
if ('事件循环进入到 poll 阶段 ' && '没有timers注册的scripts') {
if ('poll 队列 不为空') {
console.log('循环遍历它的回调队列,以同步执行它们,直到队列耗尽,或者达到系统依赖的最大值');
} else {
if ('存在setImmediate()注册的scripts') {
console.log('结束poll phase 进入到check phase 执行这些注册的scripts');
} else {
console.log('事件循环将等待被添加到队列中的回调,而后当即执行它们');
}
}
}
console.log('一旦轮询队列为空,事件循环将检查有无到期的计时器。若是有一个或多个计时器准备就绪,事件循环将返回到计时器阶段,以执行这些计时器的回调。');
复制代码
3.比较setImmediate() 和 setTimeout()
setImmediate()
和 setTimeout()
很类似的,它们什么时候被调用,决定了它们的行为方式的不一样。
setImmediate
用于在当前轮询阶段完成后执行脚本setTimeout
用于把注册的脚本在最小阈值结束后运行。它们执行的顺序将根据调用它们的上下文而变化。若是两个都是从主模块中调用,那么它们将受到进程性能的约束(这可能会受到其余应用程序的影响)。
例如,若是咱们运行的脚本不是在I/O循环中(即主模块),那么执行两个定时器的顺序是不肯定的,由于它受过程性能的约束:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// 打印结果的前后顺序是不肯定的,有时`timeout`在前,有时'immediate'在前
复制代码
可是,若是把这段代码放到I/O循环的回调中,immediate
老是先被打印出来,以下:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 在一个I/O周期内,在任何计时器的状况下,setImmediate的回调,由于在一个I/O周期内,I/O callback 的下一个阶段为setImmediate的回调。
复制代码
以下图:
说明
咱们在回头看一下,下面的代码:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
复制代码
能够看出因为两个setTimeout延时相同,被合并入了同一个expired timers queue,而一块儿执行了。因此,只要将第二个setTimeout的延时改为超过2ms(1ms无效,由于最小间隔为1s),就能够保证这两个setTimeout不会同时过时,也可以保证输出结果的一致性。
咱们在回头看一下,上面提到的另一段代码:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
复制代码
为什么这样的代码能保证setImmediate的回调优先于setTimeout的回调执行呢?由于当两个回调同时注册成功后,当前node的Event Loop正处于I/O queue阶段,而下一个阶段是immediates queue,因此可以保证即便setTimeout已经到期,也会在setImmediate的回调以后执行。
因为水平有限,理解的程度可能会有误差,欢迎你们指正。