最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 浏览器原理之三:页面 - 掘金

    正文概述 掘金(HJM515)   2021-11-12   593

    浏览器原理第三篇:讲述浏览器的页面及页面事件循环系统,介绍虚拟DOM、PWA、WebComponent。

    15. 消息队列和事件循环

    线程模型演进

    第一版 顺序执行

    将所有任务按顺序写进主线程,线程执行时这些任务依次被执行,执行完成后,线程自动退出。

    问题:无法处理新任务。

    第二版 引入事件循环

    通过一个 for 循环语句来监听是否有新的任务。

    问题:无法接收其他线程的任务。

    第三版 队列+循环

    1. 添加一个消息队列,存放要执行的任务。
    2. IO 线程中产生的新任务添加到消息队列尾部。(IO 线程,是渲染进程专门用来接收其他进程传递的消息。)
    3. 渲染主线程循环地从消息队列头部读取任务、执行任务。

    消息队列中的任务称为宏任务,每个宏任务中包含一个微任务队列。

    16. WebAPI: setTimeout

    实现原理

    在 chrome 中,除了正常的消息队列之外, 还有一个延迟执行任务的消息队列,用来存放定时器和内部一下需要延迟执行的任务。

    调用 setTimeout 设置回调函数时,渲染进程会创建一个回调任务,包含了回调函数、当前发起时间、延迟执行时间,然后将该任务添加到延迟执行队列中。

    当处理完消息队列中的一个任务之后,会计算有没有到期的任务,有就一次执行这些到期任务。 << ❓❓ 难道不是到期就把延时任务放到队列中,等待执行吗?

    延迟执行队列,实际是一个 hashmap 结构。(不符合先进先出的特征。)

    使用的注意事项

    1. 当前任务执行过久,会延迟到期任务的执行。
    2. setTimeout 存在嵌套调用,系统会设置最短间隔 4ms。
    3. 未激活的页面,setTimeout 执行最小间隔 1000ms。

    17. WebAPI: XMLHttpRequest

    为什么 xhr 请求的回调是放在消息队列里面,而我们用 axios 这种 http 库发出的请求最后回调是放在微任务里面啊,虽然 axios 里面用到了 promise,promise 的回调是微任务,可是 axios 说到底还是对原生 xhr 的封装啊 ❓❓

    创建流程

    1. 创建 XMLHttpRequest 对象,用来执行实际的网络请求操作。
    2. 为 XHR 对象注册回调函数,ontimeout、onerror、onreadystatechange。
    3. 配置基础的请求信息。(通过 xhr.responseType 的配置,真的可以自动将服务器返回的数据转换成指定格式 ❓❓)
    4. 发送请求。
    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();
    

    问题

    1. 跨域。
    2. 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,说明浏览器已经加载了所有的资源(图像、样式表等)。

    时间线面板

    1. Resource Scheduling
      1. queueing 排队:发起请求后,因资源有优先级、域名最多维护 6 个 TCP 连接、网络进程在为数据分配磁盘空间等原因,新的 HTTP 请求需要排队等候。
    2. Connection Start
      1. stalled 停滞:发起连接之前,还有一些原因可能导致连接过程被推迟。此外,代理服务器还会增加一个代理协商阶段 Proxy Negotiation。
      2. Initial connection 连接:建立 TCP 连接所花的事件。
      3. SSL:如果使用 HTTPS,还需要额外的 SSL 握手协商加密时间。
    3. Request/Response
      1. Request sent:准备请求数据并将其发送给网络(无需判断服务器是否收到)。
      2. Waiting(TTFB):等待接收服务器第一个字节的数据。反映了服务器响应速度。
      3. Content download:从第一字节事件到接收完全部响应数据所用的时间。

    优化时间线上的耗时项

    1. queueing 时间过长:域名分片(一个站点的资源放到多个域名下),升级到 HTTP2(多路复用无 6 个 TCP 连接限制)。
    2. TTFB 时间过久:
      1. 服务器处理数据太慢,可增加各种缓存技术。
      2. 网络原因(低带宽或跨网),可以使用 CDN 缓存静态文件。
      3. 请求头带有多余的信息导致服务器处理时间延长,可减少不必要的 cookie 信息。
    3. Content download 时间过久:字节数据太多,需要压缩、去掉不必要的注释等方法减少文件大小。

    22. DOM 树

    DOM 是表述 HTML 的内部数据结构,它会将 Web 页面和 JavaScript 脚本连接起来,并过滤一些不安全的内容。

    DOM 树的生成

    渲染引擎内部,HTML 解析器(HTMLParser)模块负责将 HTML 字节流转换成 DOM 结构。

    网络进程 >> HTML 解析器

    1. 在网络进程接受到响应头后,如 content-type 的值是 text/html,浏览器就会为该请求选择或创建一个渲染进程。
    2. 网络进程和渲染进程之间,建立数据传输管道,HTML 解析器动态接收字节流并将其解析为 DOM。

    字节流 >> DOM

    1. 分词器将字节流转换为 Token(StartTag Token、EndTag Token、Text Token)。
    2. 将 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 解析

    1. html 中插入 js 脚本,HTML 解析器会暂停 DOM 解析,因为 script 标签中的脚本可能会修改当前已经生成的 DOM。
    2. JS 文件的下载也会阻塞 DOM 解析。但是 Chrome 等浏览器做了预解析操作优化,当渲染引擎收到字节流后,开启一个预解析线程,分析 html 中包含的 JS、CSS 文件,解析相关文件并提前下载。
    3. JS 可能会操纵 CSSOM,所以在执行 JS 脚本前,会先下载并解析 CSS 文件。

    22. 渲染流水线

    浏览器原理之三:页面 - 掘金 提交数据之后到首次渲染这个阶段,包括了解析 HTML、下载 CSS、下载 Javascript、生成 CSSOM、执行 JS、生成布局树、绘制等一些列操作。

    瓶颈:下载 CSS、下载 Javascript、执行 JS。

    24. 分层和合成机制

    显示器 & 显卡

    1. 显卡的职责是合成新的图像,并将图像保存到后缓冲区。
    2. 系统会交换后缓冲区和前缓冲区。
    3. 显示器从前缓冲区读取最新合成的图像,显示到显示器上。每秒更新(读取)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 执行流程

    创建阶段

    1. 更加 JSX 和基础数据创建虚拟 DOM。
    2. 由虚拟 DOM 树创建出真实 DOM 树。
    3. 真实 DOM 树生成完成后,在触发渲染流水线往屏幕输出页面。

    更新阶段

    1. 数据发生变化,根据新的数据创建一个新的虚拟 DOM 树。
    2. React 使用 Stack reconciler/Fiber reconciler 算法比较两个树,找出变化的地方。
    3. 将变化的地方一次更新到真实的 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。

    具体实现步骤

    1. 使用 template 标签定义模板。包含样式、结构、行为。
    2. 创建类(继承于 HTMLElement),获取组件模板,创建影子 DOM 节点并添加上模板。
    3. 使用 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介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元