3

1. 浏览器架构

  1. 用户界面
  2. 主进程
  3. 内核

    1. 渲染引擎
    2. JS 引擎

      1. 执行栈
    3. 事件触发线程

      1. 消息队列

        1. 微任务
        2. 宏任务
    4. 网络异步线程
    5. 定时器线程

2. 从输入 url 到页面展示的过程

2.1 流程

  1. 跳转
  2. 是否有缓存
  3. DNS查找,域名解析ip
  4. 创建TCP链接,之后才有HTTP三次握手(HTTP寻在TCP之上)
  5. 发送请求(Request)
  6. 接收响应(Response),返回请求的文件 (html)
  7. 浏览器渲染(1,2并行,后面是串行)

    1. 解析HTML --> DOM ree

      1. 标记化算法,进行元素状态的标记
      2. 生成DOM
    2. 解析CSS --> CSS tree

      1. 生成CSSOM
    3. 结合 --> Render tree

      1. 结合DOM与CSSOM,生成渲染树(Render tree)
    4. layout: 布局(布局渲染树)
    5. painting: 绘制(绘制渲染树)

    clipboard.png

clipboard.png

2.2 参考

https://juejin.im/post/5c64d1...
https://juejin.im/book/5b9365...

3.浏览器存储

3.1 Cookie

3.1.1 Cookie的来源

  1. Cookie 的本职工作并非本地存储,而是“维持状态”。
  2. 因为HTTP协议是无状态的,HTTP协议自身不对请求和响应之间的通信状态进行保存,通俗来说,服务器不知道用户上一次做了什么,这严重阻碍了交互式Web应用程序的实现。

在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料。最后结帐时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,于是就诞生了Cookie

我们可以把Cookie 理解为一个存储在浏览器里的一个小小的文本文件,它附着在 HTTP 请求上,在浏览器和服务器之间“飞来飞去”。它可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态

在刚才的购物场景中,当用户选购了第一项商品,服务器在向用户发送网页的同时,还发送了一段Cookie,记录着那项商品的信息。当用户访问另一个页面,浏览器会把Cookie发送给服务器,于是服务器知道他之前选购了什么。用户继续选购饮料,服务器就在原来那段Cookie里追加新的商品信息。结帐时,服务器读取发送来的Cookie就行了。

3.1.2 什么是cookie

Cookie指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 cookie是服务端生成,客户端进行维护和存储
通过cookie,可以让服务器知道请求是来源哪个客户端,就可以进行客户端状态的维护,比如登陆后刷新,请求头就会携带登陆时response header中的set-cookie,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。

3.1.3 cookie应用场景

  1. 记住密码,下次自动登录。
  2. 购物车功能
  3. 记录用户浏览数据,进行商品(广告)推荐。

    3.1.4 Cookie的原理

clipboard.png

第一次访问网站的时候,浏览器发出请求,服务器响应请求后,会在响应头里面添加一个Set-Cookie选项,将cookie放入到响应请求中,在浏览器第二次发请求的时候,会通过Cookie请求头部将Cookie信息发送给服务器,服务端会辨别用户身份,另外,Cookie的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。

 3.1.5 Cookie生成方式

服务端
http response header中的set-cookie

客户端
js中可以通过document.cookie可以读写cookie

document.cookie="userName=hello"

3.1.6 Cookie的缺陷

  1. Cookie 不够大
    各浏览器的cookie每一个name=value的value值大概在4k,所以4k并不是一个域名下所有的cookie共享的,而是一个name的大小。
  2. 过多的 Cookie 会带来巨大的性能浪费
    Cookie 是紧跟域名的。同一个域名下的所有请求,都会携带 Cookie
  3. 由于在HTTP请求中的Cookie是明文传递的,所以安全性成问题,除非用HTTPS。

3.1.7 Cookie与安全

属性作用
value如果用于保存用户登录状态,应该将该值加密,不能使用明文的用户标识(例如可以使用md5加密)
http-only不能通过js访问cookie,减少xss攻击
secure只能在协议为https的请求中携带
same-site规定浏览器不能在跨域的请求中携带cookie,减少csrf攻击

3.2 LocalStorage

3.2.1 LocalStorage的特点

  1. 保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。
  2. 大小为5M左右
  3. 仅在客户端使用,不和服务端进行通信
  4. LocalStorage可以作为浏览器本地缓存方案,用来提升网页首屏渲染速度(根据第一请求返回时,将一些不变信息直接存储在本地)。

    3.2.2存入/读取数据

    localStorage.setItem("key","value");
    var valueLocal = localStorage.getItem("key");

    3.3sessionStorage

    3.3.1 sessionStorage的特点

  5. 会话级别的浏览器存储
  6. 大小为5M左右
  7. 仅在客户端使用,不和服务端进行通信

    3.3.2 窗口共享

  8. 即便是相同域名下的两个页面,只要它们不在同一个浏览器窗口(浏览器的标签页)中打开,那么它们的 sessionStorage 内容便无法共享
  9. localStorage 在所有同源窗口中都是共享的
  10. cookie也是在所有同源窗口中都是共享的

    3.3.3存入/读取数据

    localStorage.setItem("key","value");
    var valueLocal = localStorage.getItem("key");

    3.4 总结

特性cookielocalStoragesessionStorageindexDB
数据生命周期一般由服务器生成,可以设置过期时间除非被手动清理,否则一直存在页面关闭就清理除非被手动清理,否则一直存在
数据存储大小4k5M5M无限
与服务端通信每次都会携带在header中,对于请求有性能影响不参与不参与不参与

cookie 已经不建议用于存储。如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。

  1. Cookie 的本职工作并非本地存储,而是“维持状态”
  2. Web Storage 是 HTML5 专门为浏览器存储而提供的数据存储机制,不与服务端发生通信
  3. IndexedDB 用于客户端存储大量结构化数据

3.5 参考

https://github.com/ljianshu/B...

4. 浏览器渲染原理

4.1 什么是渲染过程

简单来说,渲染引擎根据 HTML 文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程(如下图)
clipboard.png

从这个流程来看,浏览器呈现网页这个过程,宛如一个黑盒。在这个神秘的黑盒中,有许多功能模块,内核内部的实现正是这些功能模块相互配合协同工作进行的。其中我们最需要关注的,就是HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块与JavaScript 引擎这几大模块:

  • HTML 解释器:将 HTML 文档经过词法分析输出 DOM 树。
  • CSS 解释器:解析 CSS 文档, 生成样式规则。
  • 图层布局计算模块:布局计算每个对象的精确位置和大小。
  • 视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。
  • JavaScript引擎:编译执行 Javascript 代码。
打开一个网页,看到服务器返回给客户端(浏览器)的各种文件类型
  1. document --> html
  2. stylesheet --> css
  3. script --> js
  4. jpeg --> 图片
    clipboard.png

4.2 构建DOM

浏览器会遵守一套步骤将HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤

clipboard.png

  1. 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。
    在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
  2. 将字符串转换成Token,例如:<html>、<body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。

    clipboard.png
    上图给出了节点之间的关系,例如:“Hello”Token位于“title”开始标签与“title”结束标签之间,表明“Hello”Token是“title”Token的子节点。同理“title”Token是“head”Token的子节点。

  3. 生成节点对象并构建DOM
    事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会创建节点对象。
实例
<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

上面这段HTML会解析成这样

clipboard.png

4.3 构建CSSOM

DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM

构建CSSOM的过程与构建DOM的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。

clipboard.png

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式

注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。

4.3 构建渲染树

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树

clipboard.png

在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

浏览器如果渲染过程中遇到JS文件怎么处理

渲染过程中,如果遇到<script>就停止渲染,执行 JS 代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)

JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。

原本DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。

这是什么情况?

这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。

clipboard.png

4.4 布局与绘制

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。

4.5 async和defer的作用是什么?有什么区别?

  1. async 异步
  2. defer 延缓
    clipboard.png
  3. 蓝色线代表JavaScript加载;
  4. 红色线代表JavaScript执行;
  5. 绿色线代表 HTML 解析。

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

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

情况2<script async src="script.js"></script> (异步下载)

async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好(符合异步),就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行

情况3 <script defer src="script.js"></script>(延迟执行)

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后

在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。

4.6 为什么操作 DOM 慢

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》

JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”(如下图)。

clipboard.png

过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。

4.7 总结

  1. 浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
  2. CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
  3. 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个不带defer或async属性的script标签时,DOM构建将暂停,如果此时又恰巧浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS,最后才重新DOM构建。

    4.8 参考

    深入浅出浏览器渲染原理

5. 重绘与回流(重排)

5.1 名词解析

通俗理解
  1. 重绘: 重新绘制,绘制色彩
  2. 回流(重排): 重新排版,排版位置大小
比较官方的理解
  1. 重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。
  2. 回流: 当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)

    5.2 浏览器渲染的流程中的回流与重绘

  3. 计算CSS样式
  4. 构建RenderTree
  5. Layout(布局) –-> 定位坐标和大小
  6. paint(绘制)-->正式开画

clipboard.png

注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属性会导致重新Layout,但有些改变不会重新Layout,就是上图中那些指到天上的箭头,比如修改后的CSS rule没有被匹配到元素。

我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘

回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

5.3 常见引起回流属性和方法

任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,

  1. 添加或者删除可见的DOM元素;
  2. 元素尺寸改变——边距、填充、边框、宽度和高度
  3. 内容变化,比如用户在input框中输入文字
  4. 浏览器窗口尺寸改变——resize事件发生时
  5. 计算 offsetWidth 和 offsetHeight 属性
  6. 设置 style 属性的值

5.4 常见引起重绘属性和方法

  1. 文字属性
  2. 边框样式(非大小)
  3. 色彩
  4. 背景色
  5. 阴影

clipboard.png

5.5如何减少回流、重绘

  1. 使用 transform 替代 top
  2. 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  3. 不要把节点的属性值放在一个循环里当成循环里的变量。

    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会导致回流,因为需要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
  4. 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  5. 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  6. CSS 选择符从右往左匹配查找,避免节点层级过多
  7. 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。

5.6 参考

https://github.com/ljianshu/B...

6. 浏览器下的Event Loop

6.1 线程与进程

6.1.1概念

我们经常说JS 是单线程执行的,指的是一个进程里只有一个主线程,那到底什么是线程?什么是进程?
官方的说法是
进程: CPU资源分配的最小单位
线程: CPU调度的最小单位

clipboard.png

  1. 进程好比图中的工厂,有单独的专属自己的工厂资源。
  2. 线程好比图中的工人,多个工人在一个工厂中协作工作,工厂与工人是 1:n的关系。也就是说一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
  3. 厂的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可用这些共享内存。
  4. 多个工厂之间独立存在

6.1.2 多进程与多线程

多进程:在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互干扰。

多线程:程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

以Chrome浏览器中为例,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程(下文会详细介绍),比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

6.1.3js单线程存在的问题

js是单线程的,处理任务是一件接着一件处理,所以如果一个任务需要处理很久的话,后面的任务就会被阻塞
所以js通过Event Loop事件循环的方式解决了这个问题

6.2浏览器内核

6.2.1什么是浏览器内核(渲染引擎)

简单来说浏览器内核是通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,通常也被称为渲染引擎。
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

  1. GUI 渲染线程
  2. JavaScript引擎线程
  3. 定时触发器线程
  4. 事件触发线程
  5. 异步http请求线程

6.2.2GUI渲染线程

  1. 主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。
  2. 当界面需要重绘或者由于某种操作引发回流时,将执行该线程。
  3. 该线程与JS引擎线程互斥,当执行JS引擎线程时,GUI渲染会被挂起,当任务队列空闲时,主线程才会去执行GUI渲染。

    6.2.3 JS引擎线程

  4. 该线程当然是主要负责处理 JavaScript脚本,执行代码。
  5. 也是主要负责执行准备好待执行的事件,即定时器计数结束,或者异步请求成功并正确返回时,将依次进入任务队列,等待 JS引擎线程的执行。
  6. 当然,该线程与 GUI渲染线程互斥,当 JS引擎线程执行 JavaScript脚本时间过长,将导致页面渲染的阻塞。

    6.2.4 定时器触发线程

  7. 负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval。
  8. 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。

    6.2.5 事件触发线程

    主要负责将准备好的事件交给 JS引擎线程执行
    比如 setTimeout定时器计数结束, ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS引擎线程的执行。

    6.2.6 异步http请求线程

  9. 负责执行异步请求一类的函数的线程,如: Promise,axios,ajax等。
  10. 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。

6.3 event loop

6.3.1stack,queue,heap

  1. stack(栈),先进后出
  2. queue(队列),先进先出,生活中的排队
  3. heap(堆):存储obj对象

6.3.2 执行栈

clipboard.png

js引擎运行时,当代码开始运行的时候,会将代码,压入执行栈进行执行

实例

clipboard.png

当代码被解析后,函数会依次被压入到栈中

clipboard.png

有入栈,就要有出栈,当函数c执行完,开始出栈
clipboard.png

6.3.3 当执行栈遇到异步

前面执行栈,先入后出,但其实也是同步的,同步就意味着会阻塞,所以需要异步,那当执行栈中出现异步代码会怎么样

clipboard.png

当浏览器在执行栈执行的时候,发现有异步任务之后,会交给webapi去维护,而执行栈则继续执行后面的任务

clipboard.png

同样,setTimeout同样会被添加到webapi中

clipboard.png

webapi是浏览器自己实现的功能,这里专门维护事件。

上面setTimeout旁边有个进度条,这个进度就是设置的等待时间

6.3.4 回调队列callback queue

当setTimeout执行结束的时候,是不是就应该回到执行栈,进行执行输出呢?
答案:并不是!

clipboard.png

此时,倒计时结束后的setTimeout的可执行函数,被放入了回调队列
最后,setTimeout的可执行函数,被从回调队列中取出,再次放入了执行栈

clipboard.png

这样的执行过程就叫 event loop事件循环

6.4 Event Loop的具体流程

6.4.1 执行栈任务清空后,才会从回调队列头部取出一个任务

clipboard.png

  1. console.log(1)被压入执行栈
  2. setTimeout在执行栈被识别为异步任务,放入webapis中
  3. console.log(3)被压入执行栈,此时setTimeout倒计时结束后,把可执行代码console.log(2)放入回调队列里等待
  4. console.log(3)执行完成后,从回调队列头部取出console.log(2),放入执行栈
  5. console.log(2)执行

6.4.2 回调队列先进先出

clipboard.png

clipboard.png

当console.log(4)执行完成后,从回调队列里取出了console.log(2);

只有console.log(2)执行完成,执行栈再次清空时,才会从回调队列取出console.log(3)一个一个拿,先拿console.log(2),执行完后,再拿console.log(3),执行

6.4.3 把代码从回调队列拿到栈中执行,发现在这段代码中有异步

clipboard.png

  1. 输出1,将2push进回调队列
  2. 将4push进回调队列
  3. 输出5
  4. 清空了执行栈,读取输出2(从回调队列中放到栈中执行),发现有3(异步),将3push进回调队列
  5. 清空了执行栈,读取输出4
  6. 清空了执行栈,读取输出3

5.4 Macrotask(宏任务)、Microtask(微任务)

5.4.1 什么是宏任务,微任务

宏任务: setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等
微任务: new Promise().then(回调)、MutationObserver(html5新特性)、process.nextTick、Object.observe 等。

5.4.2 微任务

Microtask(微任务)同样是一个任务队列,这个队列的执行顺序是在清空执行栈之后

clipboard.png

可以看到Macrotask(宏任务)也就是回调队列上面还有一个Microtask(微任务)
Microtask(微任务)虽然是队列,但并不是一个一个放入执行栈,而是当执行栈请空,会执行全部Microtask(微任务)队列中的任务,最后才是取回调队列的第一个Macrotask(宏任务)

  1. 将setTimeout给push进宏任务
  2. 将then(2)push进微任务
  3. 将then(4)push进微任务
  4. 任务队列为空,取出微任务第一个then(2)压入执行栈
  5. 输出2,将then(3)push进微任务
  6. 任务队列为空,取出微任务第一个then(4)压入执行栈
  7. 输出4
  8. 任务队列为空,取出微任务第一个then(3)压入执行栈
  9. 输出3
  10. 任务队列为空,微任务也为空,取出宏任务中的setTimeout(1)
  11. 输出1

clipboard.png

实例
console.log("1");
setTimeout(()=>{
    console.log(2)
    Promise.resolve().then(()=>{
        console.log(3);
        process.nextTick(function foo() {
            console.log(4);
        });
    })
})
Promise.resolve().then(()=>{
    console.log(5);    
    setTimeout(()=>{
        console.log(6)
    })
    Promise.resolve().then(()=>{
        console.log(7);
    })
})

process.nextTick(function foo() {
    console.log(8);
    process.nextTick(function foo() {
        console.log(9);
    });
});
console.log("10")

1,输出1
2,将setTimeout(2)push进宏任务
3,将then(5)push进微任务
4,在执行栈底部添加nextTick(8)
5,输出10
6,执行nextTick(8)
7,输出8
8,在执行栈底部添加nextTick(9)
9,输出9
10,执行微任务then(5)
11,输出5
12,将setTimeout(6)push进宏任务
13,将then(7)push进微任务
14,执行微任务then(7)
15,输出7
16,取出setTimeout(2)
17,输出2
18,将then(3)push进微任务
19,执行微任务then(3)
20,输出3
21,在执行栈底部添加nextTick(4)
22,输出4
23,取出setTimeout(6)
24,输出6

1,10,8,9,5,7,2,3,4,6

5.5 参考

https://juejin.im/post/5a6309...
https://github.com/ljianshu/B...

6. JavaScript中的垃圾回收和内存泄漏

6.1 前言

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存
所谓的内存泄漏简单来说是不再用到的内存,没有及时释放
Javascript具有自动垃圾回收机制(Garbage Collecation)。

6.2 垃圾回收的必要性

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

最简单的垃圾回收

JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。

var a = "浪里行舟";
var b = "前端工匠";
var a = b; //重写a

这段代码运行之后,“浪里行舟”这个字符串失去了引用(之前是被a引用),系统检测到这个事实之后,就会释放该字符串的存储空间以便这些空间可以被再利用。

6.3 垃圾回收机制

垃圾回收有两种方法:标记清除、引用计数。引用计数不太常用,标记清除较为常用。

标记清除

这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。

function addTen(num){  
     var sum += num;  //垃圾收集已将这个变量标记为“进入环境”。
     return sum;      //垃圾收集已将这个变量标记为“离开环境”。
}
addTen(10);  //输出20
var user = {name : 'scott', age : '21', gender : 'male'}; //在全局中定义变量,标记变量为“进入环境”

user = null;  //最后定义为null,释放内存
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b,标记为进入环境。
console.log(n) // n标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b    //c标记为进入环境
  return c //c标记离开环境
}

6.4 哪些情况会引起内存泄漏

虽然JavaScript会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收

6.4.1 意外的全局变量

function foo(arg) {
    bar = "this is a hidden global variable";
}

bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。

另一种意外的全局变量可能由 this 创建:

function foo() {
    this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

6.4.2 被遗忘的计时器或回调函数

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。

6.4.3 闭包

function bindEvent(){
  var obj=document.createElement('xxx')
  obj.onclick=function(){
    // Even if it is a empty function
  }
}

闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。

// 将事件处理函数定义在外面
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
  var obj = document.createElement('xxx')
  obj.onclick = function() {
    // Even if it is a empty function
  }
  obj = null
}

6.4.4 没有清理的DOM元素引用

有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面。

6.5 内存泄漏的识别方法

clipboard.png

步骤

  1. 打开开发者工具 Performance
  2. 勾选 Screenshots 和 memory
  3. 左上角小圆点开始录制(record)
  4. 停止录制
    图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题。

避免内存泄漏的一些方式

  1. 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
  2. 注意程序逻辑,避免“死循环”之类的
  3. 避免创建过多的对象

不用了的东西要及时归还

6.6 垃圾回收的使用场景优化

6.6.1 数组array优化

[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。

const arr = [1, 2, 3, 4];
console.log('浪里行舟');
arr.length = 0  // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。

6.6.2 对象尽量复用

对象尽量复用,尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为null,尽快被垃圾回收掉。

var t = {} // 每次循环都会创建一个新对象。
for (var i = 0; i < 10; i++) {
  // var t = {};// 每次循环都会创建一个新对象。
  t.age = 19
  t.name = '123'
  t.index = i
  console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。

6.6.3 在循环中的函数表达式,能复用最好放到循环外面

// 在循环中最好也别使用函数表达式。
for (var k = 0; k < 10; k++) {
  var t = function(a) {
    // 创建了10次  函数对象。
    console.log(a)
  }
  t(k)
}
// 推荐用法
function t(a) {
  console.log(a)
}
for (var k = 0; k < 10; k++) {
  t(k)
}
t = null

6.7 参考

https://github.com/ljianshu/B...


渣渣辉
1.3k 声望147 粉丝