浏览器原理第三篇:讲述浏览器的页面及页面事件循环系统,介绍虚拟DOM、PWA、WebComponent。
15. 消息队列和事件循环
线程模型演进
第一版 顺序执行
将所有任务按顺序写进主线程,线程执行时这些任务依次被执行,执行完成后,线程自动退出。
问题:无法处理新任务。
第二版 引入事件循环
通过一个 for 循环语句来监听是否有新的任务。
问题:无法接收其他线程的任务。
第三版 队列+循环
- 添加一个消息队列,存放要执行的任务。
- IO 线程中产生的新任务添加到消息队列尾部。(IO 线程,是渲染进程专门用来接收其他进程传递的消息。)
- 渲染主线程循环地从消息队列头部读取任务、执行任务。
消息队列中的任务称为宏任务,每个宏任务中包含一个微任务队列。
16. WebAPI: setTimeout
实现原理
在 chrome 中,除了正常的消息队列之外, 还有一个延迟执行任务的消息队列,用来存放定时器和内部一下需要延迟执行的任务。
调用 setTimeout 设置回调函数时,渲染进程会创建一个回调任务,包含了回调函数、当前发起时间、延迟执行时间,然后将该任务添加到延迟执行队列中。
当处理完消息队列中的一个任务之后,会计算有没有到期的任务,有就一次执行这些到期任务。 << ❓❓ 难道不是到期就把延时任务放到队列中,等待执行吗?
延迟执行队列,实际是一个 hashmap 结构。(不符合先进先出的特征。)
使用的注意事项
- 当前任务执行过久,会延迟到期任务的执行。
- setTimeout 存在嵌套调用,系统会设置最短间隔 4ms。
- 未激活的页面,setTimeout 执行最小间隔 1000ms。
17. WebAPI: XMLHttpRequest
为什么 xhr 请求的回调是放在消息队列里面,而我们用 axios 这种 http 库发出的请求最后回调是放在微任务里面啊,虽然 axios 里面用到了 promise,promise 的回调是微任务,可是 axios 说到底还是对原生 xhr 的封装啊 ❓❓
创建流程
- 创建 XMLHttpRequest 对象,用来执行实际的网络请求操作。
- 为 XHR 对象注册回调函数,ontimeout、onerror、onreadystatechange。
- 配置基础的请求信息。(通过 xhr.responseType 的配置,真的可以自动将服务器返回的数据转换成指定格式 ❓❓)
- 发送请求。
let xhr = new XMLHttpRequest();
xhr.open('GET', URL, true);
xhr.timeout = 2000;
xhr.responseType = 'json'; // text、json、document、blob、arraybuffer
xhr.setRequestHeader('key', 'val');
xhr.send();
问题
- 跨域。
- https 混合内容问题,页面内包含不符合 https 安全要求的内容,如 http 资源、图像、视频、样式表、脚本等都属于混合内容。
18. 宏任务和微任务
宏任务
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JS 脚本执行事件;
- 网络请求完成、文件读写完成事件。❓❓
异步回调实现方式 1:把异步函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务时执行回调函数。
异步回调实现方式 2:在主函数执行结束之后、当前宏任务结束之前执行回调函数,以微任务形式体现。
微任务
JS 执行一段脚本时,V8 会为其创建一个全局执行上下文,同时在内部创建一个微任务队列。
产生方式
- 第一种方式:使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修 改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产 生 DOM 变化记录的微任务。
- 第二种方式:使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也 会产生微任务。
执行时机
当前宏任务执行完成,JS 引擎准备推出全局执行上下文并清空调用栈时,JS 引擎会检查全局执行上下文中的微任务队列,按顺序执行。
MutationObserver
通过异步操作解决同步操作的性能问题,通过微任务解决实时性问题。
//选择一个需要观察的节点
var targetNode = document.getElementById("some-id");
// 设置observer的配置选项
var config = { attributes: true, childList: true, subtree: true };
// 当节点发生变化时的需要执行的函数
var callback = function (mutationsList, observer) {
for (var mutation of mutationsList) {
if (mutation.type == "childList") {
console.log("A child node has been added or removed.");
} else if (mutation.type == "attributes") {
console.log("The " + mutation.attributeName + " attribute was modified.");
}
}
};
// 创建一个observer示例与回调函数相关联
var observer = new MutationObserver(callback);
//使用配置文件对目标节点进行观测
observer.observe(targetNode, config);
// 停止观测
observer.disconnect();
19. Promise
消灭嵌套调用和频繁的错误处理。
1.Promise 中为什么引入微任务?
Promise 的 resolve 和 reject 回调采用了回调函数延迟绑定机制,所以在执行 resolve 函数时,回调函数还没有绑定,回调函数需延时执行。相比 setTimeout,Promise 使用微任务,这样既可以延时调用,又提高了代码的执行效率。
2.Promise 中是如何实现回调函数返回值穿透的?
在 then 中返回一个新的 Promise。
3.Promise 出错后,是怎么通过 "冒泡" 传递给最后那个捕获异常的函数?
出错后包装成 promise.reject 返回,如果 then 中没有第二个参数处理异常,就继续返回 promise.reject。
20. async - await
Generator 的底层实现机制 -- 协程(Coroutine)。async - await 使用了 Generator 和 Promise 两种技术。
生成器、协程
生成器函数是一个带星号的函数,可以暂停执行和恢复执行。
协程是比线程更加轻量级的存在。一个线程可以存在多个协程,但是在线程上同时只能执行一个协程。
function* genDemo() {
console.log(" 开始执行第一段 ") // 3
yield 'generator 1' // 4.通过yield交出主线程控制权
console.log(" 开始执行第二段 ") // 7
yield 'generator 2' // 8
console.log(" 执行结束 ") // 11
return 'generator 2' // 12.通过return关闭当前协程
}
console.log('main 0')
let gen = genDemo() // 1.创建gen协程
console.log(gen.next().value) // 2.通过gen.next()恢复gen协程
console.log('main 1') // 5
console.log(gen.next().value) // 6
console.log('main 2') // 9
console.log(gen.next().value) // 10
console.log('main 3') // 13
/*
gen协程和父协程在主线程上交互执行,通过yield和gen.next来配合完成。
当gen协程调用yield方法,JS引擎会保存gen协程当前调用栈信息,并恢复父协程的调用栈信息。
父协程调用gen.next过程 也是类似的处理过程。
/
async
async 是一个异步执行并隐式返回 Promise 作为结果的函数。
await
async function foo() {
console.log(1); // 3
let a = await 100; // 4.暂停foo协程,并将primise_返回给父协程, promise_.then >> 执行5
// await内部操作
// let promise_ = new Promise((resolve, reject) => {
// resolve(100) // 6.执行微任务, promise_.then设置的回调函数被激活
// }) // 7.将resolve的值传递给foo协程(赋值给a),暂定父协程执行,把控制权交给foo协程
console.log(a); // 8
console.log(2); // 9
}
console.log(0); // 1
foo(); // 2.父协程,启动foo协程
console.log(3); // 5
图片来源声明:极客时间——浏览器工作原理与实践
21. Chrome 开发者工具
DOMContentLoaded,这个事件发生后,说明页面已经构建好 DOM 了,这意味着构建 DOM 所需要的 HTML 文件、JavaScript 文件、CSS 文件都已经下载完成了。 Load,说明浏览器已经加载了所有的资源(图像、样式表等)。
时间线面板
- Resource Scheduling
- queueing 排队:发起请求后,因资源有优先级、域名最多维护 6 个 TCP 连接、网络进程在为数据分配磁盘空间等原因,新的 HTTP 请求需要排队等候。
- Connection Start
- stalled 停滞:发起连接之前,还有一些原因可能导致连接过程被推迟。此外,代理服务器还会增加一个代理协商阶段 Proxy Negotiation。
- Initial connection 连接:建立 TCP 连接所花的事件。
- SSL:如果使用 HTTPS,还需要额外的 SSL 握手协商加密时间。
- Request/Response
- Request sent:准备请求数据并将其发送给网络(无需判断服务器是否收到)。
- Waiting(TTFB):等待接收服务器第一个字节的数据。反映了服务器响应速度。
- Content download:从第一字节事件到接收完全部响应数据所用的时间。
优化时间线上的耗时项
- queueing 时间过长:域名分片(一个站点的资源放到多个域名下),升级到 HTTP2(多路复用无 6 个 TCP 连接限制)。
- TTFB 时间过久:
- 服务器处理数据太慢,可增加各种缓存技术。
- 网络原因(低带宽或跨网),可以使用 CDN 缓存静态文件。
- 请求头带有多余的信息导致服务器处理时间延长,可减少不必要的 cookie 信息。
- Content download 时间过久:字节数据太多,需要压缩、去掉不必要的注释等方法减少文件大小。
22. DOM 树
DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。
DOM 树的生成
渲染引擎内部,HTML 解析器(HTMLParser)模块负责将 HTML 字节流转换成 DOM 结构。
网络进程 >> HTML 解析器
- 在网络进程接受到响应头后,如 content-type 的值是 text/html,浏览器就会为该请求选择或创建一个渲染进程。
- 网络进程和渲染进程之间,建立数据传输管道,HTML 解析器动态接收字节流并将其解析为 DOM。
字节流 >> DOM
- 分词器将字节流转换为 Token(StartTag Token、EndTag Token、Text Token)。
- 将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。(HTML 解析器开始工作时,会默然创建一个根为 document 的空 DOM 结构,解析生成的 DOM 节点都挂载 document 之下。)
HTML 解析器维护了一个 Token 栈结构,计算节点之间的父子关系。
- StartTag Token 会依次入栈,然后为该 Token 创建一个 DOM 节点加入到 DOM 树中,父节点就是栈中相邻的那个元素生成的节点。
- Text Token,会生成一个文本节点,并将该节点加入到 DOM 树中,它的父节点就是栈顶 Token 对应的 DOM 节点。
- EndTag Token,与栈顶 Token 匹配,匹配则将 StartTag Token 出栈,不匹配则抛出错误。
JS 阻塞 DOM 解析
- html 中插入 js 脚本,HTML 解析器会暂停 DOM 解析,因为 script 标签中的脚本可能会修改当前已经生成的 DOM。
- JS 文件的下载也会阻塞 DOM 解析。但是 Chrome 等浏览器做了预解析操作优化,当渲染引擎收到字节流后,开启一个预解析线程,分析 html 中包含的 JS、CSS 文件,解析相关文件并提前下载。
- JS 可能会操纵 CSSOM,所以在执行 JS 脚本前,会先下载并解析 CSS 文件。
22. 渲染流水线
提交数据之后到首次渲染这个阶段,包括了解析 HTML、下载 CSS、下载 Javascript、生成 CSSOM、执行 JS、生成布局树、绘制等一些列操作。
瓶颈:下载 CSS、下载 Javascript、执行 JS。
24. 分层和合成机制
显示器 & 显卡
- 显卡的职责是合成新的图像,并将图像保存到后缓冲区。
- 系统会交换后缓冲区和前缓冲区。
- 显示器从前缓冲区读取最新合成的图像,显示到显示器上。每秒更新(读取)60 次。
显卡的更新频率和显示器的刷新频率是一致的。复杂情况下,处理速度可能会变慢造成卡顿。针对卡顿问题,Chrome 引入合成和分层机制,
分层 & 合成
分层:将素材分解成多个图层的操作。Chrome 中,分层在生成布局树之后,渲染引擎根据布局树的特点将其转换为层树,层树中的每个节点对应着一个图层。
合成:将这些图层合并到一起的操作。生成绘制列表后,光栅化生成图片,将这些图片合成为"一张"图片。合成操作在合成线程完成,不会影响主线程执行。因此,CSS 动画比 JS 动画高效。
在合成线程中实现的是整个图层的集合变换。涉及图层中内容的改变是重排或重绘的过程。
CSS 属性,will-change,提前告诉渲染引擎将对某类元素进行变换操作,这时渲染引擎会将该元素单独实现一层,变换完成后合成线程进行变换处理即可。当然,为一个元素准备一个独立层,占用的内存也会增加。
.box {
will-change: opacity, transform;
}
分块
合成线程会将每个图层分割为大小固定的图块,优先绘制靠近视口的图块。Chrome 的另一个策略,在首次合成图块的时候使用一个低分辨率的图片。
25. 页面性能
加载阶段性能优化
- 减少关键资源个数。
- 降低关键资源大小。
- 降低关键资源的 RTT 次数。
RTT,Round Trip Time,往返时延。1 个 HTTP 数据包在 14KB 左右,数据较大时就需要拆分成多个包来传输,包的个数越大,往返的时延越长。
交互阶段性能优化
通过以下措施,尽量减少一帧的生成时间。
- 较少 JS 脚本执行时间,避免长时间霸占主线程。
- JS 在修改 DOM 之前完成查询信息操作,避免强制同步布局。正常布局 JS 执行和重新计算样式布局是两个任务。强制同步布局会将计算样式和布局操作提前到当前任务。
- 不要在循环语句中读取和修改 DOM,避免布局抖动(反复执行布局操作)。
- 合理利用 CSS 合成动画。
- 较少在函数中频繁创建临时对象,避免频繁的垃圾回收。
26. 虚拟 DOM
DOM 的缺陷
每次 DOM 操作,渲染引擎都需要进行重排、重绘或合成等操作。当 DOM 结构复杂时,执行一次重排或重绘操作非常耗时。
虚拟 DOM
反映真实 DOM 结构的一个 JS 对象。解决频繁操作 DOM 引起页面响应慢的问题。
react 中虚拟 DOM 执行流程
创建阶段
- 更加 JSX 和基础数据创建虚拟 DOM。
- 由虚拟 DOM 树创建出真实 DOM 树。
- 真实 DOM 树生成完成后,在触发渲染流水线往屏幕输出页面。
更新阶段
- 数据发生变化,根据新的数据创建一个新的虚拟 DOM 树。
- React 使用 Stack reconciler/Fiber reconciler 算法比较两个树,找出变化的地方。
- 将变化的地方一次更新到真实的 DOM 树,渲染引擎更新渲染流水线,生成新的页面。
比较虚拟 DOM 是一个递归函数,老的 Stack reconciler 算法在主线程执行,最新的 Fiber reconciler 算法利用协程出让主线程,解决了函数占用时间过久的问题。
双缓存
开发游戏等图形操作很复杂需要大量的运算,一副完整的图像需要多次计算才能完成。如果计算完一部分就将其写入缓冲区,会造成图像显示不完整等问题。 双缓存,是将中间计算结果放到一个缓冲区,全部计算结束再将完整的图像复制到显示缓冲区。虚拟 DOM 就类似于一个中间缓冲区。
27. 渐进式网页应用 PWA
Web 应用的缺点和解决方案
- 缺少离线使用能力。 << Service Worker
- 缺少消息推送能力。 << Service Worker
- 缺少一级入口,无法将 web 应用安装到桌面。 << mainfest.json
Service Worker
在页面和网络之间增加一个拦截器,用来缓存和拦截请求。
- Service Worker 运行在浏览器进程中,主线程之外,类似于 Web Worker,为多个页面服务。
- 即使浏览器页面没有启动,Service Worker 也可接受服务器推送的消息。❓❓
- 出于安全考虑,Service Worder 采用 HTTPS 协议。
28. WebComponent
CSS 和 DOM 的全局性,阻碍了组件化。
WebComponent = Custom Element + Shadow DOM + html Templates。
具体实现步骤
- 使用 template 标签定义模板。包含样式、结构、行为。
- 创建类(继承于 HTMLElement),获取组件模板,创建影子 DOM 节点并添加上模板。
- 使用 customElements.define 来自定义元素,然后就可以像使用 HTML 元素一样使用该元素。
<template id="geekbang-t">
<style>
p {
background-color: brown;
color: cornsilk;
}
div {
width: 200px;
background-color: bisque;
border: 3px solid chocolate;
border-radius: 10px;
}
</style>
<div>
<p>time.geekbang.org</p>
<p>time1.geekbang.org</p>
</div>
<script>
function foo() {}
</script>
</template>
<script>
class GeekBang extends HTMLElement {
constructor() {
super();
const content = document.querySelector("#geekbang-t").content;
const shadowDOM = this.attachShadow({ mode: "open" });
shadowDOM.appendChild(content.cloneNode(true));
}
}
customElements.define("geek-bang", GeekBang);
</script>
<geek-bang></geek-bang>
影子 DOM
Shadow DOM,将模板中的内容与全局 DOM 和 CSS 隔离开,实现元素和样式的私有化。DOM 内部样式不会影响全局的 CSSOM,使用 DOM 接口也无法直接查询到影子 DOM 内部的元素。
浏览器为实现影子 DOM 的特性,在代码内部加入大量的条件判断。
- 当通过 DOM 接口查找元素时,渲染引擎查到 shadow-root 元素,判断是影子 DOM,会直接跳过 shadow-root 元素的查询操作。
- 当生成布局树时,渲染引擎判断 shadow-root 是影子 DOM,会直接使用影子 DOM 内部的 CSS 属性。
系列文章
浏览器原理之一:大体看看
浏览器原理之二:JS、V8
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!