最近研究JavaScript里的函数事件这些到底是如何调用的,查阅了好些资料,特别是国外一些大牛写的文章,启发非常的大,于是打算对这些知识进行梳理。
基本知识
JS是什么?
- JS是单线程,非阻塞,异步,并发的语言
- JS有 调用栈,事件循环,回调队列,其它的APIs。
JS 在运行时
- JS在运行时(像V8引擎)有堆(内存分配)和栈(执行环境), 但是他们没有 setTimeout, DOC 等,这些是浏览器的web APIs。
我们知道的JS在浏览器里有:
- 运行时的V8(堆/栈)
- 浏览器提供web APIs,比如DOM, ajax, 和setTimeout
- 事件的回调队列,比如onClick, onLoad, onDone。
- 事件循环
JS的单线程(single thread)
:仅仅是指JS代码执行在单线程里面。调用栈(call stack)
:又称执行环境(execution contexts), 当函数或者程序调用的时候,就把该函数push到调用栈,结束时候,就从栈顶端移除。遵循FILO(先进后出)原则。注意
:调用栈有个最大调用帧,chrome 浏览器最大支持16000个调用帧,超过后,会报“RangeError:Maximum call stack size exceeded”
堆(Heap)
: 内存分配(memory allocation)的一块空间。JS的引用类型的值是放在堆内存的。回调队列(callback Queue)
:JS在运行时候,有一个列表用于记录将要处理的回调事件。遵循FIFO(先进先出)原则。事件循环(event loop)
: 事件循环不断的轮询检测调用栈是否为空?如果不为空,就等待调用栈为空,否则就把回调队列列表里的事件放到调用栈执行。循环直到回调队列列表为空。
##同步函数调用栈机制
我们以一个例子来进行同步函数执行的机制, 有两个函数foo,和bar,bar内部调用foo。
function foo(a, b) {
console.log("in foo");
return a * b;
}
function bar(y) {
var x = 10;
console.log("in bar");
return foo(x, y);
}
console.log(bar(5));
以上代码运行顺序如下:
- 最先进入
console.log(bar(5));
因此它被推送到调用栈。 console.log(bar(5))
;调用函数bar(5), 等待bar(5)的返回结果,于是把bar(5)推送到调用栈顶部。- 函数bar(5)里有
console.log("in bar");
于是推送console.log("in bar");
到栈顶。 console.log("in bar");
执行完成返回,然后将console.log("in bar");
从栈顶移除。- 接着运行
return foo(x, y);
,这是调用foo(x, y)
,把foo(x, y)
推入到调用栈。 - 执行
console.log("in foo");
推入栈顶,结束,移除栈。 - 运行
return a * b;
调用a * b
,推送a * b
到栈顶,a * b
运行完后,出栈。 - 接着 return 10+5的结果返回给bar函数的foo(10, 5),这样foo(x,y)函数结束执行,foo(x,y)从调用栈移除。
- 接着bar(y)函数return foo(10, 5)的值给bar(5),这样bar(5)执行结束,从调用栈中移除。
- 最后
console.log(bar(5));
打印出15值,console.log调用完成,将console.log移除栈。这时调用栈就为空了,调用结束。
执行顺序请参考下图:
Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1
异步调用的事件循环机制
我们以setTimeout 为例, 有这样代码,用两个不同的timeout来阐述setTimeout的事件循环机制。
console.log("1");
setTimeout(function secondTimeout() {
console.log("2");
}, 5000);
setTimeout(function firstTimeout() {
console.log("3");
}, 0);
console.log("4.");
以上代码运行后打印顺序是: 1, 4, 3, 2
以上代码执行机制如下:
- 代码运行的
console.log("1");
将其push到调用栈,紧接着完成,从调用栈删除
- 接着调用setTimeout的
secondTimeout
,将其push到调用栈,由于setTimeout是web API,因此web API 处理secondTimeout这个事件,并从调用栈中移除,并且在web API中开始等待5秒。
- 接着调用setTimeout的
firstTimeout
,将其push到调用栈,由于setTimeout是web API,因此web API 处理这个firstTimeout
事件, 由于等待是0秒,于是将firstTimeout
推送到回调队列,等待调用栈的执行(遵循先进先出
原则)。这时事件循环(event loop)机制开始运行,检查这时调用栈不为空?继续排队等待:把回调队列列表的第一个队列推送到调用栈运行。由于此时,调用栈还要继续执行console.log("4.");
所以继续等待调用栈结束。
- 接着
console.log("4.");
打印完成,这时已经是到代码执行尾部,没有主代码运行了,调用栈为空。事件循环机制检查到调用栈为空后,将第一个在callback Queue里的函数firstTimout() 推送到调用栈执行,并调用console.log(3)
。结束后释放调用栈。
- 当此时调用栈为空时,event loop继续将callback Queue例表里的其他队列一个一个push到调用栈去执行。如果此时回调队列例表里有多个队列排队,那么就安装此循环,直到全部完成为止。
promise的事件循环机制
参考资料
1. JavaScript’s Call Stack, Callback Queue, and Event Loop
2. Understanding Javascript Function Executions — Call Stack, Event Loop , Tasks & more — Part 1
3. Understanding the JavaScript call stack
4. 深入浅出js事件循环机制(上)
5. 深入浅出js事件循环机制(下)