理解同步、异步和事件循环

JavaScript运行机制:

  1. 所有同步任务都在主线程上执行,形成一个执行栈;

  2. 主线程发起异步请求,相应的工作线程就会去执行异步任务,主线程可以继续执行后面的代码;

  3. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件,也就是一个消息;

  4. 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;

  5. 主线程把当前的事件执行完成之后,再去读取任务队列,如此反复重复执行,这样就行程了事件循环。

1、单线程

JS引擎在解释和执行JavaScript代码线程只有一个,叫做主线程
但实际还存在其他线程,如:处理Ajax请求线程,处理DOM事件线程,定时器线程,和文件读写线程等,叫做工作线程

2、同步和异步

同步:如果函数返回的时候,调用者就能够得到预期结果。

1
Math.sqrt(2);

异步:函数返回的时候,调用者还不能够得到预期结果,而是需要通过一定手段得到。

1
2
3
fs.readFile("foo.txt", "utf8", function(err, data){
console.log(data)
})

上面代码中,我们希望fs.readFile函数读取文件,并打印出来,但是在fs.readFile函数返回时,我们期望的结果并不会发生,而是要等文件全部读取完成之后。

异步AJAX:

主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

AJAX线程:“好的,主线程。我马上去发,但可能要花点儿时间呢,你可以先去忙别的。”

主线程::“谢谢,你拿到响应后告诉我一声啊。”

(接着,主线程做其他事情去了。一顿饭的时间后,它收到了响应到达的通知。)

同步AJAX:

主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。”

AJAX线程:“……”

主线程::“喂,AJAX线程,你怎么不说话?”

AJAX线程:“……”

主线程::“喂!喂喂喂!”

AJAX线程:“……”

(一炷香的时间后)

主线程::“喂!求你说句话吧!”

AJAX线程:“主线程,不好意思,我在工作的时候不能说话。你的请求已经发完了,拿到响应数据了,给你。”

正因为JavaScript时单线程,同步容易造成阻塞,所以,对于耗时和操作事件不确定操作,使用异步就成了必然选择。

3、异步过程

一个异步过程通常时这样的:

主线程发起一个异步请求,相应的工作线程接收线程请求并告知主线程已收到;主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定动作(调用回调函数)。

异步函数通常具有一下形式:

1
A(arg..., callbackFn)

他可以叫做异步过程的发起函数,或者叫做异步任务注册函数

从主线程的角度看,一个一度过程包括两个要素:

  • 发起函数(注册函数)
  • 回到函数

4、消息队列和事件循环

异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制时怎样实现的呢?答案是利用消息队列和事件循环。

一句话概括:

工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。

消息队列:消息队列是一个先进先出的队列,放着各种消息。

事件循环:事件循环是指主线程从消息队列中取消息,执行的过程。

实际上,主线程只会做一件事,就是从消息队列里面取消息、执行消息,再去消息,再执行。当消息队列为空时,就会等待知道消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。

消息队列中放的消息是什么东西?消息的具体结构当然跟具体的实现有关,可以认为:

消息就是注册异步任务时添加的回调函数。

以异步Ajax为例

1
2
3
$.ajax('http://segmentfault.com', function(resp) {
console.log('我是响应:', resp);
});

主线程发起Ajax请求后,会继续执行其他代码。Ajax线程负责请求 segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构成一条消息:

1
2
3
4

var message = function() {
callbackFn();
}

其中callbackFn就是前面代码中成功响应时的回调函数。

主线程在执行完当前循环中所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它,到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,Ajax线程在收到HTTP响应后,也就没有必要通知主线程,从而没必要往消息队列放消息。

image

异步过程的回调函数,一定不在当前这一轮事件循环中执行。

5、异步与事件

消息队列中的每条消息实际上都对应着一个事件

有一类很重要的异步过程:DOM事件

1
2
3
4
var button = document.getElement('#btn');
button.addEventListener('click', function(e) {
console.log();
});

从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。

从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。

事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制。我觉得它的存在是为了编程接口对开发者更友好。

另一方面,所有的异步过程也都可以用事件来描述。例如:setTimeout可以看成对应一个时间到了!的事件。前文的setTimeout(fn, 1000);可以看成:

1
timer.addEventListener('timeout', 1000, fn);

工作线程是生产者,主线程是消费者(只有一个消费者)。工作线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。

6、macrotasks与microtasks的区别

macrotasks: setTimeout setInterval setImmediate I/O UI渲染

microtasks: Promise process.nextTick Object.observe MutationObserver

microtask会在当前循环中执行完成,而macrotask会在下一个循环中执行