JavaScript有一个基于事件循环的运行时模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
运行时概念
一个运行时有且只有一个调用栈、消息队列、堆。
Stack调用栈
- 当前语句会先进栈,处理完再出栈。
- 函数调用嵌套回调函数,每一个被调用的函数都会生成一个函数作用域并放置于调用栈顶
- 处理当前函数的“指针”总是指向栈顶作用域,所以每时每刻都只会处理栈顶的函数作用域
- 栈顶函数作用域处理完后“指针”依次下移,栈顶作用域失去“指针”后,会被浏览器垃圾回收机制标记成为待回收数据或直接清除。
function foo(b) {
const a = 10;
return a + b + 11;
}
function bar(x) {
const y = 3;
return foo(x * y);
}
const baz = bar(7); // 将 42 赋值给 baz
上面的示例方法中,当前函数栈从顶到底压入了foo/bar两个函数的状态帧。具体执行顺序为:
- 当调用
bar时,第一个帧被创建并压入栈中,帧中包含了bar的参数引用和局部变量。 - 当
bar调用foo时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含foo的参数引用和局部变量。 - 当
foo执行完毕然后返回时,第二个帧就被弹出栈(剩下bar函数的调用帧)。 - 当
bar也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。
Heap堆
对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
Queue队列
- JavaScript运行时包含一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
- 一个消息会激活一个函数栈,并开始栈的堆叠,直到所有的回调函数都触发后,栈里面的帧开始逐个被清除,直到栈被清空,当前消息就算执行完成。
- 每个消息中可能有宏任务与微任务被触发,当前消息队列中的微任务会按照触发顺序排列在消息队列末尾,和当前消息队列的任务一同执行;宏任务将生成一个新的消息队列,并根据延迟时间安插最近的消息队列后执行。
事件循环概念
事件循环的执行原理:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
事件循环的一些注意事项:
- 完整执行:每一个消息完整地执行后,其他消息才会被执行。
- 优点:当一个函数执行时,不需要考虑这段函数所使用的参数被其他函数同时使用着,可以直接读、写需要的参数。
- 缺点:单线程模式,遇到占用时间长的消息,将会彻底阻塞当前应用,直到该消息完成后,才能继续进行应用流程。
- 引起消息添加的事件:事件监听器被触发、setTimeout计时器到期之后回调函数被执行。
- setTimeout的0ms延迟:等待时间并不为0,而是取决于队列里待处理的消息数量。
- 多个运行时如何互相通信:Web worker或者跨源的iframe都有自己的栈、堆和消息队列。两个不同的运行时只能通过postMessage方法进行通信。如果另一个运行时侦听message事件,则此方法会向该运行时添加消息。
事件循环的永不阻塞
由于事件循环的异步任务总是会给同步任务让行,所以在一定程度上是“永不阻塞的”,也就是每一份代码总能被执行,除非代码报错了。