最近在极客时间学习<浏览器工作原理>一课的一些笔记

开篇词的学习

  1. 浏览器进化的三个阶段

    1. 应用程序化
    2. web应用移动化
    3. web操作系统化
  2. 了解浏览器工作原理的两大优势

    1. 准确评估项目的可行性(需多语言基础,及后端知识)
    2. 更高纬度审视页面,如:
      1. 用户请求首屏加载慢
        1. ssr技术 , 如Airbnb主
        2. 缓存(cache-control) , 优化接口请求次数 , 打包文件体积优化(gzip)
      2. 操作dom某个功能元素,如button反应迟钝
        1. 优化该元素功能函数的内部逻辑(减少console.log()、优化冗余逻辑)
        2. 审视当前页面的性能消耗 , 如减少监听器 或 通过eventhub实现发布订阅等
      3. web中动画没有60帧
        1. ??? 没遇到过做动画的情况
        2. 2020-08-15 补充 : 通过设置 CSS will-change 属性,使元素单独分层,优化动画。
  3. 首屏加载问题

    1. DNS, HTTP解析
    2. DOM 解析
    3. CSS 阻塞
    4. JS 阻塞

    以上任意一步产生问题,均将造成加载延迟


宏观视角下的浏览器的学习

第一课 (打开chorme有4个进程)

打开浏览器,如 chorme 会有4个进程

  1. 进程和线程的四大特点 :

    1. 进程 中任意线程出错 , 导致整个 进程 崩溃
    2. 线程 之间共享 进程 中的数据
    3. 当一个进程关闭之后,操作系统会回收进程所占用的内存
    4. 进程之间的内容相互隔离
  2. 单进程浏览器缺点:

    1. 不稳定
      1. 一个插件会引起整个浏览器崩溃
      2. 复杂的JavaScript代码也会引起浏览器崩溃
    2. 不流畅
      1. 如代码中有无限循环的判断条件,单进程会独占所有内存来执行
    3. 不安全
      1. 插件可以通过 c/c++来编写,即 可以访问操作系统人以资源
      2. 页面脚本则可通过浏览器漏洞获取系统权限(盗号)
  3. 多线程浏览器解决以上问题

    1. 不稳定
      1. 进程相互隔离,不会导致浏览器崩溃
    2. 不流畅
      1. 即使 js文件 渲染阻塞也仅仅影响 当前 页面,同时内存泄露也只需关闭当前页面便能垃圾回收
    3. 不安全
      1. 多进程有额外的安全沙箱, chorme 吧插件进程所在安全沙箱内(safe sandbox)
  4. 最新浏览器具备5个进程

    1. 浏览器主进程
      1. 界面显示,用户交互,子进程管理,存储等
    2. GPU进程
      1. 实现 3D CSS 效果
    3. 网络进程
      1. 负责网络资源加载
    4. 渲染进程
      1. HTML,CSS,JS文件的渲染
    5. 插件进程
      1. 运行插件

    打开浏览器至少有以上4个进程(除插件)

第二课 (TCP)

FP(first paint):指从页面加载到首次开始绘制的时长.
首先要确定一个观点, 互联网中文件传输是通过 数据包 来传输的.

  1. 一个数据包从主机A主机B的传输过程:

    1. 主机A 的上层传递数据包给 主机A 网络层
    2. 主机A 的网络层添加IP头并组成 新的IP数据包 传到底层
    3. 主机A 底层通过物理传输(链路层)给目标 主机B
    4. 主机B 网络层接受并开始解析传递过来的数据包(拆开IP头)并把拆下IP头的数据包传递给 主机B 的上层
    5. 主机B 的上层接受网络层传过来的数据包
  2. UDP协议: 用户数据包协议(User Datagram Protocol)

    1. UDP不保证数据可靠性,传输速率快
    2. 此时需在基于添加IP头的 网络层之上的 传输层 添加UDP协议
    3. 一个数据包从主机A主机B的传输过程(包含UDP协议):
      1. 主机A 的上层传递数据包给 主机A 传输层
      2. 主机A 的传输层添加 UDP头 并组成 新的数据包 传到 网络层
      3. 主机A 的网络层添加IP头并组成 新的IP数据包 传到底层
      4. 主机A 底层通过物理传输(链路层)给目标 主机B
      5. 主机B 网络层 接受并开始解析 传递过来的数据包(拆开IP头)并把拆下IP头的数据包传递给 主机B 的 传输层
      6. 主机B 网络层解析完毕后的数据包传递给 主机B 的 传输层,并开始解析(拆开UDP头),并把拆下后的数据包传递给 主机B 的上层
      7. 主机B 收到来自 主机B 传输层的数据包
    4. UDP协议的缺点:
      1. 数据包在传输过程中容易丢失
      2. 大文件拆分来传递,UDP并不能组合这些数据包
  3. TCP:把数据完整地送达应用程序

    1. TCP协议解决UDP协议的两个问题:
      1. 对于数据包丢失的情况,TCP 提供重传机制
      2. TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件
    2. 数据传输流程同UDP协议,唯一不同的是协议由UDP头变更为TCP头
    3. 完整的TCP链接又包含3个步骤
      1. 三次握手,建立链接(以下以客户C(client)端和服务器S(sever)来举例)
        1. C->S 传递一个数据包,并对S说:我能建立链接了吗 (第一次)
        2. S->C S收到数据包 , 确认过后 , 对C说 : 可以建立链接了 (第二次)
        3. C->S C收到S的数据包 , 确认可以连接了 , 此时三次握手完毕 (第三次)
      2. 传输阶段 开始进行数据传输
      3. 四次挥手,断开链接
      4. S->C 传输完所有数据后,对C说,我数据传输完毕了。
      5. C->S 接受后对 S 说,哦,我知道了。
      6. C->S 再次发送消息,我也传输完了。
      7. S->C 接受后,哦,我知道了,此时完成四次挥手。
  4. 总结

    1. 数据传输通过数据包链接,数据包在传输过程中易丢失和出错
    2. IP 负责把数据包送达目的主机
    3. UDP 负责把数据包送达具体应用
    4. TCP 保证了数据完整地传输,它的连接可分为三个阶段:建立连接、传输数据和断开连接

第三课 (构建HTTP请求)

主要是关于浏览器构建HTTP请求的过程
HTTP是 建立在 TCP 连接基础 之上的, 一种允许浏览器向服务器获取资源的协议,是 Web 的基础

  1. 浏览器端发起 HTTP 请求流程:

    1. 构建请求:一般是 GET / index.html HTTP 1.1
    2. 查找缓存(解决两个问题)
      1. 缓解服务器压力,提升性能
      2. 对网站来说,缓存是实现快速资源加载的重要部分
    3. 准备IP地址和端口(DNS查询)
    4. 等待TCP队列(TCP队列不超过6个)
    5. 建立TCP连接(三次握手建立连接,传输数据,四次挥手断开连接)
    6. 发送HTTP请求
  2. 服务器端处理 HTTP 请求流程 :

    1. 返回请求
      1. 带上HTTP报文 和 HTTP 实体 返回 响应
      2. 响应包括响应行 响应头 响应体
      3. 通过状态码来查询响应的状态
    2. 断开连接(如果加入Connection:Keep-Alive,TCP便不会断开连接)
    3. 重定向
  3. 为什么很多站点第二次打开速度会很快?

    1. 因为缓存的机制
    2. 浏览器构建HTTP请求,从服务器得到响应(添加了cache-control,此时便有缓存)
    3. 第二次进入相同站点,服务器收到HTTP请求会先查询是否有cache-control,如果未过期,则直接跳过该部分资源的请求
    4. 如果缓存过期,则请求该缓存部分最新数据,再次进行缓存
  4. 登录状态是如何保持的?

    1. cookie 通过 服务器 响应 添加 Set-Cookie 进行设置的
    2. 用户登录后 发送请求 携带上 Cookie
    3. 服务器收到 请求后对比 cookie 检验是否过期,未过期则保持登录状态

第四课(为4-6的总结 页面从导航到展示的过程)

只解决一个问题,从输入URL到页面展示,这中间发生了什么?

  1. 简易流程

    1. 浏览器进程收到用户输入的URL请求,浏览器进程将URL转发给网络进程
    2. 网络进程发起真正的URL请求
    3. 网络进程收到响应头,开始解析响应头数据,并将数据转发给浏览器进程
    4. 浏览器进程收到网络进程响应头数据后,发送提交导航消息给渲染进程
    5. 渲染进程接到提交导航 消息后 ,开始准备接受HTML数据,接受方式是与网络进程建立连接通道
    6. 最后渲染进程向浏览器进程确认提交
    7. 浏览器进程收到渲染进程的提交文档消息后,开始移除之前的旧文档,更新浏览器进程中的页面状态
    8. 小结:用户发出的URL请求到页面开始解析的过程叫导航
  2. 详细流程

    1. 用户输入(判断输入关键字还是URL)
      1. 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
      2. 如果是链接,浏览器加上HTTP协议,并传递给网络层
    2. URL请求过程
      1. 查找缓存
        1. 有缓存: 进入网络进程解析响应流程
        2. 无缓存:
          1. 进行DNS解析, 获取IP地址和端口
          2. 利用IP地址和服务器建立TCP连接
          3. 构建请求头
          4. 发送请求头
          5. 进入网络进程解析响应流程
    3. 网络进程解析响应过程:
      1. 1开头, 表明连接建立 , 还在处理中
      2. 2开头, 表明响应成功 , 走到第 4 步
      3. 3开头, 表示重定向 , 重新由步骤 2 开始 , 向重定向的地址发送请求
      4. 4开头, 表示客户端错误 , 返回错误信息
      5. 5开头, 表示服务器错误 , 返回错误信息
    4. 渲染进程接收提交导航
      1. 将HTML文件转换成浏览器看得懂的语言, 生成DOM树
      2. 将CSS文件转换成浏览器看得懂的语言 stylesheet , 并计算DOM树中各个节点的CSS属性
      3. 渲染进程根据DOM树中各个节点 , 生成对应的 布局树
      4. 对布局树进行分层 , 并生成 分层树 z-index
      5. 为每个图层生成绘制列表 , 并提交给 合成进程
      6. 合成进程将图层分成土块,并在光栅化线程池中将图块转换成位图
      7. 合成线程发送绘制图块命令 DrawQuad 给浏览器进程
      8. 浏览器进程根据 DrawQuad 消息生成页面, 并显示到显示器上
  3. 以上就是浏览器输入URL到页面展示过程的完整过程

  4. 总结

    1. 优化首屏加载速度可以从4个方面入手
      1. JS文件阻塞
      2. CSS文件阻塞
      3. HTML文件阻塞
      4. DNS, HTTP解析
    2. 浏览器渲染进程中,冗余的css会造成浏览器的额外开销
      1. 重排: 更新元素几何属性, 例如: 高度, 宽度 , 开销最大
      2. 重绘: 更新元素的绘制属性, 例如: 背景色 , 开销其次
      3. 直接合成: 如transition, 开销最小
      4. 减少重排,重绘的解决办法
        1. 用class替代style
        2. 避免table布局
        3. 批量操作DOM,如框架
        4. 禁用浏览器的Debounce resize 事件
        5. 对DOM属性进行读写分离
        6. will-change(上下层叠属性):transform 优化

浏览器中JavaScript执行机制

第一课 (变量提升)

需要搞懂js 的执行上下文

  1. var a = 1 为例, 到输出结果的过程

    1. 对语句进行词法分析, 结果为 var , a , = , 2
    2. 进行语法分析, 形成抽象语法树, 其根(root)为 var 的顶级节点, 叶子节点为 a 和 2
    3. 代码生成, 创建一个 a 的变量(LHS), 并将 2 赋值给 a (RHS), 过程为 , var aa = 2
  2. 变量提升

    1. 上述过程清晰阐述了语句 var a = 1 的过程, 在期间,使用 var 来声明变量, 会使得 var a 语句提升到 全局执行上下文的最前面, 再对其进行赋值操作.
    2. 这就是为什么 console.log(a) 在前 var a = 2 在后 ,却能打出 a 的值为2的分析过程
    3. 如果在此过程中声明函数function a(){} , a 会被 function 覆盖.

第二课 (调用栈)

总得来说, 执行函数, 是先将函数压入调用栈, 执行完毕后, 再弹出调用栈的操作
栈的特点是后进先出, js的调用栈称为执行上下文栈

1
2
3
4
5
6
var a = 2
function add(){
var b = 10
return a+b
}
add()
  1. 考虑上述代码 , 其执行顺序为 :
    1. 从全局执行上下文中,取出 add 函数代码。
    2. 对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
    3. 执行代码,输出结果。
  2. 抽象对js函数执行的理解 , 大致步骤如下 :
    1. 创建全局上下文,并将其压入栈底。
    2. 第二步是调用 add 函数。
    3. 如果 add 函数内部还有函数 , 将 add 内部执行函数 压入 调用栈
    4. add 内部函数 执行完毕 , 返回结果 , 并弹出栈 , 开始执行 add 函数
    5. 当 add 函数 执行完毕 , 返回值

第三课 (作用域和闭包)

作用域是指 变量与函数的可访问范围
ES6之前 , 作用域分 全局作用域 和 函数作用域
ES6之后 , 出现 块级作用域

  1. 全局作用域 : 对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。用 var 声明的变量, 存储在 变量环境
  2. 函数作用域 : 函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
    1. 闭包 :
  3. 块级作用域 : 比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域 , 块级作用域内的变量 仅在块级作用域内有效。
    1. 在块级作用域内使用 letconst 声明变量 , 会形成 暂死区 , 此时得不到变量的值,但是 词法环境 中却存在该变量
  4. 作用域链 : 当前执行函数, 如果引用了本身没有的变量 , 会从本层的外层寻找该变量 , 考虑函数执行上下文 , 并分析 outer 为 上一个函数的作用域 , 还是 全局作用域.

第四课 (this)

关键字 this 不会凭空出现 , 分析其 执行上下文 , 就能得出 this 的指向

1
2
3
4
5
6
7
8
9
let obj = {
name:'xxx',
getName(name){
console.log(this)
console.log(this.name)
}
}

let x = obj.getName
  1. 绑定 this 的 三种形式 :
    1. 显示绑定 :
      1. obj.getName.call(obj)
      2. obj.getName.apply(obj)
      3. obj.getName.bind(this)()
        1. .bind()操作只返回一个函数,需要自己手动调用
        2. .call()和.apply()不需要手动调用,会自动调用,仅仅是传参不同
    2. 隐式绑定 :
      1. x() === window.x.call(window) , 结果为 window 和 空
    3. new 操作的 this 指向 new 出来的那个对象 , new 的操作如下
      1. 新建一个临时对象 let newTemp = {}
      2. 调用 createObj.call(newTemp)
      3. 执行 createObj 函数
      4. 返回这个临时对象
    4. 箭头函数中的 this 与执行上下文中的 this 相同

V8工作原理

第一课(栈和堆)

JavaScript 的 7 种基本类型分别是
Number,String,Boolean,BigInt,Symbol,Null和Undefined.
JavaScript 的 引用类型只有一种 Object

  1. js存储方式
    1. 基本类型 存储于 栈内存 中, 栈内存仅仅只存储 引用类型的地址
    2. 引用类型 存储于 堆内存 中, 堆中的数据是通过引用和变量关联起来的

第二课(垃圾回收)

有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。
如果这些垃圾数据一直保存在内存中,那么内存会越用越多,
所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。

  1. 不同语言的垃圾回收政策
    1. 手动回收
      1. 手动使变量的值为null
    2. 自动回收
      1. JS执行过后的变量垃圾也分为两种
        1. 新生代的垃圾
          1. 副垃圾回收器,主要负责新生代的垃圾回收。
        2. 老生代的垃圾
          1. 主垃圾回收器,主要负责老生代的垃圾回收。
  2. 垃圾不回收造成的问题
    1. 内存泄漏
    2. 直观的感受就是页面越来越卡
  3. 避免以上的原因
    1. 尽量少使用闭包, 闭包会使函数的引用变量一直存放在内存中, 长时间占用内存
    2. 确定不使用的临时变量时使其值为 null

第三课(V8如何执行一段JS代码)

需要理解的几个重要概念
编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、
字节码(Bytecode)、即时编译器(JIT)

  1. 在编译器语言编译过程中
    1. 编译器对源代码进行词法分析和语法分析, 生成抽象语法树
    2. 优化代码
    3. 最后生成处理器能理解的代码
      1. 如果编译成功,将会生成一个可执行的文件。
      2. 如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
  2. 在解释型语言的解释过程中
    1. 同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST)
    2. 不过它会再基于抽象语法树生成字节码
    3. 最后再根据字节码来执行程序、输出结果。
  3. 抽象语法树
    1. 是源代码语法结构的一种抽象表示
  4. 字节码
    1. 字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
  5. 即时编译器
    1. 字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)
    2. 具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
  6. JS在执行一段代码的过程
    1. 生成抽象语法树(AST)和执行上下文
      1. 第一阶段是 分词 , 如 : var a = 1, 将被分为 var, a, =, 1
      2. 第二阶段是 解析 , 又称 语法分析 , 其作用是将上一步生成的 token 数据,根据语法规则转为 AST。
        1. 如果源码符合语法规则,这一步就会顺利完成。
        2. 但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
      3. 生成字节码
      4. 执行代码
  7. 综上所述, 包括之前的知识, 对JS性能优化的总结
    1. 提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互
    2. 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程
    3. 减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存
  8. 为什么V8代码执行时间越久,执行效率越高?
    1. 即时编译(JIT)技术的存在
    2. 解释器执行字节码的过程中,如果发现有热点代码(HotSpot),
    3. 那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,
    4. 然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,
    5. 这样就省去了省去了字节码“翻译”为机器码的过程大大提升了代码的执行效率。

浏览器中页面循环系统

消息队列和事件循环

JS处理代码时的顺序
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个”任务队列”(task queue)。
只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
一但”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。
那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。
这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点。
也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

  1. 总结
    1. 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型
    2. 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
    3. 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
    4. 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
    5. 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。

Web Api

setTimeout 和 XMLHttpRequest (宏任务)

  1. setTimeout
    1. 如果当前任务执行时间过久,会影响定时器任务的执行。
    2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。
    3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒。
    4. 延时执行时间有最大值。
    5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉。
  2. XMLHttpRequest(AJAX)
    1. 创建 XMLHttpRequest 对象。
    2. 为 xhr 对象注册回调函数。
    3. 配置基础的请求信息。
    4. 发起请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
setTimeout
// 1.for 循环事件过长 会影响 setTimeout 的执行时间
function bar() {
console.log('bar')
}
function foo() {
setTimeout(bar, 0);
for (let i = 0; i < 5000; i++) {
let i = 5+8+8+8
console.log(i)
}
}
foo()
// 2. setTimeout 中 this指向
var name= 1;
var MyObj = {
name: 2,
showName: function(){
console.log(this.name);
}
}
setTimeout(MyObj.showName,1000) // 1
// 改为 匿名函数 或者 箭头函数解决上面问题
//箭头函数
setTimeout(() => {
MyObj.showName()
}, 1000);
//或者function函数
setTimeout(function() {
MyObj.showName();
}, 1000)


// AJAX
// 创建xhr对象
let xhr = new XMLHttpRequest
// 注册事件回调
xhr.onreadystatechange = ()=>{}
xhr.ontimeout = (e)=>{}
xhr.onerror= (e)=>{}
// 打开请求
xhr.open('Get', URL, true);
// 发送请求
xhr.send();

微任务和宏任务

微任务可以在实时性和效率之间做一个有效的权衡。

  1. 宏任务
    1. 渲染事件(如解析 DOM、计算布局、绘制);
    2. 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
    3. JavaScript 脚本执行事件;
    4. 网络请求完成、文件读写完成事件。
  2. 微任务
    1. 把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。
    2. 执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。
  3. 结论
    1. 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
    2. 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以你在写代码的时候一定要注意控制微任务的执行时长。
    3. 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
  4. 每一次事件循环顺序都是宏->微->宏,每次清空当前消息队列时会检查微任务队列中是否已清空。

Promise、Async/Await

Promise 解决回调地狱的问题
Async/Await 用同步的写法来编写异步代码

  1. Promise
    1. .then()的写法消灭嵌套调用
    2. .catch()合并多个错误处理
    3. .race() 多个请求只取最快
    4. .all() 等待请求全部完成
    5. .finally() 无论resolvereject最后都会走到这个分支
  2. Async/Await
    1. 该方法是 generator 生成器,调用 generator.next().value 的语法糖
      1. 生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。
      2. 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
      3. 外部函数可以通过 next 方法恢复函数的执行。
    2. Async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

浏览器中的页面

Chrome开发工具

Chrome 开发者工具(简称 DevTools)是一组网页制作和调试的工具,
内嵌于 Google Chrome 浏览器中。

  1. Elements面板
    1. 可查看DOM结果、编辑CSS样式,用于测试页面布局和设计页面。
  2. Console面板
    1. 可以看成是JavaScript Shell,能执行JavaScript脚本。通过Console在页面中与JavaScript对象交互。
  3. Network面板
    1. 展示页面中所有请求内容列表,能查看每项请求的请求行、请求头、请求体、时间线以及网络请求瀑布图等信息。
    2. 可根据网络请求来观察HTTP 1.1 下 当前浏览器最多支持 6 个 TCP 连接。
      1. 把站点升级到 HTTP 2 , 可突破上述问题限制,因为 HTTP 2 的多路由复用机制。
  4. Source面板
    1. 查看Web应用加载的所有文件
    2. 编辑CSS和JavaScript文件内容
    3. 将打乱的CSS文件或者JavaScript文件格式化
    4. 支持JavaScript的调试功能
    5. 设置工作区,将更改的文件保存到本地文件夹中
  5. Performance面板
    1. 记录和查看Web应用生命周期内的各种事件,并用来分析在执行过程中一些影响性能的要点。
  6. Memory面板
    1. 用来查看运行过程中JavaScript占用堆内存情况,追踪是否存在内存泄露的情况等
  7. Application面板
    1. 查看Web应用的数据存储情况
      1. PWA的基础数据
      2. IndexedDB
      3. Web SQL
      4. 本地和会话存储
      5. Cookie
      6. 应用程序缓存
      7. 图像
      8. 字体和样式表
  8. Security面板
    1. 显示当前页面的一些基础安全信息

JavaScript是如何影响DOM树创建的

从网络传给渲染引擎的 HTML 文件字节流是无法直接被渲染引擎理解的,
所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是 DOM。
DOM 提供了对 HTML 文档结构化的表述。

  1. 在渲染引擎中,DOM 有三个层面的作用。
    1. 从页面的视角来看,DOM 是生成页面的基础数据结构。
    2. 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
    3. 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。
  2. DOM 树如何生成
    1. 首先确认一个观念,HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。
    2. 网络进程接受到响应头之后,会根据响应头中的 content-type 字段判断文件类型,如果值为text/html,浏览器会判断这是一个HTML类型,然后为该请求选择或创建一个渲染进程
    3. 渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,解析器就将收到的字节流解析为DOM。
  3. DOM形成的详细过程
    1. 第一个阶段,通过分词器将字节流转换为 Token。
      1. 如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
      2. 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
      3. 如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
    2. 至于后续的第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。
  4. JavaScript 是如何影响 DOM 生成的
    1. 内联 <script>console.log(1)</script> 会阻塞DOM树的形成。
    2. 标签内插入下载的JS文件 <script type="text/javascript" src='foo.js'></script>

CSS如何影响首次加载时的白屏时间

当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线程,
如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据。

  1. CSSOM 的作用
    1. 提供给 JavaScript 操作样式表的能力。
    2. 为布局树的合成提供基础的样式信息。
  2. 影响页面展示的因素
    1. 主要原因就是渲染流水线影响到了首次页面展示的速度。
    2. 视觉上经历的3个阶段
      1. 等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。
      2. 提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
      3. 等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。
    3. 优化策略
      1. 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
      2. 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
      3. 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。
      4. 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。

为什么CSS动画比JavaScript动高效?

因为 will-change 属性的存在,
使得浏览器在解析 CSS 文件时会具有该属性的元素提前分层,
后续仅仅只在渲染主进程内的合成线程进行合成,该方法是效率最高的,
JavaScript修改元素样式,可能会引起 重排或重绘 皆是效率最低的。

  1. 显示器如何显示图像
    1. 前缓冲区 : 每秒固定读取 60 次前缓冲区中的图像,并将读取的图像显示到显示器上。
    2. 后缓冲区 : 显卡的职责就是合成新的图像,并将图像保存到后缓冲区中。
  2. 帧和帧率
    1. 帧 : 一张图片理解为帧。
    2. 帧率 : 1s 内更新帧数的频率,如,1s内更新60帧,帧率为60Hz(60FPS)。
  3. 如何生成一帧图像,通常渲染路径越长,生成图像花费的时间就越多。
    1. 重排 : 需要重新根据 CSSOM 和 DOM 来计算布局树,这样生成一幅图片时,会让整个渲染流水线的每个阶段都执行一遍,如果布局复杂的话,就很难保证渲染的效率了。
    2. 重绘 : 没有了重新布局的阶段,操作效率稍微高点,但是依然需要重新计算绘制信息,并触发绘制操作之后的一系列操作。
    3. 合成 : 操作的路径就显得非常短了,并不需要触发布局和绘制两个阶段,如果采用了 GPU,那么合成的效率会非常高。
      1. 分层和合成 : 为了提升每帧的渲染效率,Chrome 引入了分层和合成的机制。
        1. 分层 : 将素材分解为多个图层称为分层,分层体现在生成布局树之后。
        2. 合成 : 将这些图层合并到一起称为合成,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。
      2. 分块 : 合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。
    4. 如何利用分层技术优化代码
      1. will-change 属性,通过例子可以观察到,在chrome devtools的 performance面板、layers面板中可以观察到 具有该属性的元素会被提前分层成为单独的一帧图片 从而使得元素不需要重排或重绘,提高了动画合成的效率,但是该操作会占用相对大的内存,根据不同情况下考量何时使用该属性。

如何系统的优化页面

页面优化即让页面更快的显示和响应。
一个页面有三个阶段:加载阶段、交互阶段、关闭阶段。
加载阶段:请求发出到渲染完整页面,影响因素有网络JavaScript脚本
交互阶段:页面加载完成到用户交互,影响因素主要是JavaScript脚本
关闭阶段:主要是用户发出关闭指令后页面的一些清理操作。

  1. 加载阶段
    1. 造成阻塞:JS文件、首次加载的HTML文件、CSS文件等。
    2. 不会造成阻塞:图片、视频、音频等资源。
    3. 造成阻塞的原因
      1. 关键资源个数
      2. 关键资源大小
      3. 请求关键资源需要多少个RTT(round trip time)
    4. 优化策略
      1. 减少关键资源个数,使用如内联css和内联javascript
      2. 减小关键资源大小,去除css重复代码,javascript文件中注释和重复代码
      3. 减少rtt次数,一次rtt最多14kb的数据包传输,减小css和html和js文件体积大小。
  2. 交互阶段:主要考虑重排、重绘
    1. 减少JS执行时间
      1. 一次执行的函数分解为多步操作
      2. 使用 web worker 进行非dom操作的逻辑处理
    2. 避免强制同步布局
      1. css计算属性是在单独的任务中进行的
      2. 如mwui-vue中处理nav组件的动画使用到了js使css强制计算当前元素的css属性.
    3. 避免布局抖动
    4. 合理利用css合成动画(will-change属性)
    5. 避免频繁垃圾回收(尽可能减少闭包的使用)

虚拟DOM

如果通过JS脚本直接修改DOM的话,会引起如,重排、重绘、合成,甚至因为不当操作,还会引起如,布局抖动和强制同步布局等一系列的操作。所以虚拟DOM,改进了上述的问题。

  1. 虚拟DOM
    1. 虚拟dom做了什么
      1. 将页面变更内容应用到虚拟DOM上,而不是直接修改DOM。
      2. 数据变化时,生成新的虚拟DOM,对比新旧虚拟DOM,找出变化的节点。
      3. 数据停止变化后,渲染到DOM上,更新页面
  2. 几种设计模式
    1. 双缓存
      1. 虚拟DOM,类似双缓存中的Buffer(缓存)
    2. MVC
      1. Model(数据层)、View(视图层)、Controller(逻辑处理层)
  3. react中的具体流程
    1. Controller 监控 DOM 变化,DOM变化,Controller 通知 Model 更新数据。
    2. Model 更新数据后,Controller通知 View ,告知数据发生改变。
    3. View 接受更新消息后,生成新的虚拟DOM。
    4. 新虚拟DOM生成好后,与旧的虚拟DOM比较(diff),找出变化节点。
    5. react此时将变化节点应用到DOM上,触发DOM节点的更新。
    6. DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。

浏览器中的网络

HTTP/1

HTTP 是浏览器中最重要且使用最多的协议,是浏览器和服务器之间的通信语言,也是互联网的基石。

  1. HTTP/0.9
    1. 请求过程
      1. 客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。
      2. 建立好连接之后,会发送一个 GET 请求行的信息。
      3. 服务器接收请求信息之后,读取对应文件,并将数据以ASCII字符流返回给客户端
      4. 传输完成后,断开连接。
    2. 3个特点
      1. 只有一个请求行,并没有 HTTP 请求头和请求体,因为只需要一个请求行就可以完整表达客户端的需求了。
      2. 服务器也没有返回头信息,这是因为服务器端并不需要告诉客户端太多信息,只需要返回数据就可以了。
      3. 返回的文件内容是以 ASCII 字符流来传输的,因为都是 HTML 格式的文件,所以使用 ASCII 字节码来传输是最合适的。
  2. HTTP/1.0 解决HTTP/0.9不支持多种不同类型的数据处理的问题.
    1. 首先,浏览器需要知道服务器返回的数据是什么类型,然后浏览器才能根据不同的数据类型做针对性的处理。
    2. 其次,服务器会对数据进行压缩后再传输,所以浏览器需要知道服务器压缩的方法。
    3. 再次,浏览器告诉服务器它想要什么语言版本的页面。
    4. 最后,浏览器需要知道文件的编码类型。
    5. 以上问题皆需要通过设置请求头去设置。
  3. HTTP/1.1 相比较HTTP/1.0的改进。
    1. 改进持久连接,HTTP/1.1 中增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。
    2. 不成熟的 HTTP 管线化,TCP内只要有一段数据包传输出错,就会造成队头阻塞。
    3. 提供虚拟主机的支持
    4. 对动态生成的内容提供了完美支持
    5. 客户端 Cookie、安全机制

HTTP/2

__END__

o0Chivas0o
文章作者:o0Chivas0o
文章出处浏览器工作原理
作者签名:Rich ? DoSomethingLike() : DoSomethingNeed()
版权声明:文章除特别声明外,均采用 BY-NC-SA 许可协议,转载请注明出处