javascript中的EventLoop

本文介绍 javascript 引擎中的事件循环(EventLoop)是如何工作的。

数据在内存中的存储方式

在 javascript 中,数据类型分为 基本类型引用类型。那么 基础类型 是存储在 栈内存(stack) 中的简单数据,按值存取,例如:Boolean,String,Number,undefined,null,Symbol,BigInt。

引用类型(Object,Function)则是存储在 堆内存(heap) 中,并在 栈内存 中存储着该 引用类型 数据的一个引用,指向该数据在 堆内存 的存储地址。

1
2
3
4
5
6
var a = ''
var b = 14
var obj = { foo: '' }
var fn = function(){
console.log( 'hello' )
}

上面代码中各变量在内存中的存储形式如下:

demo-1

执行栈(call stack)

javascript 中,变量声明,函数调用都会按照程序的逻辑依次被压入栈内执行。

观察下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// main.js

function multiply(a, b){
return a * b;
}
function calcSquare(n){
return multiply(n, n);
}
function printSquare(n){
var result = calcSquare(n);
console.log(result);
}
printSquare(4);

我们在主程序 main.js 中依次定义了 multiply , calcSquareprintSquare 这3个函数。在 printSquare 中调用了 calcSquare,而 calcSquare 中又调用了 multiply

主程序开始后,printSquare() 首先被压入执行栈,随后 calcSquare() 压入执行栈,最后是 multiply()

栈具有 先进后出,后进先出 的特点,所以位于栈顶的函数先执行,依次类推。

以下是进栈后的情况:

demo-2

现在,让我们看看上面代码在执行栈内执行的动态示意图:

demo-3

栈内异常

如果代码在栈内执行遇到错误,javascript 引擎会抛出如下错误,并终止主程序的继续执行。

demo-4

栈满错误

有时,由于程序过于庞大或代码编写失误,导致栈内任务剧增,使得执行栈满,javascript 引擎会抛出如下错误。

demo-5

同步阻塞

在主线程执行栈中,只能存在同步代码。也就是说,在执行栈中的任务都是同步任务。如果存在耗时较长的同步任务,那么我们的程序就会阻塞,直到上一个任务完成才会继续向下执行。

例如下面这样的代码:

1
2
3
4
5
6
7
8
9
var a = $.getSynchronous("/a");
var b = $.getSynchronous("/b");
var c = $.getSynchronous("/c");

// get data done, now log them out

console.log(a);
console.log(b);
console.log(c);

在上方代码中,有3行耗时较长的同步代码,在这 3 行代码执行完之前,后面的打印操作便会一直处在等待状态(阻塞),那么在这个阻塞的事件段内,用户界面一直会处于白屏或 loading 态,这对于用户体验是很糟糕的。

demo-6

异步回调

观察下面的代码,思考其打印结果是什么?

1
2
3
4
5
6
7
console.log("Hello");

setTimeout(function (){
console.log("World");
}, 5000);

console.log("Hi");

打印结果:

1
2
3
Hello
Hi
World

那么,为什么会是这样的结果呢?它的执行过程是怎么样的?

让我们再看一下上面代码的执行过程:

demo-7

由于执行栈中只能存在同步代码,从执行栈内的动态示意图来看,同步的 console.log('Hello') , setTimeout(callback) (callback并没有被立即执行), console.log('Hi') 被一次压入了执行栈内,而 setTimeout 中的回调函数最后被压入执行栈中。

事件循环

到这里,终于可以谈一谈 事件循环 了,当我们的代码中出现了异步任务时,javascript 引擎是如何工作的呢?

首先,javascript 引擎在完成代码解析后,将同步代码依次压入执行栈中,在遇到异步代码,例如:DOM 事件,AJAX请求,setTimeout/setInterval 这样的异步任务时,会在异步任务完成后,将其回调函数推进一个叫做 回调队列(Callback Queue) 的队列中,等待执行栈内的任务结束后,再依次从 回调队列 的队头取出回调函数压入执行栈中继续执行。

demo-8

再让我们站在 事件循环 的角度看一次上面代码的执行过程:

demo-9

如果 setTimeout(callback, 0) 会是怎样?

1
2
3
4
5
6
7
console.log("Hi");

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

console.log("Welcome");

我们直接看上面代码的执行效果:

demo-10

没错!依然是同步的任务先执行,异步任务的回调函数依然会在异步时间到的时候被推进 回调队列 中,在同步任务结束后执行栈空时,回调队列 中的任务才会被压进 执行栈

在这里的延时时间 0,并不能阻碍 setTimeout() 是一个异步代码,其回调函数依然会被推进 回调队列 中。