深入浅出浏览器工作原理

chrome多进程架构

进程和线程的关系

  • 进程是操作系统为应用程序分配的独立空间和资源,一个进程中可能包含多个线程
  • 当一个进程关闭之后,操作系统会回收进程所占用的内存
  • 进程中任意一个线程执行出错,都会导致整个进程的崩溃
  • 线程之间共享进程中的数据
  • 进程之间的内容互相隔离,一个进程崩溃或挂起,不会影响到其他进程

chrome多进程架构

  • 1个浏览器主进程(负责各个进程的调度)
  • 1个GPU进程(负责硬件加速)
  • 1个网络进程(负责网络请求)
  • 多个插件进程(负责chrome插件的运行)
  • 多个渲染进程
    chrome中每一个标签页都是一个独立的渲染进程,渲染进程负责tab内网页展示的所有工作,排版引擎blink和JavaScript引擎v8都是运行在该进程中,该进程包含以下线程:
    • GUI渲染线程(负责页面渲染)
    • JavaScript引擎线程(负责执行js脚本)
    • 事件触发线程(负责管理事件监听)
    • 定时器触发线程(负责管理定时器任务)

chrome为什么要让每一个标签页是一个独立的进程呢?

主要是为了隔离,当某一个标签页崩溃或内存泄漏时不会影响到其他页面;如果所有标签页都在同一个进程中,一个页面崩溃将会导致所有页面都看不了了,是非常不好的用户体验。

事件循环(Event Loop)

我们都知道JS是单线程、非阻塞的,单线程指的是JS引擎在执行脚本的时候同一时间只能处理一件事情,比如有两个监听事件同时被触发了,但他们的回调函数不会同时执行,必须进行排队处理。非阻塞指的是异步任务(可能非常耗时)不会阻塞同步任务的执行,那么这里的非阻塞就是利用事件循环来实现的。

事件循环其实就是浏览器对js事件/任务的处理机制,能够让各种任务有条不紊的排队执行:

  • JavaScript中的代码分为同步任务和异步任务,其中异步任务又分为宏任务和微任务
  • 同步任务优先在主线程上执行,形成执行栈
  • 当遇到异步任务,js引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步任务返回结果后,会将它的回调函数添加到执行栈之外的任务队列中,其中,任务队列分为宏任务队列和微任务队列,js会根据异步事件的类型,将其回调函数添加到对应的队列当中。
  • 当执行栈空闲时,就会从任务队列的队首读取一个宏任务执行,执行过程中如果遇到微任务就将它添加到微任务队列中,在本次循环的宏任务执行完之后立即执行微任务队列
  • 浏览器为了能够使得JS引擎线程与GUI渲染线程有序切换,会在当前宏任务结束之后,下一个宏任务开始之前,对页面进行重新渲染
  • 所以浏览器中任务的执行顺序是:
    • 宏任务 > 微任务队列 > 渲染 > 宏任务 > 微任务队列 。。。
  • 异步任务分类
    • 宏任务
      • script(主代码块)、setTimeoutsetInterval 、I/O 、UI rendering
      • 在nodejs中还有 setImmediate 也是宏任务
    • 微任务
      • PromiseObject.observeMutationObserver
      • 在nodejs中还有 process.nextTick 也是宏任务,它是所有任务中执行时机最快的,快于 promise

一个很经典的题:

javascript 复制代码
console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
}
async1()

setTimeout(function() {
    console.log('setTimeout')
}, 0)

new Promise(resolve => {
    console.log('Promise')
    resolve()
}).then(function() {
    console.log('promise1')
}).then(function() {
    console.log('promise2')
})

console.log('script end')

依次输出:script start、async2 end、Promise、script end、async1 end、promise1、promise2、setTimeout

浅析:

  • 首先,同步任务优先逐行执行,所以首先打印出 script start、async2 end、Promise、script end,这里要注意:
    • await之前的代码都是同步执行的,await之后的代码相当于写在promise.then中,属于微任务
    • Promise构造函数中的代码是同步执行的,then里的代码才是异步执行
  • 主代码块是一个宏任务,在其执行过程中产生的微任务会在当前宏任务执行完后立即执行。所以接着依次打印出async1 end、promise1、promise2,它们都属于微任务。
  • 一轮循环结束,主执行栈继续去任务队列读取新一轮的宏任务,输出setTimeout

页面的加载解析及渲染过程

又名:从输入url到页面加载完成都发生了什么? 这是一个很经典的面试题,能够全方位的考察知识面,主要分为三个阶段:网络请求、解析、渲染

网络请求

  • 浏览器首先解析输入的内容是url还是搜索关键字
  • DNS解析
    • 将域名解析为ip地址
    • 为了减少时间消耗,DNS解析会走多级缓存,包括浏览器缓存、hosts文件、本地dns服务器缓存,任何一环有命中的话就直接返回,否则才会由本地DNS服务器发出迭代请求去解析(根域名服务器、顶级域名服务器、权威域名服务器)
    • 客户端发出的DNS解析请求是一个递归请求,本地DNS服务器发出的解析请求是一个迭代请求
  • 建立TCP连接
    • chrome要求同一个域名下同时最多只能有6个TCP连接,超过6个的话剩下的请求就得等待
    • 进行三次握手
  • 发送HTTP请求
    • 构建请求报文,包括请求行、请求头、请求体
  • 服务器处理请求并返回响应
  • 响应完成之后,判断Connection字段,如果请求头或响应头中包含Connection: Keep-Alive,表示建立了持久连接,这样TCP连接会一直保持,之后请求同一站点的资源会复用这个连接。否则进行四次挥手,断开TCP连接。

解析

  • 将html解析为DOM树
    • DOM树本质上是一个以document为根节点的多叉树,它能够通过js操作
  • 样式计算
    • 将三种css设置方式(内联、外链、style标签)合并为styleSheets
    • 标准化样式属性,如em转为pxred转为#ff0000bold转为700等等
    • 使用继承和层叠规则计算每个节点的最终样式,生成CSSOM
      • 在计算完样式之后,所有的样式会被挂载到window.getComputedStyle当中,也就是说可以通过js来获取计算后的样式
  • 生成布局树
    • 遍历DOM树上的节点,并按照规则把它们添加到布局树中
      • 布局树只包含可见元素,比如head标签和设置了display: none的元素,将不会被放入其中
    • 确定布局树中节点的坐标位置
    • 布局树生成完毕之后触发DOMContentLoaded事件

渲染

  • 分层,构建图层树
    • 对于一些复杂的3D变换,或者使用z-index做z轴排序等,为了方便的实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树(LayerTree)
    • 接下来渲染引擎会将图层的绘制拆分成一个个绘制指令,生成绘制列表
  • 分块和栅格化
    • 在渲染进程中绘制操作是由合成线程来完成的,合成线程要做的第一件事情就是将图层分块,它会优先将视口附近的图块生成位图,将图块转换为位图的过程就是栅格化
  • 绘制
    • 栅格化操作完成后,合成线程会生成一个绘制命令,并发送给浏览器进程
    • 浏览器进程根据这个命令,把页面内容生成到内存,然后把这部分内存发送给显卡,最后渲染到显示器上
    • 显示器上的画面有固定的刷新频率,一般是60HZ,即60帧,也就是一秒更新60张图片,一张图片停留的时间约为16.7ms

注意

  • js的加载会阻塞DOM解析,在它加载并且执行完之前,不会往下解析 DOM 树。因为js可以操作dom,这两者不能并行
  • css的加载不会阻塞DOM树的解析,但会阻塞DOM树的渲染,因为需要DOM树和CSSOM树共同合成render树

重绘和回流

重绘和回流是由渲染引擎操作的,渲染引擎和JS引擎是互斥的,不可同时进行,因此在重绘和回流时,JS引擎是无法工作的,我们应该尽量避免无意义的重绘和回流,减小浏览器资源浪费。

重绘(repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流(reflow)

Render Tree中元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。 回流比重绘的代价要更高。有时即使仅仅一个元素的尺寸或位置改变,它的父元素以及任何跟它相关的元素也会产生回流,牵一发动全身。

何时触发

  • 添加/删除dom元素
  • 元素的位置、尺寸发生变化时
  • 查询某些属性或调用某些方法时
    • clientWidthclientHeightclientTopclientLeft
    • offsetWidthoffsetHeightoffsetTopoffsetLeft
    • scrollWidthscrollHeightscrollTopscrollLeft
    • getComputedStyle()
    • getBoundingClientRect()
    • scrollTo()

如何避免

  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次更改class属性
  • 避免频繁操作dom
    • 可以创建一个documentFragment,在它上面应用所有的dom操作,最后再把它添加到文档中
    • 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的dom操作不会引发回流和重绘
  • 避免频繁读取会引发回流的属性,如果确实需要多次使用,可以用一个变量缓存起来
  • 对具有复杂动画的元素使用绝对定位,使其脱离文档流,否则会引起父元素及后续元素频繁回流

内存管理及垃圾回收

V8垃圾回收机制

不同浏览器的垃圾回收机制有所不同,大部分浏览器采用的都是标记清除或者引用计数。

比如在chrome中,将堆内存分为了新生代和老生代,新生代中存放数量小存活时间短的对象,老生代中存放数量多存活时间长的对象。刚开始所有对象都在新生代,当达到一定条件会被晋升到老生代中,新生代和老生代采用的是不同的垃圾回收算法。

  • 新生代中采用scavenge算法,它将空间分成两半,每次只用一半,然后把其中活着的对象复制到另一半中,然后将这两个空间角色对换,当一个对象被复制两次以上还依然存活时,就会被晋升到老生代中。
  • 老生代中主要使用标记清除,它分为标记和清除两个阶段,在标记阶段遍历调用栈,在遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。然后在遍历过程中标记,标记完成后将垃圾数据进行清理。

内存泄露

不再用到的内存,没有及时释放,就是内存泄漏

常见原因

  • 意外的全局变量

    比如在函数中声明变量时没有写 let/const/var 关键字,这样等于声明了一个全局变量,在函数调用完之后不会被回收

    或者在函数中给this上挂载属性,当函数在全局作用域被调用时,属性就挂在了window上

  • 缓存

    有时候为了方便数据的快捷复用,我们通常会用对象键值对来缓存数据,但这种缓存通常没有过期策略,也没有大小限制,需要小心缓存超过 v8 的内存限制。

  • 闭包

    如果一个函数返回了一个具有父级函数作用域的中间函数,一旦有变量引用这个中间函数,这个中间函数将不会释放,同时原始的作用域也不会释放,除非不再引用,才会逐步释放。

避免内存泄漏

  • 少用全局变量,避免意外产生全局变量
  • 使用闭包时注意及时清理
  • 使用 WeakSetWeakMap 弱引用结构,它们对于值的引用都是不计入垃圾回收机制的
相关推荐
你挚爱的强哥4 分钟前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
y先森38 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy38 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891141 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js