JavaScript运行机制:
所有同步任务都在主线程上执行,形成一个执行栈;
主线程发起异步请求,相应的工作线程就会去执行异步任务,主线程可以继续执行后面的代码;
主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件,也就是一个消息;
一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;
主线程把当前的事件执行完成之后,再去读取任务队列,如此反复重复执行,这样就行程了事件循环。
1、单线程
JS引擎在解释和执行JavaScript代码线程只有一个,叫做主线程。
但实际还存在其他线程,如:处理Ajax请求线程,处理DOM事件线程,定时器线程,和文件读写线程等,叫做工作线程。
2、同步和异步
同步:如果函数返回的时候,调用者就能够得到预期结果。
1 | Math.sqrt(2); |
异步:函数返回的时候,调用者还不能够得到预期结果,而是需要通过一定手段得到。
1 | fs.readFile("foo.txt", "utf8", function(err, 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 | $.ajax('http://segmentfault.com', function(resp) { |
主线程发起Ajax请求后,会继续执行其他代码。Ajax线程负责请求 segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构成一条消息:
1 |
|
其中callbackFn就是前面代码中成功响应时的回调函数。
主线程在执行完当前循环中所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它,到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,Ajax线程在收到HTTP响应后,也就没有必要通知主线程,从而没必要往消息队列放消息。
异步过程的回调函数,一定不在当前这一轮事件循环中执行。
5、异步与事件
消息队列中的每条消息实际上都对应着一个事件
有一类很重要的异步过程:DOM事件
1 | var button = document.getElement('#btn'); |
从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。
从异步过程的角度看,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会在下一个循环中执行