萝三画室

深入理解JS-part2-单线程、异步和setInterval

本文是深入理解JS系列的part2,详述JS单线程执行和异步操作的实现。JS单线程执行的意思是,同一时刻,总是只有一个线程在执行JS代码。但我们又发现,Ajax请求和setInterval等不都是异步操作吗?如果是单线程执行,怎么会有异步操作呢?异步操作一定是两个以上线程共同完成的啊?带着这个疑惑,我们从JS引擎和浏览器线程开始说起。

JS是单线程执行的

在本系列文章part1部分我们知道,浏览器中的JS引擎负责编译和运行javascript代码。JS是单线程执行的,就是说同一时刻内只会有一段代码在JS引擎内运行。

浏览器是多线程的

一般而言,浏览器在运行时常驻三个线程:

  • JS引擎线程:负责执行js代码
  • GUI渲染线程:负责渲染页面
  • 事件队列线程:异步代码队列

其中,JS引擎线程是主线程,它和GUI渲染线程同时只有一个线程在运行,另一个线程被挂起。js涉及页面交互,可能引发页面重绘,重绘工作要等到js引擎空闲时运行。事件队列是异步代码执行之前的排队等候区。主线程执行到异步代码时,会将异步事件推入事件队列中排队,js引擎空闲时,会按顺序从队列中提取事件并执行。

因此,单线程指的是JS引擎同一时刻只能执行一段代码,是语言行为;异步操作的实现是由多线程的浏览器中的js引擎和事件队列共同完成,是浏览器行为。

另外,由JS引擎线程和GUI渲染线程互斥可以知道,js导致页面重绘实际上也是异步的,代码中改变DOM之后页面并不会立刻更新。这就可以理解vue中的this.$nextTick()出现的原因了,详见vue总结

异步实际上还是同步

理解了单线程和异步之后,我们发现,其实此处的异步其实还是同步,它只是将代码执行的时间推迟到了js引擎空闲时,并没有同时多线程运行js代码。下面举个栗子完整的看一下异步事件执行流程。

1
2
3
4
5
6
7
(()=>{
console.log("a");
setTimeout(()=>{
console.log("b")
}, 1000);
console.log("c");
})()

我们发现运行后控制台依次输出a、c、b。
js引擎的工作流程是这样的:
进入作用域->执行同步代码输出a->向下遇见异步代码,不执行,设定在js引擎空闲后1秒将其推入事件队列->向下执行同步代码输出c,此时js引擎已处于空闲状态->1秒后异步事件推入事件队列->js引擎空闲,执行异步事件。

从这个过程中我们看到,在进入一个作用域并开始运行js代码时,js引擎从上至下按顺序执行代码。遇到同步代码直接执行;遇到异步代码,则会在作用域内所有同步代码执行完毕后(也就是js引擎空闲时),将其推入事件队列,再按顺序执行异步事件。

为什么说是执行完所有同步代码才会执行异步代码呢?我们将上面setTimeout中的时间间隔改为0,发现输出顺序并没有改变。这个技巧可以用来改变代码执行顺序。

setTimeout和setInterval的真实意义

很多人会将setTimeout和setInterval错误地理解为:xx秒之后执行事件。而它们真正的含义是:在js引擎空闲后xx秒,将异步事件推入事件队列。这二者的差别是很大的。通常情况下js执行时间很短看不出区别,但如果在同步代码耗时较长或事件队列前面有其他异步任务时,错误的理解就会造成意料之外的行为。

那么也就不难理解,setTimeout和setInterval中的时间间隔只是表示大致时间,最终到底是就多久后才执行异步事件是不能准确确定的。比如2秒后js引擎才空闲、比如异步队列中,它前面有其他耗时的异步事件…..

下面要说的就是我曾经犯过的白痴错误,裱自己已示警示- -|||
以下代码:

1
2
3
4
5
6
7
8
9
10
11
(()=>{
console.log(0,new Date())
for (let i = 1;i<5;i++){
setInterval(function(){
console.log(i,new Date())
},2000)
console.log(i)
}
})()

我的错误的想法,会将输出结果猜想为:0,1,2,3,4,5,两个相邻输出的时间间隔为2秒。
然而通过本文我们知道,实际输出是0,1,2,3,4,5,0和1之间相隔2秒,1-5几乎是同时输出。
在这里我犯了两个错误:异步理解错误、setInterval理解错误……

所以说学习真的不能想当然呀╮(╯▽╰)╭,多研究理解才是解决问题的正确途径♪(^^∇^^*)。

——本节完——