JavaScript 的执行顺序 回调队列

本文以JavaScript 执行为主线展开,包括 回调队列,DOM 渲染。

JavaScript 是单线程(Web Worker 可以实现多线程) 自上而下依次执行的,先执行同步任务,遇到异步任务会丢到回调队列(call back Queue)中排队执行。

浏览器里里有一个运行JavaScript 的环境 – 叫JavaScript 引擎,比如 Chrome 使用的是 V8 引擎,它可以解释并执行JavaScript 代码,Node.js 的运行环境也是 V8 引擎,它同时还提供一个内置模块和 API 给程序员调用。

Event Loop 🔄 不断的轮询 Call Stack 是否为空,为空就把回调队列的任务取出来放到调用栈执行。
回调队列的任务分微作务和宏任务两种,微任务优先执行。

Web APIs

如果你下载了 V8 引擎的代码,你去搜索 setTimeout, DOM, XMLHttpRequest, fetch 这些东西,它们并不在里面,它们属于Web APIs,node.js 里就没有这些 API 给你调用。

当引擎遇到 setTimeout 代码时:

  1. 引擎将代码送到 Call Stack
  2. Call Stack setTimeout 设定的回调函数和定时时间推给对应 Web API
  3. 计时器开始计时
  4. setTimeout 设定的时间到了,Web API 再把 setTimeout 设定的回调函数丢进宏任务队列
  5. 排队等待
  6. 轮到这个宏任务了,Event loop 把它交给 Call Stack

当引擎遇到事件监听代码(addEventListener)时:

  1. 引擎将代码送到 Call Stack
  2. Call Stack 把它丢给Web API
  3. 等待触发事件
  4. 事件被触发,Web API 再把回调函数丢进宏任务队列并等待再次被触发或移除监听(removeEventListener)
  5. 排队等待
  6. 轮到这个宏任务了,Event loop 把它交给 Call Stack

Node.js 有另外的API:

Call Stack 与 Heap

JS 引擎包含一个Call Stack(函数调用栈)和一个Heap(堆),Heap 用来分配内存,Stack 用来处理函数调用。

JS 中,原始值类型的数据,都以线的形式存在 Call Stack 中,每个变量在Call Stack 中都有一个地址。而引用值类型的数据,是以堆的形式存在 Heap 中,然后在 Call Stack 存储它在 Heap 中的地址,在函数执行完毕之后,如果没有任何变量引用这个引用值,那么这个对象会被标记为垃圾,由垃圾回收机制回收内存空间。

当 JS 引擎拿到我们的脚本后,它做的第一件事就是为我们代码中的数据设置内存。

Promises 和 Async/Await

.then() 中的回调函数在 Promise 被解决后被调用
.catch() 中的回调函数在 Promise 被拒绝或发生错误后被调用

如果Promise 没有被 resolve 或reject,保持pending 状态会导致内存泄漏。

.then() 和 .catch 本身都返回 Promise 对象,所以能形成 Promise 链。
当 引擎遇到 Promise 方法,Promise 对象本身是同步执行,当Promise 对象状态变为 fulfilled 或 rejected,那么 .then 方法中的回调函数会被丢进微任务队列中。
遇到 await 关键字时,async 右边的表达式会先执行,然后把剩余的代码封装成一个新的Promise 对象丢到微任务队列中排队。

Promise.all() 只要一个状态变为rejected,则整个 Promise.all() 的状态也将被拒绝,并且拒绝的值将是被拒绝的 Promise 对象的拒绝原因(rejection reason)

JS 操作 DOM 是同步的,但 DOM 的渲染通常是异步的

看这个要循环很久的耗时代码。

msg.textContent = 'Hello World'
for(let i=0; i<10000000000; i++) {
  let j = i * i
}
这个循环执行了15秒后才在页面上看到Hello World,为什么?

JS 首先执行 msg.textContent = ‘hello world’,修改内存中的 DOM 是同步的,但浏览器的运行逻辑并不是你在JS 里修改下DOM 就去立马去渲染,然后执行下一行JS 代码(查看《浏览器是怎么工作的》)。

通常是执行完脚本(Evaluate Script)上应该执行的全部代码,然后就会有Recalculate Style 构建 Render Tree,然后Layer 阶段构建盒模型,然后Paint , Commit 生成一个由多个图层(Layer)组成的位图(Bitmap),最后Composite 合成最终的位图,显示在屏幕上,而这个过程是耗时耗资源的。

如果执行一行操作DOM 代码,就渲染一下,对资源的消耗是不经济的,自然会卡顿。

React 和 Vue 都是使用虚拟 DOM 技术通过优秀的 diff 算法大大提高页面的渲染性能。

Ref:

https://dev.to/lydiahallie/javascript-visualized-event-loop-3dif