一.调用堆栈
1.1
调用堆栈是一种机制,用于解释器(如web浏览器中的JavaScript解释器)跟踪其在调用多个函数的脚本中的位置—当前正在运行的函数以及从该函数中调用的函数等。
- 当脚本调用函数时,解释器将其添加到调用堆栈中,然后开始执行该函数。
- 由该函数调用的任何函数都会添加到更高的调用堆栈中,并在到达它们的调用的地方运行。
- 当前函数完成后,解释器将其从堆栈中取出,并在最后一个代码列表中停止的位置继续执行。
- 如果堆栈占用的空间超过了分配给它的空间,则会导致“堆栈溢出”错误。
总之,我们从一个空的调用堆栈开始。无论何时调用函数,它都会自动添加到调用堆栈中。一旦函数执行了它的所有代码,它就会自动从调用堆栈中删除。最终,堆栈又是空的。
1.2
JavaScript 引擎说起来最流行的是谷歌的 V8 引擎, V8 引擎使用在 Chrome 以及 Node 中。这个引擎主要由两部分组成:
内存堆:这是内存分配发生的地方
调用栈:这是你的代码执行时的地方
JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事。
并发与事件循环。当调用栈中的函数调用需要大量的时间来处理,浏览器就不能做任何事,它会被堵塞住。这意味着浏览器不能渲染,不能运行其他的代码,它被卡住了。
如何在不阻塞 UI 的情况下执行复杂的代码,让浏览器不会不响应?解决方案就是异步回调。
如果你在 JavaScript 应用程序中难以复现和理解问题,请查看 SessionStack 。 SessionStack 会记录你的 Web 应用中的所有东西:所有的 DOM 更改、用户交互、JavaScript 异常、堆栈跟踪、网络请求失败、调试消息等。
通过 SessionStackSessionStack ,你可以以视频的方式重现问题,并查看发生在用户身上的所有事情。
1.3
JavaScript 中有三种执行上下文类型。
- 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。 一个程序中只会有一个全局执行上下文。
- 函数执行上下文 — 每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
- Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。
JavaScript 引擎是怎样创建执行上下文的。创建执行上下文有两个阶段:1) 创建阶段 和 2) 执行阶段。在创建阶段会发生三件事:
- this 值的决定,即我们所熟知的 This 绑定。
- 创建词法环境组件。
- 创建变量环境组件。
ExecutionContext = {
ThisBinding = <this value>,
LexicalEnvironment = { ... },
VariableEnvironment = { ... },
}
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。
1.4
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
- ajax进入Event Table,注册回调函数success。
- 执行console.log(‘代码执行结束’)。
- ajax事件完成,回调函数success进入Event Queue。
- 主线程从Event Queue读取回调函数success并执行。
setTimeout
这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。
setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
setInterval
会每隔指定的时间将注册的函数置入Event Queue
于setInterval(fn,ms)
来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue
。一旦setInterval
的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
- macro-task(宏任务):包括整体代码
script,setTimeout,setInterval
- micro-task(微任务):
Promise,process.nextTick
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 段代码作为宏任务,进入主线程。
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
遇到console.log(),立即执行。 - 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
- ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event
Queue中setTimeout对应的回调函数,立即执行。 - 结束。