Fork me on GitHub

Any application that can be written in JavaScript, will eventually be written in JavaScript.

JavaScript 单线程机制面试题

之前有次面试的时候,被问到一个有关单线程的问题,后来整理了一下,增加了一些复杂度。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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){}; //while 循环会一直循环为 true 的条件,知道为 false 才跳出
console.log('finished waiting');
}

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

console.log('click begin');
waitFiveSeconds();

如果你能很清晰的判断输出结果,并且没有疑惑,那说明你对 JavaScript 的单线程理解的不错,这篇文章没有看的必要了,如果不清楚,请继续看下去。

执行机制

JavaScript 是一门单线程语言,即同一时间只能执行一段代码。所以,对于同步任务,后一个任务只能等前一个任务处理完才能被执行。

然而,借助于事件驱动机制,JavaScript 可以通过异步任务去处理耗时比较长的操作,比如 Ajax 请求数据这种。再通过回调,将这些操作变成异步,放在异步的任务队列里执行,从而避免 JavaScript 执行线程的阻塞。

总结下来,JavaScript 对任务的处理机制是:

  • 所有同步任务在执行线程上立刻执行
  • 对异步任务,按照事件触发的顺序,依次将回调函数放入异步任务队列里(比如鼠标点击了、定时器设定的时间到了、HTTP 请求的状态变了等)
  • 一旦同步任务执行完,执行线程开始读取异步任务队列,依次执行每个任务对应的回调,这个运行机制也称为 Event Loop
  • 重复上面三步

只有在执行线程空闲的情况下,才会去执行异步任务队列中的任务(其中事件会优先执行)。

定时器

定时事件也是会被放在异步任务队列中的一类事件,用来指定某些代码在多少时间后执行。

有两个函数可以实现定时功能,一个是 setTimeout,一个是 setInterval,两者机制相同,区别是前者是一次性执行,后者反复执行。它们都接受两个参数,第一个是回调函数,第二个是延迟毫秒数。后面的讲解以 setTimeout 为例。

前面 JavaScript 执行机制小节中提到,setTimeout 只是在设定时间到了后,将回调放入异步任务队列,而不是立即执行。因此,回调执行的时间是大于等于实际设定的毫秒数的。

如果将延迟时间设为 0 ,即 setTimeout(fn, 0),表示在执行线程空闲后,立即执行指定回调 fn,但这个回调要等任务队列中处于等待状态的事件处理程序全部执行完后,才会“立即”调用。这一点在《JavaScript 权威指南》 14.1 计时器小节中有提到。

setTimeout(fn, 0) 的主要作用是,改变了代码流程,把 fn 的执行放在了当前同步代码全部执行完之后。

PS: 在 HTML5 规范中 setTimeout 的延时的最小值为 4 毫秒。

浏览器不是单线程的

虽然 JS 的执行是单线程的,但浏览器并不是单线程,而是通常有以下四种:

  • 界面渲染线程(UI)
  • JavaScript 执行线程
  • 浏览器事件触发线程
  • HTTP请求线程

因此,在 Ajax 请求时,浏览器会新开一个线程来请求,在请求的状态变更时,将相应的回调放入异步任务队列,在执行线程空闲的时候,Event Loop 开始。

题目解析

文章开头那道题的答案是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
0
1
2
3
4
click begin
finished waiting
click
click
timer a
timer b
click
click

下面简单分析一下:

首先,同步任务按先后顺序最先执行,耗时比较长的同步任务会阻塞当前线程,6-8 行、24 行和 25 行的输出没有疑问。对应输出 1-7 行。

然后,在执行线程工作的时候,1-4 行定时器产生的回调、 10-12 行定时器产生的回调和两次 click 对应的回调被先后放入异步任务队列。由于执行线程空闲后,在进行 Event Loop 的时候,会先查看是否有事件可执行,接着再处理其他异步任务。因此会产生 8-11 行这样的输出顺序。

最后,5s 后的两次 click 事件被放入异步任务队列,由于此时执行线程为空,便被立即执行了。对应输出 12-13 行。