阅读本文,你将知道:
- 同步和异步
- 阻塞和非阻塞
- JavaScript的异步实现方式
- 进一步深入理解为什么“JavaScript是异步事件驱动的单线程编程语言”

前言
在最初学习JavaScript的时候,就从各个地方得知JavaScript是一门单线程编程语言,但是令人疑惑的是,为什么一个单线程语言能够同时执行HTTP请求同时渲染页面?为什么代码的书写顺序和执行顺序并不一致?
这就是异步带来的效果
这里我们必须达到如下的共识:
- 浏览器是多线程的,常驻线程有:
- 浏览器 GUI 渲染线程
- JavaScript 引擎线程
- 浏览器定时触发器线程
- 浏览器事件触发线程
- 浏览器 http 异步请求线程
- JavaScript是单线程的
这里需要注意的是GUI渲染进程和JavaScript引擎进程是互斥的,因为如果这两个线程可以同时运行的话,JavaScript的DOM操作将会扰乱渲染线程执行渲染前后的数据一致性。
本文想要探讨的,是JavaScript线程里的异步设计,千万别和多线程混淆了。
想要了解更多关于浏览器多线程机制请参考:www.cnblogs.com/hksac/p/659…
开始
一切得先从CPU开始讲起:
CPU的指令执行速度是远高于硬盘读取速度和主存读取速度的。而I/O操作就会涉及到硬盘存取和主存读取,常见的I/O操作有文件I/O,网络I/O。(I/O = Input / Output)。
所以,观察以下这一段伪代码:
var a = 2;
for(let i =0;i<10;i++){
doSomeWork();
}
let buffer = openFile('./work.txt')
buffer.add('hello world');
在CPU眼中,他会把代码看成这两部分:

绿色部分因为不涉及到I/O操作,所以CPU执行速度超快,但是当运行到红色部分时,却是一个非常耗时的操作,而这段时间,CPU是处于一个'无所事事'的状态(DMA获取总线控制权之后一切I/O与CPU无关),因为文件如果没有读取进来,下面的工作也无法开展。
同步在这里的意思,即书写代码的顺序就是代码执行的顺序,如果JavaScript设计成同步的话,那么当执行到openFile
这一行的时候,将会等待该I/O操作完成CPU才继续往下执行。
设想一下,当发送Ajax请求(网络I/O)的时候,整个页面被阻塞无法操作将会是多差的体验。
而诸如鼠标点击事件,滑动事件,失焦事件,在CPU看来,都是处理得特别慢的事件(虽然对我们来说是一瞬间的事情),如果将JavaScript设计成同步,也会特别浪费CPU性能。
而阻塞和非阻塞关注的CPU在I/O发生时的工作情况
在上面这个读取文件的例子中
- 如果在读取文件的同时,该线程被‘挂起’(可以理解为进程的阻塞态),CPU不在关注这个线程直到结果被返回,属于阻塞式
- 如果在读取文件的同时,CPU会时不时关注并检查一遍结果是否返回,则属于非阻塞式
如果无法区分同步
和阻塞
,请参考这里
异步则解决了代码被耗时任务阻止其往下执行的缺点
多线程异步有着比较好的解决方案:
- 给涉及到I/O操作的部分新开一个线程执行
- 主线程不等待继续往下执行
- I/O线程执行完之后将结果写回公共区并通知主线程(也可以是主线程去轮询)
- 主线程执行其回调
但是
JavaScript是一门单线程语言,本身无法提供多线程,那么是通过怎样的机制来实现异步的?
先给出答案:JavaScript通过事件循环和浏览器各线程协调共同实现异步
JavaScript认为任务分为两种,一种是全由CPU决定完成速度的任务,我们称其为同步任务,一种是由多种因素(如硬盘读取速度,网速,点击反馈速度)决定完成速度的任务,我们称其为异步任务。
举个简单的例子
- 函数声明,for循环,变量声明,赋值操作等都可以属于同步任务
- 读取文件,网络请求,网页事件都看做异步任务
JavaScript将所有的异步任务都会放进一个队列里面,在执行完所有的同步任务之后,会去队列中找到最先进入队列的异步任务执行。

仔细观察上图,结合本文在最开始提到的浏览器多线程设计:
- JavaScript线程首先执行同步任务
- 在执行完同步任务之后,会去异步任务队列的队头取出任务执行
- 浏览器各个线程会在事件触发且完成事件之后将回调函数写入异步队列(先进先出队列)
因为诸如事件触发,http请求都是耗时无法直接确定的任务,也就是说JavaScript线程无法得知异步的任务回调函数究竟什么时候会写入异步任务队列,那么这个地方,就需要一个机制,去时刻轮询这个任务队列,这就是事件循环(event loop)
现在我们再看如下代码的执行顺序:
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
真正的执行顺序是
var req = new XMLHttpRequest();
req.open('GET', url);
req.send(); // i am here
req.onload = function (){};
req.onerror = function (){};
同理
while(1){
console.log('1')
}
setTimeOut(()=>{
console.log('00000000000000000')
},1)
也将同样永远不会输出00000000000000000
讨论下为什么这样设计
因为JavaScript的工作环境是一个典型的异步应用场景:充斥着各种ajax事件和浏览器事件。各个事件的触发时间和得到反馈的时间都不得而知,如果设计成同步语言,将会带来极差的浏览器使用体验。
需要设计一个成一个生产者-消费者
模型(也可以看做是观察者模式),来管理这样的异步任务。
浏览器需要做的事情太多了,一手需要负责渲染,一手需要负责http请求,一手还需要执行JavaScript,将JavaScript设计成单线程不仅能够让浏览器更好地控制各个线程,同时对开发者来说也更简单。多线程涉及到锁,临界区,冲突解决的学习成本还是比较高的。
总结
再次来看一下这一句话:
JavaScript是异步事件驱动的单线程编程语言
异步:写代码顺序不一定是执行顺序,JavaScript线程先执行同步任务。 事件驱动:其他线程在各事件完成后将回调函数写入队列,都是以抽象事件作为触发机制的。 单线程:不能开多线程而是用eventloop来实现异步的。
JavaScript通过事件循环和浏览器各线程协调共同实现异步
JavaScript的异步设计非常优秀,这也让基于V8引擎的Node在服务端大方异彩,能够更加简单地开发出适合高密集I/O的web应用。
之后会基于JavaScript的EventLoop总结下关于Node的异步I/O(涉及到多线程)
如果喜欢,请关注
最新博客会最先更新在http://www.helloyzy.cn