# 执行机制
# 引擎
Google 的 V8 引擎作为示例,该引擎主要包含两个组件:
Memory Heap内存堆,这是内存分配发生的地方Call Stack调用堆栈,这是在代码执行时栈帧存放的位置
# javascript 的单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种:
- 同步任务(
synchronous)在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务 - 异步任务(
asynchronous)不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
常见的异步任务有Ajax操作、定时器(setTimeout/setInterval)、UI事件(load(图片js文件的加载等)、resize、scroll、click等)。
# Event-Loop(事件循环)
上面的图具体来说,运行机制如下:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入
Event Table并注册函数。 - 当指定的事情完成时,
Event Table会将这个函数移入Event Queue。 - 主线程内的任务执行完毕为空,会去
Event Queue读取对应的函数,进入主线程执行。 - 上述过程会不断重复,也就是常说的
Event Loop(事件循环)。
怎么判断栈空:引擎存在 monitoring process 进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里检查是否有等待被调用的函数。
例子:
let data = [];
$.ajax({
url: www.javascript.com,
data: data,
success: () => {
console.log("发送成功!");
},
});
console.log("代码执行结束");
2
3
4
5
6
7
8
9
上面是一段简易的 ajax 请求代码:
ajax进入Event Table,注册回调函数success。- 执行
console.log('代码执行结束')。 ajax事件完成,回调函数success进入Event Queue。- 主线程从
Event Queue读取回调函数success并执行。
# 调用栈
所有任务都在主线程上执行,形成一个执行栈(execution context stack)
执行栈是一种数据结构,如果执行一个函数就会把该函数放到栈顶,当函数返回的时候,就会将这个函数从栈顶弹出:
function m(x, y) {
return x * y;
}
function p(x) {
var s = m(x, x);
console.log(s);
}
p(5);
2
3
4
5
6
7
8
当程序开始执行的时候,栈是空的,然后步骤如下:
- p 入栈
- m 入栈
- m 执行完出栈
- p 执行完出栈
- 栈空
每一个进入栈的过程都称为调用帧
当你达到调用栈最大的大小的时候就会发生 栈溢出,而且这相当容易发生,特别是在你写递归的时候却没有全方位的测试它,这个时候浏览器会抛出异常。
# 宏任务和微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
macro-task(宏任务):包括整体代码,setTimeout,setIntervalmicro-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");
2
3
4
5
6
7
8
9
10
11
- 这段代码作为宏任务,进入主线程。
- 先遇到
setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。 - 接下来遇到了
Promise,new Promise立即执行,then函数分发到微任务Event Queue。 - 遇到
console.log(),立即执行。 - 好啦,整体代码作为第一个宏任务执行结束,看看有哪些微任务?我们发现了
then在微任务Event Queue里面,执行。 - 第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务
Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。 - 结束。
