目录
JavaScript 引擎
JavaScript 引擎说起来最流行的当然是谷歌的 V8 引擎了, V8 引擎使用在 Chrome 以及 Node 中
这个引擎主要由两部分组成:
内存堆:
这是内存分配发生的地方。当V8引擎遇到变量声明和函数声明的时候,就把它们存储在堆里面。
调用栈:
这是你的代码执行时的地方。当引擎遇到像函数调用之类的可执行单元,就会把它们推入调用栈。
JS单线程,指的是在JS引擎中,解析执行JS代码的调用栈是唯一的,所有的JS代码都在这一个调用栈里按照调用顺序执行,不能同时执行多个函数。
运行时
我们可以把JS的运行时环境看作一个大的容器,里面有一些其他的小容器。当JS引擎解析代码时,就是把代码片段分发到不同的容器里。
Web API
(保存一些不在主线程中立即执行的代码片段,有可能在一个分线程中,也有可能在多个分线程中)
还有很多引擎之外的 API,我们把这些称为浏览器提供的 Web API,比如说 事件监听函数、DOM、HTTP/AJAX请求、setTimeout等等。
事件循环:
持续的检测调用栈和回调队列,如果检测到调用栈为空,它就会通知回调队列把队列中的第一个回调函数推入执行栈。
回调队列:
按照先进先出的顺序存储所有的回调函数。在任意时间,只要Web API容器中的事件达到触发条件,就可以把回调函数添加到回调队列中去。
示例:
setTimeout(function(){
console.log('Hey, Why am I last?')
}, 0)
function sayHi(){
console.log('Hello')
}
function sayBye(){
console.log('Goodbye')
}
sayHi()
sayBye()
执行过程是这样的:
- JS引擎会检查整段代码的语法错误,如果没有错误,就从头开始深度解析
- 首先遇到setTimeout函数调用,把它推入执行栈顶
- 解析函数体,发现setTimeout函数是Web API的一种,因此就把它分发到Web API模块然后推出栈
- 因为定时器设置了0ms延迟,因此Web API模块立即把它的匿名回调函数推入到回调函数函数队列。事件循环检测执行栈是否是空闲,但是当前栈并不空闲,因为…
- 当setTimeout函数一被分发到Web API模块,JS引擎发现了两个函数声明,把它们存储在堆内存里,然后遇到了sayHi函数的调用,就把它推入了栈顶
- sayHi函数调用了console.log函数,因此console.log就被推入了栈顶
- JS引擎开始解析console.log的函数体,它接收了一个消息去打印‘Hello’,然后被弹出栈
- JS引擎返回到函数sayHi的执行,遇到函数的结束符号}之后,把sayHi弹出栈
- sayHi函数一出栈,紧接着sayBye函数被调用,它就被推入栈顶,被解析,调用console.log,把console.log推入栈顶,打印一条消息,弹出栈。然后sayBye函数弹出栈
- 事件循环检测到执行栈终于空闲了,通知回调队列,然后回调队列把其中的匿名函数推入执行栈
- 匿名函数(就是setTimeout的回调函数)被解析,调用console.log,console.log推入栈顶
- console.log执行完毕、再出栈
- 匿名函数再被推出栈,程序结束
调用椎栈
可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
当开始执行 JS 代码时,首先会执行一个 main 函数,然后执行我们的代码。
根据先进后出的原则,后执行的函数会先弹出栈,在图中我们也可以发现,foo 函数后执行,当执行完毕后就从栈中弹出了。
平时在开发中,也可以在报错中找到执行栈的痕迹
function foo() {
throw new Error('error')
}
function bar() {
foo()
}
bar()
可以在上图清晰的看到报错在 foo 函数,foo 函数又是在 bar 函数中调用的。
当我们使用递归的时候,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现栈溢出。