浏览器渲染过程及JS引擎浅析

栏目: JavaScript · 发布时间: 5年前

内容简介:文章目录相信大家都听过一道经典的面试题:“在浏览器输入URL后回车之后发生了什么”,我一直想解答这个问题,不过这个题目涉及的知识面非常广,想要解答需要一定的知识储备。这篇文章我们讨论这个问题中的一部分,当浏览器拿到服务器传回的在进入正题之前我们来想几个问题,然后在跟着问题的脚步来分析:
  • Home
  • Programming >Front end >Javascript
  • 浏览器渲染过程及JS引擎浅析

文章目录

前言

本文由于作者水平有限,肯定有错误之处,如果你看到,希望能够指出,感谢。

相信大家都听过一道经典的面试题:“在浏览器输入URL后回车之后发生了什么”,我一直想解答这个问题,不过这个题目涉及的知识面非常广,想要解答需要一定的知识储备。这篇文章我们讨论这个问题中的一部分,当浏览器拿到服务器传回的 html 文档后如何处理文档然后呈现在显示器上呢?

在进入正题之前我们来想几个问题,然后在跟着问题的脚步来分析:

1. 我们都知道DOM,CSS的加载和渲染和JS的执行之间存在阻塞,阻塞发生的时候浏览器会下载其他页面资源吗

2. 都说JS是单线程的,那么Event Loop线程是什么呢,为什么JS要设计成单线程呢

3. setTimeout是如何执行的,为什么setTimeout的delay和执行时间不同

4. 浏览器的具体渲染流程是什么样的

希望我写完这篇文章和你看完这篇文章之后都能解答这几个问题。

浏览器

在解决问题之前,我们要先了解我们的对象:浏览器。浏览器的功能很简单,就是根据我们给出的URI,替我们向服务器发出请求,获取服务器上的资源并在展示给我们。这里的资源主要是HTML文档也可以是图片,pdf或其他类型的文件,取决于我们给出的 URI 采取的协议以及请求的文件类型。

浏览器的主要组件

  1. 用户界面(The userinterface):包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  2. 浏览器引擎(The browser engine):用户页面和渲染引擎的中间层,负责在用户界面和渲染引擎之间传递信息。
  3. 渲染引擎(The rendering engine):负责请求显示的内容。比如请求的是HTML文档,那么渲染引擎负责解析HTML和CSS,并将解析的结果显示到显示器上。
  4. 网络组件:用于网络调用,比如http请求。
  5. 用户界面后端(UI backend):用于绘制一些小部件。
  6. JavaScript解释器(JavaScript Interpreter):解析并执行JavaScript代码。
  7. 数据存储(Data Storage):浏览器需要保存各种类型的数据到本地,比如cookies, localStorage, IndexedDB, WebSQL and FileSystem.

这些组件之间是如何配合工作的呢,我们都知道 chrome 是内存杀手,那么我们打开的多个 tab 又是如何分配资源的,不同的tab之间会争夺资源吗?

进程与线程

很多前端开发人员都知道 JavaScript 是单线程语言,但具体是什么单线程呢,其实说的是JS引擎,也就是上面的JavaScript解释器是单线程的,那么这个单线程到底是什么意思呢,我们要来说说线程和进程的概念和关系,以及浏览器中的进程与线程。

进程和线程在大学的操作系统课程上讲的很多,不过可能很多同学忘记了,那么我们来说说进程和线程的概念。

我们的计算机中的所有计算都是在cpu中完成的,cpu的计算速度非常快,大部分的设备是完全跟不上cpu的速度的,那么怎么办呢。用金字塔式存储体系来根据信息处理的紧急程度分开存储,它们一般从下到上越来越小,越来越快,越来越贵。如下图:

大部分人都知道计算机有硬盘和内存,而我们的软件一般就安装在硬盘中,在硬盘中的数据是我们最不急需处理的,它们就静静地呆在那里。当我们想要运行一个程序,这些文件就被装载到内存中去了,而对于内存中最急需处理的文件会被传输到CPU的高速缓存中,也就是我们买CPU的时候会看到三级,二级,一级缓存,而在这些缓存中的文件最后会被以此传到CPU的寄存器中让CPU执行,只有寄存器的速度勉强能跟上CPU。

但是我们可以同时开很多软件,同时做很多工作,比如我可以一边听着音乐,一边修改着Word,同时还在后台开着浏览器后台播放着直播,不仅如此,我们的操作系统还有很多程序需要运行。那么它们是按照什么顺序进CPU运行的呢。我们前面已经说过CPU非常快,所以当我们的程序进入CPU运行的时候,其他资源比如显卡都应该就位了,这些资源就构成了程序执行的上下文。这个程序执行上下文就是我们的进程,也就是操作系统分配系统资源的最小单位,说白了进程就是管理程序和计算机资源之间的分配关系手段。当我们的程序终于等到能够进入CPU运行的机会,先装载执行上下文,然后执行程序,程序执行完成或系统分配的时间结束,就必须保存执行上下文,让排队的下一个进程进入计算。CPU就不断重复着装载上下文,执行程序,保存上下文的过程。

那么线程是什么呢,当我们的程序终于获得CPU的临幸,我们当然希望我们的程序能尽快执行完成。如果我们的程序只有一个逻辑要执行,那么我们其实只需要一个线程就可以了,但如果有几个并行的任务需要执行,我们可以借助多核CPU同时开多个线程,在一个执行上下文中共享系统分配的资源,来协同更快地完成任务。(单核CPU也有多线程,操作系统在不同的线程之间快速切换,在进程间交替运行,减少CPU闲置的时间)

操作系统对进程的处理和资源的分配会复杂很多,我们只是了解一下大致的概念。阮一峰的博客有一篇更形象一点的比喻,大家可以借助比喻帮助理解: 进程与线程的一个简单解释

浏览器的进程和线程

现在大家应该已经对进程和线程有一定的了解了,那么我们来说一说浏览器的进程和线程,首先我们要说,浏览器程序是多进程的,我们的每一个标签页(tab)都是一个独立的进程。我们可以打开 Chromemore tools 中的 task manager ,就可以看到当前浏览器的进程。

从途中我们可以看到我们的每一个标签页都是一个进程,对应的 pid 在Mac的任务管理器中也都能看到,不过除了标签页的进程,我们还看到了比如 GPU ProcessBrowser 等进程,那么我们浏览器到底有哪些进程呢?我们结合上面浏览器的主要组件来看(以 Chrome 为例):

1. Browser进程:控制chrome应用界面的一些组件,比如地址栏,书签栏,前进后退按钮等。还控制一些浏览器的不可见部分,比如网络请求和文件访问等。

2. Renderer进程(浏览器内核):控制标签页内部网页要显示的一切,也就是我们访问的内容都是有渲染引擎控制,比如页面渲染,脚本执行,事件的处理。

3. Plugin进程:控制网站应用的插件,比如flash。

4. GPU进程:独立于其他进程处理GPU任务,它被独立出来是因为GPU处理来自不同程序的请求。

第四点翻译的可能有点问题,原文 Inside look at modern web browser ,这个系列文章非常不错,推荐大家看看。

多进程什么好处呢,最简单的比方,我打开了三个tab浏览三个网页,每个页面都有单独的渲染进程,如果其中一个页面的代码非常糟糕崩溃了,那么你的另外两个页面不会受到影响,你依然可以继续浏览。而如果三个页面共享一个进程,那么你的两外两个页面也将崩溃。

不过正因为如此,chrome简直是内存杀手,相信大家都有体会。

渲染引擎

对于我们前端来说,最重要的就是渲染引擎以及它工作的渲染进程,这是我们打交道最多的地方,学期其他的浏览器知识能让我们更清楚渲染引擎在浏览器中的定位和工作流程。渲染引擎也就是我们经常说的浏览器内核。渲染进程是多线程的,那到底有哪些线程,分别做什么工作呢?

  1. GUI渲染线程
    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
    • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  2. JavaScript引擎
    • 负责处理Javascript脚本程序。(例如V8引擎),JS引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。
  3. 事件触发线程
    • 管理Event Loop,Event Loop的标准是在 HTML5 中,是渲染引擎的一个线程来处理,所以并不和JS单线程执行矛盾。
    • 当一个事件被触发时,该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系,所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。
  4. 定时器触发线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确),因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)。W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  5. 异步请求线程
    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

其中前三个是常驻线程,是所有浏览器内核必须实现的,后两个线程执行完就会终止。

关于JS引擎为什么是单线程的,因为大部分程序控制UI的都会是单一单线程,因为JS主要使用场景是与用户交互和操作DOM,如果两个线程同时操作一个DOM会很复杂。而H5也提供了多线程方法 web worker ,可以创建多个线程,但是子线程完全受控于主线程且不得操作DOM。

渲染过程

浏览器接收到服务器返回的HTML文档后就开始解析并渲染HTML文档,主要流程如下:

1. 解析HTML文档,将元素按层次转化成一棵DOM树,根节点为 document

2. 解析CSS样式文件(包括外部CSS文件和样式元素以及js生成的样式),获取样式数据。

3. 结合样式和DOM树计算出节点的样式,生成渲染树(render tree),渲染树包含多个带有样式属性的矩形,这些矩形的排列顺讯就是它们在屏幕上显示的顺序。

4. 进入布局阶段,从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标。

5. 遍历渲染树,每个节点将使用UI后端层来绘制。

对于渲染引擎的渲染细节感兴趣的同学可以看看这篇文章, How browsers work ,中文翻译的不是很好,推荐大家结合英文看。

Reflow和Repaint

  1. Reflow: 对于DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式(浏览器的、开发人员定义的等)来计算并根据计算结果将元素放到它该出现的位置。
  2. Repaint: 当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了。

我们可以发现 Reflow 对应的是渲染过程中的第四步,而 Repaint 对应的是渲染过程的第五步。直白一点说就是当DOM被修改后需要重新计算渲染树 render tree 的一部分或者全部的时候,我们就需要 Reflow ,而如果元素的修改不影响渲染树,那么只要 Repaint 就可以了。

显而易见, Reflow 的成本要比 Repaint 高得多, DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow 。以下这些行为会触发 Reflowrepaint

  • 删除,增加,或者修改DOM元素节点。
  • 移动DOM的位置,开启动画的时候。
  • 修改CSS样式,改变元素的大小,位置时,或者将使用display:none时,会造成reflow;修改CSS颜色或者visibility:hidden等等,会造成repaint。
  • 修改网页的默认字体时。
  • Resize窗口的时候(移动端没有这个问题),或是滚动的时候。
  • 内容的改变,(用户在输入框中写入内容也会)。
  • 激活伪类,如:hover。
  • 计算offsetWidth和offsetHeight。

现在的浏览器已经对渲染的过程尽可能的优化,不过我们还是可以在编码的过程只能够注意一些细节:

  • 尽量避免style的使用,对于需要操作DOM元素节点,重新命名className,更改className名称。
  • 如果增加元素或者clone元素,可以先把元素通过documentFragment放入内存中,等操作完毕后,再appendChild到DOM元素中。
  • 不要经常获取同一个元素,可以第一次获取元素后,用变量保存下来,减少遍历时间。
  • 尽量少使用dispaly:none,可以使用visibility:hidden代替,dispaly:none会造成重排,visibility:hidden会造成重绘。
  • 不要使用Table布局,因为一个小小的操作,可能就会造成整个表格的重排或重绘。
  • 使用resize事件时,做防抖和节流处理。
  • 对动画元素使用absolute / fixed属性。
  • 批量修改元素时,可以先让元素脱离文档流,等修改完毕后,再放入文档流。

DOM, CSS, JS的阻塞

渲染过程并不像上面描述的那么简单,特别是页面的绘制是一件开销非常大的事情,所以浏览器尽量最有效率的绘制,避免那些没必要的重绘和回流。要避免这个问题,DOM加载和解析,CSS加载和解析,和JS的加载执行之间的顺序和阻塞就非常重要,如果处理不当,页面很可能要重绘。因为渲染树是DOM和CSS结合生成的,而JS可以操作DOM和样式,必须处理好三者之间的逻辑。

在分析具体情况之前我们先说两个事件 DOMContentLoadedloadload 事件大家都很熟悉, wiindow.onload 相信大家都用过,在页面加载完成后会触发 load 事件,此时,在文档中的所有对象都在DOM中,所有图片,脚本,链接以及子框都完成了加载 。而 DOMContentLoaded 事件当初始的 HTML 文档被完全加载和解析完成之后就会被触发,而无需等待样式表、图像和子框架的完成加载。

  1. 外部资源文件的下载不会被阻塞(js,css,image)

    我们的HTML文档一般都会包含外部链接,引入资源文件,包括js,css,图片等,我们的主线程会一个一个的请求这些链接,不过现代浏览器一般会有一个预加载监视器 preload scanner 来加速这些链接的下载,因为我们的文档解析需要事件,所以提前发送请求获取资源文件会提高效率。这个预加载监视器会找到 <img><link> 之类的标签,发送给网络线程请求资源。

  1. CSS不会阻塞DOM解析

    这一点其实很好理解,CSS不会引起DOM的变化,它们两个只要尽早解析完成,然后生成渲染树就可以了,并行加载并不影响。我们可以设计一个场景模拟出这个状态,现在chrome中把 network throttling 设置一个较小的数值(我设置的50kb/s),然后加载一个稍大的CSS,然后在css之前用一个 defer 属性的js获取页面上的DOM( defer 属性表示这个js会在 DOMContentLoaded 事件发生后立即执行),如果能够在css加载好之前获取 dom 节点,说明CSS是不会阻塞DOM解析的。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>css-dom-parse</title>
    <script defer src="css-dom-parse.js"></script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
</head>
<body>
    <div>test</div>
</body>
</html>
//css-dom-parse.js
const div = document.querySelector('div');
console.log(div);

结果如下图

此时我们的css还没下载完成,但是我们可以看到 console 选项卡里面 div 标签已经被打印出来了,说明,此时DOM已经解析完成触发 DOMContentLoaded 事件。

  1. CSS会阻塞DOM渲染
    因为DOM的渲染需要DOM树和CSSOM树共同来生成渲染树,所以在CSS加载完成之前,DOM是不会进行渲染的。还是用上面那个例子,我们会发现CSS没有加载完成时,页面上是不显示 test 的,而当CSS加载完成的瞬间,标签就被渲染到页面上了。
  2. CSS会阻塞JS

    这一点可能大家有点疑惑,但是仔细想一想,我们的脚本可以在文档解析阶段请求样式信息,如果此时我们的样式还没有加载和解析,那么脚本必然会获得错误的结果,这样会产生很多问题,目前的浏览器做法就是在CSS加载解析的过程中会阻塞JS的加载执行。虽然我们不常遇到这样的情况,但是也要清楚为什么会发生这样的情况。我们还用刚刚的方法,不过这次我们需要快速看到结果,可以把速度设置的快一点, network throttling 设置为 slow 3G ,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>css-dom-parse</title>
    <script>
        var starttime = new Date().getTime();
        console.log("page start" + starttime);
    </script>
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
    <script src="css-dom-parse.js"></script>
</head>
<body>
    <div>test</div>
</body>
</html>
var endtime = new Date().getTime();
console.log("delay:" + (endtime - starttime));

我们在CSS加载之前记录事件并保存到全局变量 starttime 中,然后在CSS之后加载一个JS文件,当这个JS文件执行的时候,我们输出执行时间并计算时间差。结果如下图:

我们把时间线拖到页面加载的最开始,我们发现html和js都已经下载完毕,但是css的请求还没有完成,而我们在看看输出的时间,我们的js是在 2487ms 后才执行,正是css加载完成后才执行的js。

  1. JS会阻塞DOM解析

    当解析起遇到 script 标签时,文档会立即停止解析知道脚本执行完毕,如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。因为我们的脚本会操作DOM,所以在脚本跑完之前浏览器不知道脚本会把DOM改成什么样,所以就等脚本执行完再进行解析。测试这个也很简单,我们用一个有 defer 属性的js在 DOMContentLoaded 的时候输出内容,然后再写一个一定时常的死循环,看看输出结果。代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>css-dom-parse</title>
    <script>
        var starttime = new Date().getTime();
        console.log('starttime' + starttime);
    </script>
    <script defer src="domcontentloaded.js"></script>
    <script src="css-dom-parse.js"></script>
</head>
<body>
    <div>test</div>
</body>
</html>
//domcontentloaded.js
console.log("DOMContentLoaded! " + (new Date().getTime() - starttime));

//css-dom-parse.js
var now = new Date().getTime();

while (new Date().getTime() - now < 3000) {
    continue;
}
console.log("time out" + new Date().getTime());

结果如下图:

可以看出 DOMContentLoaded 事件一直等到JS执行完成以后才触发。

  1. 浏览器解析到script标签会立即触发一次渲染

    这个机制可能很多朋友不是很清楚,当我们的浏览器在解析文档的过程中遇到 script 标签,他会立即把已经解析的部分渲染了,这当然是个比较极端的情况,一般良好的代码不会遇到。大概是因为解析到一半的时候渲染树还没有,如果此时JS要操作的DOM的话,浏览器不知道如何处理,只能先把当前内容渲染了。测试代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Test</title>
    <script>
      console.log("page start" + new Date().getTime());
    </script>
    <style>
      div {
        background: lightblue;
        width: 500px;
        height: 500px;
      }
    </style>
  </head>
  <body>
    <div>test3</div>
    <script src="js/test.js"></script>
    <style>
      div {
        background: lightgray;
      }
    </style>
    <script src="js/test1.js"></script>
    <style>
      div {
        background: lightpink;
      }
    </style>
  </body>
</html>
//test.js 和 test1.js相同,都是执行三秒
var now = new Date().getTime();

while (new Date().getTime() - now < 3000) {
    continue;
}
console.log("js11 start" + new Date().getTime());

执行后我们会发现 div 先被渲染成 lightblue ,三秒后被渲染成 lightgray ,再过三秒被渲染成 lightpink

CSS虽然不会阻塞DOM,但是如果在CSS后有一个JS,CSS会阻塞这个JS,而这个JS会阻塞DOM,所以有时候会造成CSS阻塞DOM的错觉。

defer和async的区别

上面有几段代码用了 defer 属性,其实还有个属性 async ,这里说一下这两个属性。

1. <script src="script.js"></script>

– 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

  1. <script async src="script.js"></script>
    • 有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
  2. <script defer src="myscript.js"></script>
    • 有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

把所有脚本都丢到之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,可以保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

1. defer 和 async 在网络读取(下载)过程中是一样的,都是异步的(相较于 HTML 解析)

2. 两者差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的

3. defer是按照加载顺序执行脚本的

4. async则是乱序执行的,对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行

5. async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的,最典型的例子:Google Analytics

Event Loop

先了解三种数据结构

1. 栈(stack):栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈是只能在某一端插入和删除的特殊线性表。

2. 堆(heap):堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。堆是线性数据结构,相当于一维数组,有唯一后继。

3. 队列(queue):特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列。队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)。

javaScript是单线程,也就是说只有一个主线程,主线程有一个栈,每一个函数执行的时候,都会生成新的execution context(执行上下文),执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中, running execution context(正在执行的上下文)始终处于栈的顶部。当函数执行完后,它的执行上下文会从栈弹出。把JS引擎再细分有三个部分:

1. Stack:主线程的函数执行都压在这个栈中。

2. Heap :存放对象,数据。没有引用的对象会被垃圾回收。

3. Task Queue :执行栈为空的时候从任务队列中取一个任务执行,再次为空时再次到任务队列中取任务执行,如此循环,所以称为Event Loop。

浏览器渲染过程及JS引擎浅析

具体过程如下图:

Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。图中的异步处理模块就是我们之前在JS引擎中提到的事件触发线程,当JS引擎遇到异步任务的时候就把异步函数交给事件触发线程,当异步函数达到执行条件之后,事件触发线程会把异步任务根据类型压入指定任务队列。下图

任务队列

Js 中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。

– 宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.

– 微任务:process.nextTick, Promise, Object.observer, MutationObserver.

浏览器环境下,Event Loop是按照 HTML5 的标准来实现,当执行栈空的时候JS引擎会按如下规则取任务执行:

1. 取一个宏任务来执行。执行完毕后,下一步。

2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步。

3. 更新UI渲染。

Event Loop 会无限循环执行上面3步,这就是Event Loop的主要控制逻辑。其中,第3步(更新UI渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新UI成本大,所以,一般都会比较长的时间间隔,执行一次更新。

从逻辑上来看,浏览器倾向于尽可能快地执行完微任务,当全局任务(其实是全局函数中的同步任务)执行完之后,会立即执行微任务队列,即使微任务队列执行完了,在每次执行完一个宏任务之后都会检查微任务队列,如果就微任务就一直执行到微任务队列为空才会执行宏任务。

console.log('script start');

// 微任务
Promise.resolve().then(() => {
    console.log('p 1');
});

// 宏任务
setTimeout(() => {
    console.log('setTimeout');
}, 0);

var s = new Date();
while(new Date() - s < 50); // 阻塞50ms

// 微任务
Promise.resolve().then(() => {
    console.log('p 2');
});

console.log('script ent');

/*** output ***/
// one macro task
script start
script ent

// all micro tasks
p 1
p 2

// one macro task again
setTimeout

NodeJs的Event Loop

在Event Loop之前会先做这些工作:

1. 初始化 Event Loop

2. 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。

3. 执行主代码中出现的所有微任务:先执行完所有nextTick(),然后在执行其它所有微任务。

4. 开始 Event Loop

Event Loop分为6个阶段:

1. timers: 这个阶段执行setTimeout()和setInterval()设定的回调。

2. pending callbacks: 上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。

3. idle, prepare: 仅内部使用。

4. poll: 执行 I/O callback,在适当的条件下会阻塞在这个阶段

5. check: 执行setImmediate()设定的回调。

6. close callbacks: 执行比如socket.on(‘close’, …)的回调。

每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。

setTimeout

把setTimeout单独拿出来说是因为它有几点比较特别的地方。

1. setTimeout是异步的,它会有主线程交给事件触发线程,然后放到宏队列中去。并且HTML5标准规定了 delay (setTimeout)的第二个参数至少为 4ms ,即使你写 0

setTimeout(function () {
    console.log(1);
}, 0);
console.log(2);

//output
2
1
  1. setTimeout的 delay 只能表示它被事件触发程序放到任务队列中的时间,如果此时任务队列前面没有任务,执行栈也为空,那么回调函数会被立即执行,但是如果任务队列前面还有未执行任务或者执行栈中不为空,则需要继续等待。
var starttime = new Date().getTime()
console.log("start " + starttime);

setTimeout(function () {
    var endtime = new Date().getTime()
    console.log("end " + endtime);
    console.log("timediff " + (endtime - starttime));
}, 1000);

while (new Date().getTime() - starttime < 3000) {
    continue;
}

//output
start 1556197854352
end 1556197857353
timediff 3001

总结

本文只是阅读的文章结合自己的一点见解,只是浏览器的渲染过程以及JS引擎的一点皮毛,而且在前端技术日新月异的今天,浏览器也在不断地进步和优化,想要更好的掌握其原理还需要不断学习。

参考文章

  1. 原来 CSS 与 JS 是这样阻塞 DOM 解析和渲染的
  2. css加载会造成阻塞吗?
  3. defer和async的区别
  4. JavaScript 异步、栈、事件循环、任务队列
  5. 一次弄懂Event Loop
  6. 从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理
  7. 浏览器的工作原理
  8. 浏览器渲染原理(性能优化之如何减少重排和重绘)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Effective Java

Effective Java

Joshua Bloch / Addison-Wesley Professional / 2018-1-6 / USD 54.99

The Definitive Guide to Java Platform Best Practices—Updated for Java 9 Java has changed dramatically since the previous edition of Effective Java was published shortly after the release of Jav......一起来看看 《Effective Java》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具