说说 JS 运行机制

311 阅读7分钟

假如我是路边卖水果摊的小老板,遇到这个问题了,想尽快寻找答案。 靠!小老板啥都不知道,只看懂了前面 “说说” 两个字,还能怎么办?百度吧

1. 先查一下什么是 JS。

百度结果,小老板懂了,哦!JS 是一种脚本语言。ECMA国际组织为它制定了统一的 ECMAScrip 标准。现在主要被用于浏览器的web开发,以及服务器编程(Node.js)

  • 浏览器的JS:JS = ECMAScript + DOM + BOM
  • 服务器Node.js:JS = ECMAScript + os + file + net + database

2. 小老板重新定义了问题:“说说浏览器中 JS 的运行机制?”

小老板想了想:“靠!浏览器的本身怎么运行我都不知道,更别说浏览器里的 JS ”,于是只好继续百度。

一番查找后,小老板了解到,电脑里的应用程序,一般由多个进程组成,而每个进程又由多个线程组成。而浏览器中 JS 脚本的执行,就是一个线程负责,就是JS 引擎线程

3. 小老板又重新定义了问题:“说说浏览器中JS 引擎线程的运行机制?”

小老板悟性很高,立马想到,要深入去解析JS 引擎线程的运行机制,不如先了解一下,包含这个线程的进程是如何运行的。

这时小老板了解到浏览器的进程都有哪些了,其中JS 引擎线程就在渲染进程里面:

  • Browser进程
    • 浏览器的主进程(负责协调、主控),该进程只有一个
    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将渲染(Renderer)进程得到的内存中的Bitmap(位图),绘制到用户界面上
    • 网络资源的管理,下载等
  • 第三方插件进程
    • 每种类型的插件对应一个进程,当使用该插件时才创建
  • GPU进程
    • 该进程也只有一个,用于3D绘制等等
  • 渲染进程
    • 即通常所说的浏览器内核(Renderer进程,内部是多线程)
    • 每个Tab页面都有一个渲染进程,互不影响
    • 主要作用为页面渲染,脚本执行,事件处理等

4. 小老板又又重新定义了问题:“说说浏览器中渲染进程JS 引擎线程的运行机制?”

啥都不懂的小老板,只好继续百度了。先查一下渲染进程什么时候会工作。

简单了解了下浏览器中输入 url 到出现页面,发生的一系列反应:

  1. DNS 查询
  2. TCP 连接
  3. HTTP 请求即响应
  4. 服务器响应
  5. 客户端渲染 为了解答问题,小老板现在丝毫不关心前4点浏览器究竟干了什么。很显然第5点才是渲染进程用武之地。比如在第4点的时候,服务器响应了下面的html文件。
<!-- 小老板语气比较暴躁,请谅解~ -->
<!DOCTYPE html>
<html lang="en">
  <script>alert('Fire in the hold!')</script>
  <body>
    <div>Son of Bittch!</div>
  </body>
</html>

然后小老板又先进一步了解渲染进程具体的工作步骤:

  1. 处理 HTML 标记并构建 DOM 树
  2. 处理 CSS 标记并构建 CSSOM 树。
  3. 将 DOM 与 CSSOM 合并成一个渲染树。
  4. 根据渲染树来布局,以计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上。

以及渲染进程中的两个比较重要的线程:

  1. GUI渲染线程
    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
  2. JS引擎线程
    • JS引擎线程就是JS内核,负责处理Javascript脚本程序
    • GUI渲染线程与JS引擎线程是互斥的,JS 引擎线程会阻塞GUI渲染线程

小老板结合渲染进程的工作步骤和两个关联的线程分析得知:在解析上面的 html 文件的时候,JS 引擎线程会在第1点步骤的时候发挥作用,也就是GUI渲染进程解析到<script>alert('Fire in the hold!')</script>时,直接交给了JS 引擎线程处理了。(终于来到主角了。JS 执行阻塞DOM构建,小老板先放一边不管了)。

4. 小老板再次总结,又又又重新定义了问题:“说说浏览器中渲染进程JS 引擎线程的运行<script>alert('Fire in the hold!')</script>的机制?”

依旧是啥都不懂的小老板,发起了灵活拷问:计算机是如何读懂 Javascript 的?毕竟对猩猩而言,代码只不过是一串符号。

小老板想了一下,这有啥好说的,既然是单线程,代码只能一行接一行的执行。就直接弹窗 Fire in the hold! 呗。

这时,小老板意识到问题应该没那么简单。连夜学了几句 JS:

setTimeout(() => {
  console.log(0);
}, 1000);
new Promise((resolve) => {
  console.log(1);
  resolve();
}).then(() => {
  console.log(2);
});
console.log(3);

好家伙,打印顺序是:1320。根本不讲武德,说好的按顺序执行呢?

小老板这里理性分析了一波:受限于单线程JS 引擎线程只能同步执行脚本,这一点他确凿无疑。之所以不按顺序打印,肯定是执行的顺序变了。是什么导致执行的顺序改变呢?小老板经过百度得知,原来是JS 引擎线程的执行机制:Event Loop

5. 小老板再次归纳问题:“说说浏览器中渲染进程JS 引擎线程Event Loop的执行机制”

啥都不懂的小老板,经过多番研究(百度)。明白了Event Loop的机制的一些缘由。

1. 为啥要用Event Loop执行机制。

比如有个需求,需要点击页面后,可以领取红包。比如实现的脚本代码如下:

click(document.body) // 没有 Event Loop 的 JS,会卡住在这里,一直等待用户的点击
alert("Fuck you man!")
click(document.body) // 没有 Event Loop 的 JS,用户只能任由程序的摆布。
alert("给,你的红包!")

小老板对只能 “同步执行的JS” 卧槽了一句:“你在教我做事?” 小老板只能按照脚本的顺序操作,不然页面就卡住,毫无用户体验。为此Event Loop执行机制,应运而生!!

2. Event Loop机制原理。

为了解决第1点的问题,在Event Loop机制下,把任务分成两种:同步任务异步任务。 机智的小老板一下懂了,同步任务就是只能按顺序一个个地执行脚本。异步任务的脚本则是被通知可以执行的时候再执行。

为了实现异步任务的功能,Event Loop机制下有一个事件队列,当异步任务的脚本可以被执行的时候,事件队列尾部添加这个回调脚本。当JS 引擎执行栈中的代码执行完毕,就会去读取事件队列,将异步任务所对应的回调脚本添加到执行栈中执行。这个过程不断循环,所以称作:Event Loop

经过一番百度,小老板知道原来在渲染进程中,还有其他线程与JS 引擎线程协作。

渲染进程中其他的线程:

  • 事件触发线程

    • 控制事件循环,并且管理着一个事件队列(task queue)
  • 定时触发器线程

    • setInterval与setTimeout所在线程,用于计时并触发定时
    • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
  • 异步http请求线程

    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
  1. 为什么是单线程?
  • JS 之所以要单线程运行,受限于它在浏览器里用途。试想一下,如果有两个js线程同时操作 Dom,一个要删除节点,一个要修改这个节点,应以哪个为准?
  1. 单线程如何处理异步任务?

    • 讲道理单线程的 JS 只能一个接一个的按顺序执行任务。但是在浏览器中,用户交互,数据请求的操作,要求 JS 要在特定的时刻执行特定的代码。因此需要其他线程的共同协作。

JS 线程把这些任务交给 “事件触发线程”,事件触发线程管理着一个任务队列,一旦其中一个任务有了结果,就会添加一个事件回调。等 JS 线程的同步任务执行完毕,系统就会去查询任务列表的事件回调,把可以执行的任务添加到执行栈当中运行。

站在巨人的肩膀