本文以JavaScript 执行为主线展开,包括 回调队列,DOM 渲染。
JavaScript 是单线程(Web Worker 可以实现多线程) 自上而下依次执行的,先执行同步任务,遇到异步任务会丢到回调队列(call back Queue)中排队执行。
浏览器里里有一个运行JavaScript 的环境 – 叫JavaScript 引擎,比如 Chrome 使用的是 V8 引擎,它可以解释并执行JavaScript 代码,Node.js 的运行环境也是 V8 引擎,它同时还提供一个内置模块和 API 给程序员调用。

Call Stack
是否为空,为空就把回调队列的任务取出来放到调用栈执行。
Web APIs
如果你下载了 V8 引擎的代码,你去搜索 setTimeout, DOM, XMLHttpRequest, fetch 这些东西,它们并不在里面,它们属于Web APIs,node.js 里就没有这些 API 给你调用。
当引擎遇到 setTimeout
代码时:

- 引擎将代码送到 Call Stack
- Call Stack setTimeout 设定的回调函数和定时时间推给对应 Web API
- 计时器开始计时
- setTimeout 设定的时间到了,Web API 再把 setTimeout 设定的回调函数丢进宏任务队列
- 排队等待
- 轮到这个宏任务了,Event loop 把它交给 Call Stack
当引擎遇到事件监听代码(addEventListener
)时:
- 引擎将代码送到 Call Stack
- Call Stack 把它丢给Web API
- 等待触发事件
- 事件被触发,Web API 再把回调函数丢进宏任务队列并等待再次被触发或移除监听(removeEventListener)
- 排队等待
- 轮到这个宏任务了,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
如果Promise 没有被 resolve 或reject,保持pending 状态会导致内存泄漏。

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
}

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