stephen's blog

[object Object] object(s)
 

Understanding Event Loop in Javascript

前言

前段时间看了 Philip Roberts 关于event-loop的演讲视频收获良多,本文主要是笔者对event loop模型的总结和整理。

传送门:
YouTube: Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014

JavaScript Runtime

我们知道web应用在浏览器中运行会涉及到一系列比如JS runtime、event loop、dom、ajax等等技术,而在弄明白event loop之前我们必须先了解一下JS Runtime的基本作用。

如今业内领头的JS Runtime非Chrome的V8莫属了,下图简要的概括了JS Runtime是个什么样子:

其中heap、stack、callback queue、event loop是JS引擎基本的特征,而关于DOM、ajax、Timer等则属于Web APIs。

主线程

JS是单线程的,即只有一个主线程。而当主线程运行时,JS Runtime会产生heap和stack,其中:

1
2
3
4
5
6
7
8
9
10
11
function multiply(a,b){
return a * b
}
function square(n) {
return multiply(n,n)
}
function printSquare(n){
var squared = square(4)
console.log(squared)
}
printSquare(4)

首先调用printSquare()的时候,创建第一个堆栈帧,包含参数和局部变量,当printSquare()调用square()时,压入第二个堆栈帧,当square()调用multiply()的时候压入第三个堆栈帧,接着函数返回从栈中依次推出。将过程可视化为:

这就是我们所说的调用栈(Call Stack),JS Runtime每次只是逐行执行代码。

Callback Queue

我们知道任务主要分为同步任务(synchronous) 和 异步任务(asynchronous)。同步任务主要是在主线程中执行,而当JS引擎执行异步任务的时候(如setTimeout()、onclick()),浏览器内核相应模块(即WebAPIs)会处理相关的方法,达到触发条件后相关联的回调函数便会添加到Callback Queue中。当执行栈的代码执行完毕就会读取Callback Queue,依次执行相关的回调函数。

Event loop

之所以称为Event loop,是因为JS运行时会创建类似于这样的循环:

1
2
3
while(queue.waitForMessage()){
queue.processNextMessage();
}

每执行一次循环体称为一次Tick,每次Tick的过程就是查看Callback Queue中是否有待处理的Message,如果有则取出来放入执行栈中由主线程执行。

WebAPIs

主要是一些异步操作,比如DOM Binding、ajax、timer,这些异步操作由浏览器内核的webcore来执行。

Block & Non-Block

没有非常严格的定义什么是阻塞的什么是非阻塞的,不过我们知道JS执行代码是通过Call Stack这项技术,比如以下代码:

1
2
3
4
5
6
var foo = $.getSync('//foo.com')
var bar = $.getSync('//bar.com')
var qux = $.getSync('//qux.com')
console.log(foo)
console.log(bar)
console.log(qux)

foo()压入栈内,调用完后出栈,接着bar压入栈内…

熟悉JS的人都知道这段代码是阻塞的,一个请求完成才会去执行下一个请求。如果在浏览器内,一个页面进行热请求过长,页面其他部分就不能操作,显然这样的用户体验是非常差的,那么解决办法是什么?使用异步回调函数,比如使用setTimeout():

1
2
3
4
5
6
7
console.log('Hello')
setTimeout(function timeout(){
console.log('Here')}, 5000)
console.log('World')
// 'Hello'
// 'World'
// 'Here'

这就是为什么JS不阻塞的原因了,当执行异步函数setTimeout()并不会阻塞下面函数的执行,等异步函数达到触发条件后(上面例子是500ms后)在执行回调函数。那这里就有一个问题,异步函数具体是怎么样工作的呢,调用异步函数后,相应的回调函数又是放在哪?问题的答案就是Event Loop模型。

Event Loop

这里可以使用Phi写的一个可视化工具loupe,具体来看一下上面异步代码是怎么样运作的:

上图中,首先在Call Stack中压入console.log(‘Hello’)的方法,这只是一个普通方法,所以该方法会立即出栈执行。

接着继续往下执行setTimeout(),这是一个WebAPIs的方法,也就是异步方法,JS runtime会将延迟函数交给浏览器内核相应模块处理(这里是timer模块),然后立即继续往下执行代码。

这时setTimeout()执行5秒后,timer模块检测到延时处理方法到达触发条件,于是将相应的回调函数加入任务队列。而这个时候Call Stack为空,所以通过event loop轮询检查任务队列是否有回调函数,检查到timeout(),将其压入Call Stack,接着检测到console.log(‘Here’)方法继续压入栈,最后出栈执行。

DOM操作、ajax等都是一样的原理,这些都是由浏览器内核相应模块来处理。