JavaScript异步机制详解

学习JavaScript的时候了解到JavaScript是单线程的,刚开始很疑惑,单线程怎么处理网络请求、文件读写等耗时操做呢?效率岂不是会很低?随着对这方面内容的了解和深刻,知道了其中的奥秘。本篇文章就主要讲解一下JavaScript怎么处理异步问题。javascript

1、同步与异步

在介绍JavaScript的异步机制以前,首先介绍一下:什么是同步?什么是异步? html

同步

若是在函数返回的时候,调用者就可以获得预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。java

以下所示:git

//在函数返回时,得到了预期值,即2的平方根
Math.sqrt(2);
//在函数返回时,得到了预期的效果,即在控制台上打印了'hello'
console.log('hello');
复制代码

上面两个函数就是同步的。github

若是函数是同步的,即便调用函数执行的任务比较耗时,也会一直等待直到获得预期结果。面试

异步

若是在函数返回的时候,调用者还不可以获得预期结果,而是须要在未来经过必定的手段获得,那么这个函数就是异步的。segmentfault

以下所示:浏览器

//读取文件
fs.readFile('hello.txt', 'utf8', function(err, data) {
    console.log(data);
});
//网络请求
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx; // 添加回调函数
xhr.open('GET', url);
xhr.send(); // 发起函数
复制代码

上述示例中读取文件函数 readFile和网络请求的发起函数 send都将执行耗时操做,虽然函数会当即返回,可是不能马上获取预期的结果,由于耗时操做交给其余线程执行,暂时获取不到预期结果(后面介绍)。而在JavaScript中经过回调函数 function(err, data) { console.log(data); }onreadystatechange ,在耗时操做执行完成后把相应的结果信息传递给回调函数,通知执行JavaScript代码的线程执行回调。bash

若是函数是异步的,发出调用以后,立刻返回,可是不会立刻返回预期结果。调用者没必要主动等待,当被调用者获得结果以后会经过回调函数主动通知调用者。网络

2、单线程与多线程

在上面介绍异步的过程当中就可能会纳闷:既然JavaScript是单线程,怎么还存在异步,那些耗时操做到底交给谁去执行了?

JavaScript其实就是一门语言,说是单线程仍是多线程得结合具体运行环境。JS的运行一般是在浏览器中进行的,具体由JS引擎去解析和运行。下面咱们来具体了解一下浏览器。

浏览器

目前最为流行的浏览器为:Chrome,IE,Safari,FireFox,Opera。浏览器的内核是多线程的。

一个浏览器一般由如下几个常驻的线程:

  • 渲染引擎线程:顾名思义,该线程负责页面的渲染
  • JS引擎线程:负责JS的解析和执行
  • 定时触发器线程:处理定时事件,好比setTimeout, setInterval
  • 事件触发线程:处理DOM事件
  • 异步http请求线程:处理http请求

须要注意的是,渲染线程和JS引擎线程是不能同时进行的。渲染线程在执行任务的时候,JS引擎线程会被挂起。由于JS能够操做DOM,若在渲染中JS处理了DOM,浏览器可能就不知所措了。

JS引擎

一般讲到浏览器的时候,咱们会说到两个引擎:渲染引擎和JS引擎。渲染引擎就是如何渲染页面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不一样的引擎对同一个样式的实现不一致,就致使了常常被人诟病的浏览器样式兼容性问题。这里咱们不作具体讨论。

JS引擎能够说是JS虚拟机,负责JS代码的解析和执行。一般包括如下几个步骤:

  • 词法分析:将源代码分解为有意义的分词
  • 语法分析:用语法分析器将分词解析成语法树
  • 代码生成:生成机器能运行的代码
  • 代码执行

不一样浏览器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

之因此说JavaScript是单线程,就是由于浏览器在运行时只开启了一个JS引擎线程来解析和执行JS。那为何只有一个引擎呢?若是同时有两个线程去操做DOM,浏览器是否是又要不知所措了。

因此,虽然JavaScript是单线程的,但是浏览器内部不是单线程的。一些I/O操做、定时器的计时和事件监听(click, keydown...)等都是由浏览器提供的其余线程来完成的。

3、消息队列与事件循环

经过以上了解,能够知道其实JavaScript也是经过JS引擎线程与浏览器中其余线程交互协做实现异步。可是回调函数具体什么时候加入到JS引擎线程中执行?执行顺序是怎么样的?

这一切的解释就须要继续了解消息队列和事件循环。

如上图所示,左边的栈存储的是同步任务,就是那些能当即执行、不耗时的任务,如变量和函数的初始化、事件的绑定等等那些不须要回调函数的操做均可归为这一类。

右边的堆用来存储声明的变量、对象。下面的队列就是消息队列,一旦某个异步任务有了响应就会被推入队列中。如用户的点击事件、浏览器收到服务的响应和setTimeout中待执行的事件,每一个异步任务都和回调函数相关联。

JS引擎线程用来执行栈中的同步任务,当全部同步任务执行完毕后,栈被清空,而后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。

JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,若是没有新的任务,就会等待,直到有新的任务,这就叫事件循环。

上图以AJAX异步请求为例,发起异步任务后,由AJAX线程执行耗时的异步操做,而JS引擎线程继续执行堆中的其余同步任务,直到堆中的全部异步任务执行完毕。而后,从消息队列中依次按照顺序取出消息做为一个同步任务在JS引擎线程中执行,那么AJAX的回调函数就会在某一时刻被调用执行。

4、示例

引用一篇文章中提到的考察JavaScript异步机制的面试题来具体介绍。

执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(>5s)后,再点击两下,整个过程的输出结果是什么?

setTimeout(function(){
    for(var i = 0; i < 100000000; i++){}
    console.log('timer a');
}, 0)

for(var j = 0; j < 5; j++){
    console.log(j);
}

setTimeout(function(){
    console.log('timer b');
}, 0)

function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}

document.addEventListener('click', function(){
    console.log('click');
})

console.log('click begin');
waitFiveSeconds();
复制代码

要想了解上述代码的输出结果,首先介绍下定时器。

setTimeout的做用是在间隔必定的时间后,将回调函数插入消息队列中,等栈中的同步任务都执行完毕后,再执行。由于栈中的同步任务也会耗时,因此间隔的时间通常会大于等于指定的时间

setTimeout(fn, 0)的意思是,将回调函数fn马上插入消息队列,等待执行,而不是当即执行。看一个例子:

setTimeout(function() {
    console.log("a")
}, 0)

for(let i=0; i<10000; i++) {}
console.log("b")
复制代码
b  a
复制代码

打印结果代表回调函数并无马上执行,而是等待栈中的任务执行完毕后才执行的。栈中的任务执行多久,它就得等多久。

理解了定时器的做用,那么对于输出结果就容易得出了。

首先,先执行同步任务。其中waitFiveSeconds是耗时操做,持续执行长达5s。

0
1
2
3
4
click begin
finished waiting
复制代码

而后,在JS引擎线程执行的时候,'timer a'对应的定时器产生的回调、 'timer b'对应的定时器产生的回调和两次 click 对应的回调被前后放入消息队列。因为JS引擎线程空闲后,会先查看是否有事件可执行,接着再处理其余异步任务。所以会产生 下面的输出顺序。

click
click
timer a
timer b
复制代码

最后,5s 后的两次 click 事件被放入消息队列,因为此时JS引擎线程空闲,便被当即执行了。

click
click
复制代码

参考文章

JavaScript:完全理解同步、异步和事件循环(Event Loop)
从setTimeout说事件循环模型
JavaScript单线程和异步机制
JavaScript的单线程机制
JavaScript单线程异步的背后——事件循环机制
JavaScript 运行机制详解:再谈Event Loop

相关文章
相关标签/搜索