写在前面
本文是基于极客时间浏览器基础课程的总结,侵权必删
浏览器特点
- 浏览器天生具有开放的 基因。以为其背后是标准的实现,而标准又是各方达成的共识。
- 可以通过浏览器的学习,更方便的了解前端知识体系,以应对日益复杂的前端体系的日益复杂
浏览器多进程
目前架构。浏览器分为主进程,渲染进程,GPU进程,插件进程,扩展进程,网络进程等等。
主进程 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
渲染进程 讲HTML,CSS,JS转换为用户可以与之交互的网页。 排版引擎和js引擎都是运行在这个进程里的。一 个Tab一个进程,‘同一站点’的不同tab会放在同一个进程中。
GPU进程 3d效果与ui绘制都是在这个进程中执行的
网络进程 之前是主进程的一个模块,后来被独立出来一个进程,用于网络资源的加载 插件进程 用于插件的运行,因插件容易崩溃。
多进程缺点
1.更高的资源占用
2.更复杂的体系架构。 浏览器各模块间的耦合性高,扩展性差。会导致现在的架构难以适应新的需求
如何解决这个问题呢?
未来面向服务的架构SOA(Services Oriented Architecture)
提供chrome基础服务。把各个模块重构成独立的服务。使用自定义的接口,通过IPC通信。使得更加的松耦合,易于维护和扩展的系统
弹性架构
在强大性能设备上会以多进程的方式运行基础服务,但是如果在资源受限的设备上(如下图),Chrome 会将很多服务整合到一个进程中,从而节省内存占用。
你认为推动浏览器发展的主要动力是什么?
- 硬件设备的快速发展。
- 日益过剩的项目需求,交互的复杂
获取网络资源之一个数据包的旅程
我们都知道,互联网世界的链接,就是靠数据堆叠出来的。那么在这个过程中,协议的目的就是为了在数据传输过程中规定相同的接收和传输方式。接下来就从一个数据包的角度去思考 从应用A到应用B的过程中经历了什么。
IP 保证数据送达指定主机
UDP 保证数据 从主机B 送达达应用B
TCP 保证数据 完整的传输
从输入URL 到展示页面的全流程
大致分为两个阶段 加载阶段 和 渲染阶段
加载阶段
tips: 会判断输入的内容是搜索信息还是 url。如果是搜索信息就会调用浏览器自己的搜索引擎进行检索否则执行下面步骤
- 查看是否有协议信息否则,增加协议 例如 www.baidu.com
- 浏览器进程会通过进程间通信(IPC),将URL发送给网络进程,并有网络进程发起请求,进行 html资源的请求
- 首先会查询本地缓存,有缓存直接返回缓存信息,否则执行下一步
- 调用cdn服务查询真实的IP地址,如果请求协议是 HTTPS,那么还需要建立 TLS 连接。
- 建立TCP链接,并开始请求数据。
- 拦截到响应后,如果遇到响应头的状态码为 301或302,就会查找 响应头中的Location字段包含的URL并重新从第1步执行
- 当状态码为200时,判断 响应头的ContentType字段是否是 html。如果是文件资源则交给下载进程并结束当前流程。否则进入提交文档阶段
准备渲染阶段
准备渲染进程
浏览器会把属于从A页面打开的B页面如果属于同一根域名的,浏览器会复用同一个渲染进程,否则开辟一个新的渲染进程
提交文档阶段
- 文件传输完成后,
浏览器进程会发送 提交文档的消息给渲染进程,渲染进程接收到 提交文档 的消息后,与网络进程建立传输数据的管道 - 提交完成后,
网络进程发送 确认提交消息给浏览器进程 - 浏览器进程接收到消息后,会刷新浏览器界面及信息(页面状态,导航 历史状态,安全状态,地址栏URL)。并进入真正的 渲染阶段
渲染阶段
- 渲染进程拿到HTML资源后, 会开始加载子资源并把资源丢给渲染引擎进行渲染流水线操作。
- 浏览器会根据 HTML 生成DOM 树。
- 根据css文件计算出 cssSheet树
- 合并两个文件,生成 渲染树(布局树),主要工作就是合并样式,并确认元素位置
- 分层,浏览器会将具有 层叠上下文属性或 需要剪裁的内容进行分层,目的是为了使2d平面具有3d效果
- 渲染引擎将待绘制的元素根据绘制的规则拆分成一个个指令,并把指令合并成一个待绘制列表,
- 当绘制列表准备好后,把待绘制,主线程会把该绘制列表提交(commit) 给合成线程,
- 合成线程把图层根据视口大小划分为图块,通常是 256x256 或者 512x512,
- 然后合成线程将按照靠近视口的图块放进栅格化线程池,并由栅格化线程池依次生成位图。
- 通常会使用GPU线程加速栅格化(快速栅格化)。当使用GPU加速后,栅格化线程池会发送生成图块的指令,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。
- 所有的图块光栅化后,合成线程会发送”DrawQuad“指令给浏览器进程来绘制图块。
- 当 浏览器进程 中的 viz组件接收到指令后,会根据指令内容绘制在内存中,然后再将内存中的内容显示在页面上
重绘&重排&合成
重排如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等;触发整个渲染流水线重绘如果通过 JavaScript 更改某些元素的背景颜色;触发 第5条以后的工作合成如果使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段;触发第7步以后的工作
需要注意的是, css的阻塞是不会影响dom的渲染的。只不过会出现没有样式。等到css返回会重新从第4步开始
js 执行之路
js 执行阻塞DOM渲染的一些场景
前置条件: 当页面拿到HTML页面之后就会开始执行上面DOM渲染的流程。但是遇到script标签之后,就会区分下面三种执行方式 defer 与 async 与 默认script标签
默认script标签同步请求资源,并停止DOM渲染的流程。js代码执行完成后,继续解析html- 如果在JavaScript中访问了某个元素的样式,那么这时候就需要等待这个样式被下载完成才能继续往下执行,所以在这种情况下,CSS也会阻塞DOM的解析。
- 无论内联或外链 都会阻塞DOM的渲染
defer并行请求资源,等待DOM渲染完成后,再执行当前js脚本async并行请求资源,并立即执行当前js脚本,会阻塞 DOM渲染
js 代码的执行可以分为 编译阶段和执行阶段(这是针对同一执行上下文)。从宏观角度看,js代码是一边执行一边编译。如何理解呢,就是执行一段js代码的同时,当遇到函数调用时,再去创建函数执行上下文并执行。
编译阶段
-
编译阶段会通过变量提升的创建执行上下文和可执行代码
小知识点
- 如果 变量定义的函数和具名函数名相同 区分调用时机,先于变量赋值则使用 具名函数,否则使用定义的函数
- 如果 两个同名变量 后定义的函数会覆盖先定义的函数
- 如果 两个函数名相同 后定义的函数会覆盖先定义的函数
-
创建执行上下文会遵循下面规则
- 无论任意执行上下文,都是 把创建的执行上下文压入调用栈并执行。待执行完毕后出栈,并等待垃圾回收时,回收代码
- 默认创建全局执行上下文,执行代码,跟随页面的生命周期销毁
- 当调用函数时,创建函数执行上下文,执行代码,函数执行完,并销毁
- 当使用
eval函数时,也会创建独立的执行上下文,执行代码,代码执行完毕并销毁
-
执行上下文中包含 变量环境,词法环境,可执行代码三个部分
- 变量环境,使用 var或function定义的变量
- 词法环境,使用 const 和 let 定义的变量
- 可执行代码, 除了变量声明和函数声明定义之外的 代码
- 函数只会在第一次执行时被编译,但是此时的变量环境和词法环境只存在最顶层(不包含块级作用域)的数据。
- 当执行到块级作用域的时候,块级作用域中通过let和const申明的变量会被追加到词法环境中,当这个块执行结束之后,追加到词法作用域的内容又会销毁掉。
- 执行阶段会按照代码由上到下执行。
- 当执行时遇到函数调用,会取出函数并创建函数执行上下文;
- 当执行代码时,遇到 事件绑定会把找到 监听的目标元素,
- 然后创建一个事件监听器对象,包含要监听的事件和回调
- 把创建好的事件监听器对象放到目标元素的事件监听列表中。
- 当事件触发时,依次执行监听器列表中的回调。
作用域
作用域 就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
es6之前,作用域分为 全局作用域 与 函数作用域
-
全局作用域中的对象,在代码的任何位置都可以访问
-
函数作用域,就是在函数内部定义的变量或函数
es6之后 引入了 块级作用域 一切包裹在 花括号{}中的变量都是块级作用域
// if块
if(1){}
// while块
while(1){}
// 函数块
function foo(){}
// for循环块
for(let i = 0 ; i<10; i++) {}
// 单独的块
{}
暂时性死区
上面的第5条中提到了块级作用域的代码编译流程中说过,通过let和const 声明的变量会在执行时加入词法环境中,所以在声明前使用变量是会出现暂存死区的。
作用域链
在每个执行上下文的变量环境中都包含一个outer 对象。这个对象指向的就是对象的外部引用,也可以理解为father执行上下文。全局执行上下文的outer为空。js的变量查找过程就是沿着这个outer属性查找的。 这条通过outer串联起来的查找路径就是作用域链。
词法(静态)作用域 与 动态作用域
两者的区别在于 作用域的确认时机;
采用静态作用域,作用域是在代码书写阶段就确定的;而动态作用域是在代码执行时才能确定。 举个例子
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
当采用静态作用域时,执行foo函数,先从foo函数内部查找是否有局部变量value,如果没有,就根据书写的位置,查找上面一层的代码,在这里是全局作用域,也就是value等于1,所以最后会打印1
当采用动态作用域时,执行foo函数,依然是从foo函数内部查找是否有局部变量value。如果没有,就从调用函数的作用域,也就是bar函数内部查找value变量,所以最后会打印2
闭包
何为闭包?
根据词法作用域的规则,内部函数用于可以访问外部函数的变量,即使外部函数已经执行完毕,但内部函数引用外部函数的变量依旧存在,我们把这些变量的集合称为闭包。如果外部函数名为 foo,那么这些变了的集合就称为foo函数的闭包
使用场景
- 当我们希望有些变量作为 私有变量使用时。就可以使用闭包创建一个高阶函数。只提供方法使用
- 循环为多个节点绑定同一个监听方法,并共同操作同一个对象
闭包的回收
- 如果闭包的函数是一个全局变量,闭包会一直等到页面关闭
- 如果闭包的函数是一个局部变量,会等到局部作用域销毁后,在下次js引擎垃圾回收时,回收这块内存。
this指向
- 默认的函数调用 指向 全局变量,严格模式 指向 undefined
- 对象形式调用 指向对象本身
- 嵌套的函数中或回调函数,this指向 同1
- 箭头函数没有自己的this。继承外部环境的this
bind call apply
- call:执行函数。参数形式为 零散的
- apply: 执行函数。函数的参数 为一个数组
- bind: 返回新函数。参数形式为 零散的。
事件循环机制
js任务分为同步和异步
- 同步:在主进程中顺序执行的任务
- 异步:不在主进程中执行,而是挂起到任务队列中的任务
异步任务分为 微任务与宏任务
- 宏任务: script( 整体代码)、setTimeout、setInterval、UI 交互事件、setImmediate(Nodejs环境)
- 微任务:promise,MutationObserver,process.nextTick(Nodejs环境)
执行顺序
- 首先,浏览器会把js代码从上到下执行,执行时,遇到宏任务或微任务会放到队列中,然后继续执行同步任务直至没有同步代码需要执行,
- 随后,JavaScript 引擎首先从宏任务队列中取出第一个任务;
- 执行完毕后,再将微任务中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行,也就是说在执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行。
- 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。
也是就是说,一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务。