本文是深入理解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代码。下面举个栗子完整的看一下异步事件执行流程。
我们发现运行后控制台依次输出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引擎才空闲、比如异步队列中,它前面有其他耗时的异步事件…..
下面要说的就是我曾经犯过的白痴错误,裱自己已示警示- -|||
以下代码:
我的错误的想法,会将输出结果猜想为:0,1,2,3,4,5,两个相邻输出的时间间隔为2秒。
然而通过本文我们知道,实际输出是0,1,2,3,4,5,0和1之间相隔2秒,1-5几乎是同时输出。
在这里我犯了两个错误:异步理解错误、setInterval理解错误……
所以说学习真的不能想当然呀╮(╯▽╰)╭,多研究理解才是解决问题的正确途径♪(^^∇^^*)。
——本节完——