浏览器工作原理与实践

Scroll Down

Chrome打开一个页面,四个进程

线程VS进程

  1. 线程不能单独存在,它是由进程来启动和管理
  2. 一个进程是一个程序的运行实例

单进程浏览器

  1. 不安全
  2. 不稳定
  3. 不流畅

多进程浏览器(多进程时代)

  1. 浏览器进程
  2. 渲染进程
  3. GPU进程
  4. 网络进程
  5. 插件进程

TCP协议:页面完整送达浏览器

  1. 建立连接阶段(三次握手)
  2. 传输数据阶段(接收端需要对每个数据包进行确认操作)
  3. 断开连接阶段(四次挥手)

HTTP请求流程:第二次打开速度会变快(DNS缓存,页面资源缓存)

浏览器发起HTTP请求

  1. 构建请求
  2. 查找缓存(浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术)
  3. 准备IP地址和端口
  4. 等待TCP队列(Chrome机制,同一个域名最多只能连接6个TCP连接)
  5. 建立TCP
  6. 发送HTTP请求

服务器处理HTTP请求

  1. 返回请求
  2. 断开连接(keep-alive:保持TCP连接可以省去下次请求时需要建立连接的时间,提升资源加载速度。)
  3. 重定向

导航流程:从输入URL到页面展示

描述流程

  1. 用户从浏览器进程输入请求信息
  2. 网络进程发起URL请求
  3. 服务器响应请求,浏览器进程开始准备渲染进程(从旧页面中打开新页面,新页面与旧页面属于同一个站点,新页面将会复用父页面的渲染进程)
  4. 渲染进程准备好,需要向渲染进程提交页面数据,提交文档阶段
  5. 渲染进程收到文档,便开始解析页面和加载子资源,完成页面渲染

进程流程

浏览器进程 -> 网络进程 -> 渲染进程

渲染进程:(上) HTML、JS、CSS变成页面

  1. 构建DOM树
  2. 样式计算
    把CSS转换为浏览器能够理解的结构(styleSheets);
    计算出DOM树中每个节点的具体样式;
    转换样式表中属性值,使其标准化:
    例:
p { font-size: 2em } => p { font-size: 32px}
  1. 布局阶段(创建布局树, 布局计算)

渲染进程:(下) HTML、JS、CSS变成页面

渲染流程描述

  1. 渲染进程将HTML内容转换成DOM树
  2. 渲染引擎将CSS样式转化stylesheets,计算DOM节点样式
  3. 创建布局树,计算元素布局信息
  4. 对布局树分层,创建分层树
  5. 为每个图层创建绘制列表,并提交合成线程
  6. 合成线程将图层分成图块,并在光栅化线程池将图层转换成位图
  7. 合成线程发送绘制图块命令到DrawQuad给浏览器进程
  8. 浏览器进程根据DrawQuad消息生成页面,并显示

相关概念

  1. 更新元素的几何属性(重排),需要更新完整的渲染流水线,开销大
  2. 更新元素的绘制属性(重绘),省去布局和分层阶段,执行效率比重排要高一些

变量提升:JS按顺序执行么?

变量提升:

在JS执行过程中,js引擎把变量的声明部分和函数声明部分提升到代码开头,
变量提升后设置默认值 undefined。

JS执行流程

  1. 编译阶段:
    变量提升代码;执行部分代码;两个相同名字的函数,那么最终生效的是最后一个函数;
  2. 执行阶段

调用栈:JS栈溢出

  1. 栈:后进先出
  2. 调用栈有大小,超出则报错,这就是栈溢出
  3. 常见:递归,没有任何条件终止

块级作用域:var缺陷以及引用let,const

作用域:

作用域控制着变量和函数的可见性和生命周期

  1. 全局作用域:任何地方都能访问
  2. 函数作用域:函数内部访问,函数执行结束,变量会被销毁
  3. 块作用域(let):一对大括号包裹的一段代码

变量提升所带来的问题

  1. 变量容易在不被察觉的情况下被覆盖掉
  2. 本应销毁的变量没有被销毁:for(var i ....) 中的i

如何支持块级作用域

第一步编译并创建执行上下文:

  1. var 声明的变量,存放在变量环境
  2. let 声明的变量,被存放在词法环境
  3. 函数作用域内部,let声明变量并没有存放在词法环境

第二步继续执行代码

作用域和闭包:js相同代码如何选择

作用域链:

查找变量的链条,由词法作用域决定。

词法作用域:

  1. 由代码中函数声明的位置决定的,所以词法作用域是静态的作用域。
  2. 代码阶段就决定好的,和函数是怎么调用的没有关系

闭包

内部函数总是可以访问其外部函数中声明的变量。(回调函数、返回一个函数)使用不当,会很容易造成内存泄漏。

this: js上下文视角

全局执行上下文的this:

  1. 非严格下指向window,
  2. 严格下指向undefined。

函数执行上下文的this:

  1. 在全局环境中调用一个函数,函数内部的this指向全局变量window
  2. 通过一个对象调用其内部的一个方法,该方法执行上下文this指向对象本身

this设置

  1. 通过函数的call、bind、apply方法设置
  2. 通过对象调用方法设置
  3. 通过构造函数设置

自定义new

  1. 创建一个空对象(即{});
  2. 新对象隐式原型__proto__链接到构造函数显式原型prototype上
  3. 新创建的对象作为 this 的上下文
  4. 如果该函数没有返回对象,则返回 this
function _new(constructor, ...arg) {
  var obj = {}; 
  obj.__proto__ = constructor.prototype;
  var res = constructor.apply(obj, arg);
  return Object.prototype.toString.call(res) === '[object Object]' ? res : obj; 
}

this设计缺陷

  1. 嵌套函数中的this不会从外层函数继承(使用es6箭头函数可解决;声明变量selft存放this。)
  2. 普通函数中的this默认指向全局对象window

栈空间和堆空间:数据如何存储

  1. 静态语言:
    在使用之前就需要确认其变量数据类型
  2. 动态语言:
    在运行过程中需要检查数据类型的语言
  3. 支持隐式类型转换称为弱类型语言,不支持则强类型语言
  4. 内存空间
    1. 代码空间
    2. 栈空间(原始类型的数据值)
    3. 堆空间(引用类型的数据值)
    4. 原始类型的赋值会完整赋值变量值、而引用类型的赋值时复用引用地址

垃圾回收:垃圾数据如何回收

垃圾回收策略

  1. 手动回收:C语言
  2. 自动回收(垃圾回收器):js,java,python。

调用栈回收:

js引擎通过向下移动ESP(记录当前执行状态的指针)来销毁函数保存在栈中的执行上下文。

堆回收

  1. 新生代(副垃圾回收器):
    存放的是生存时间短的对象
    副垃圾回收器:Scavenge算法。
    对象晋升策略:新生代经历两次垃圾回收还存活对象,将移动到老生代。
  2. 老生代(主垃圾回收器):
    存放的是生存时间久的对象
    主垃圾回收器:标记清除、标记整理
  3. 增量标记:
    垃圾回收和js应用逻辑交替进行。

编译器和解释器:v8如何执行js

编译型语言:

需要通过编译器的编译过程、并且编译之后直接保留机器能读懂的二进制文件,这样下次运行程序时,直接运行该二进制文件。

解释型语言:

每次运行时都需要通过解释器对程序进行动态解释和执行

V8执行一段js代码

  1. 生成抽象语法树AST和执行上下文(babel):
    1. 分词,称为词法分析
    2. 解析,称为语法分析
  2. 生成字节码:
    介于AST和机器码之间的一种代码,但是与特定类型机器码无关,字节码需要解释器将其转换为机器码后才能执行。
  3. 执行代码:
    热点代码:再次执行时,只需要执行编译后的机器码就可以。
    即使编译:解释器在解析执行字节码的同时,收集代码信息,
    判断是否热代码,如果是热代码便将热代码的机器码保存起来。

JS性能优化

  1. 提升单次脚本的执行速度,避免js的长任务霸占主线程
  2. 避免大的内联脚本,因为在解析HTML过程中,解析和编译也会占用主线程
  3. 减少js文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存

消息队列和事件循环:页面活起来

  1. 事件循环机制:
    要想在线程运行过程中,能接收并执行新的任务。
  2. 消息队列:
    是一种数据结构,可存放要执行的任务,符合先进先出
  3. 渲染进程专门有一个io线程用来接收其他进程传进来的消息(IPC)

页面使用单线程缺点

  1. 处理高优先级任务:宏任务、微任务。
  2. 单个任务执行时长过久:js通过回调功能规避这种问题。

重剑无锋:js面向对象

三大特征

封装:

限制对象内部组件直接访问机制;将数据和方法绑定起来,对外提供方法,从而改变对象状态机制
1. 闭包: 函数调用改变其上下文中的其他变量;函数返回值不同
2. 纯函数:函数调用不允许改变其所属的上下文;相同入参函数调用一定能得到返回值

继承:

一个对象或者类能够自动保持另一个对象或者类的实现的一种机制

  1. js原型链继承:
C.prototype = new P()
  1. js构造继承:
C(){
P.call(this)
}

多态:

同样的接口有着不同的实现。

函数一等公民:

函数可以不依附任何类或对象等实体而独立存在,它可以单独作为参数、变量或者返回值在程序传递。

百花齐放:前端MVC

Angula

  1. 双向绑定:数据对象发生变更以后,要及时更跟DOM树;用户操作改变DOM树以后,要回头更新数据对象
  2. 依赖注入:$scope
  3. 过滤器:业务对象到视图对象的转换,简单而清晰解决

React + Redux

  1. 数据单向流动,状态被隔离,严重地管理起来
  2. Redux:
    1. 不同的操作,只不过引起Action的type不同,或者上面承载的业务数据不同。
    2. 无状态,纯函数,Store中状态的改变也只有一个来源

WebApi: settimeout 如何实现

渲染进程创建一个回调任务,包含showName、当前发起时间、延迟执行时间

注意事项:

  1. 如果当前任务执行时间过久,会影响延迟到定时器任务的执行
  2. 如果settimeout存在嵌套调用,那么系统会设置最短时间间隔4毫秒
  3. 未激活的页面,settimeout执行最新间隔是1000毫秒
  4. 延时执行时间有最大值24.8天
  5. 使用settimeout设置的回调函数中this全局。(使用箭头函数)

requestAnimationFrame与浏览器刷新同步,而settimeout设置16.7也会存在延迟

不一样的体验:交互设计

单页面应用SPA:

  1. 不易于网页的SEO
  2. 浏览器上网页操作功能,后退、前进

渐进式增强:语义

响应式布局:

  1. meta
  2. @media

WebApi: XMLHttpRequest如何实现

回调函数(同步回调):

将一个函数作为参数给另外一个函数

异步回调:

回调函数在主函数外部执行的过程

运行机制

  1. 创建XMLHttpRequest对象
  2. 为XHR对象注册回调函数
  3. 打开请求
  4. 配置基础的请求信息
  5. 发起请求

问题:

  1. 跨域问题
  2. HTTPS混合内容

宏任务和微任务:待遇问题

宏任务:

  1. 渲染事件,解析HTML,计算布局,绘制
  2. 用户交互事件,鼠标点击,滚动页面
  3. js脚本执行事件
  4. 网络请求完成,文件读写完成事件

微任务:

一个需要执行异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务之前

执行顺序

  1. 微任务和宏任务是绑定的,每个宏任务在执行时,都会创建自己的微任务队列
  2. 微任务的执行时长会影响当前宏任务的时长
  3. 在一个宏任务中,分别创建一个用于回调任务的宏任务和微任务,
    无论什么情况下,微任务都早于宏任务执行

Promise:告别回调函数

异步编程:

  1. 代码逻辑不连续
  2. 回调地狱(嵌套调用、任务的不确定性)

优势

  1. 消灭嵌套调用(实现回调函数的延时绑定,需要将回调函数OnResolve的返回值穿透到最外层)
  2. 合并多个任务的错误处理(实现错误“冒泡”)

async/awit:使用同步方式

async:

async是一个通过异步执行并隐式返回promise作为结果的函数

await:

默认创建一个Promise对象,暂停当前协程执行。将主线程的控制权转交给父协程,父协程结束之前,检查微任务,执行await后函数。

解决问题:

  1. promise 出现大量的then函数,使得代码不是很容易阅读;
  2. async/await提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源能力,使得代码逻辑更加清晰。

生成器:

生成器函数时一个带星号函数,而且时可以暂停执行和恢复执行;

function * A(){ yield ----};
  1. 遇到yield关键字,js引擎将返回关键字后面的内容给外部,并暂停函数执行;
  2. 外部函数可以通过next方法恢复函数执行;

DOM树:js如何影响DOM构建

js文件下载过程会阻塞DOM解析

DOM:

  1. DOM是生成页面的基础数据结构
  2. DOM提供给js脚本操作的接口,通过接口,js可对DOM结构进行访问,从而改变文档结构、样式和内容
  3. DOM是一道安全防线,一些不安全的内容在DOM解析阶段排除在外

DOM树生成:(HTML解析器;XSSAuditor:安全检查模块,检测DOM词法安全)

  1. 通过分词器将字节流转换为Token:
    Token栈结构:

    1. StartTag
    2. 文本
    3. endTag
  2. 将Token解析为DOM节点,

  3. 并将DOM节点添加到DOM树中

渲染流水线:css如何影响白屏时间

CSSDOM:

  1. 提供给js操作样式表的能力
  2. 为布局树的合成提供基础的样式信息

样式计算:

渲染引擎为对应的DOM元素选择对应的样式信息;

计算布局:

渲染引擎需要计算布局树中每个元素对应的几何位置;

减少白屏时间策略:

  1. 内联js、css,获取html文件之后就可以直接开始渲染
  2. 尽量减少文件大小
  3. js标记上sync 或者 defer
  4. 对过大的css文件,可以通过媒体查询属性,将其拆分多个不同用途的css文件,在特定的场景才会加载特定css文件

分层和合成机制:css动画高效

生成一帧图像:

重排、重绘、合成(合成线程:分层、分块、合成)

css动画:不会影响主线程执行

页面性能:如何优化

加载阶段:

减少关键资源个数、降低关键资源大小、降低关键资源的RTT次数。
RTT:数据接收发送往返时间

交互阶段:

  1. 减少JavaScript脚本执行时间
  2. 避免强制同步布局(js强制计算样式、布局到当前任务中)
  3. 避免布局抖动(避免for循环取DOM值)
  4. 合理利用css合成动画
  5. 避免频繁的垃圾回收

虚拟DOM:真实DOM不同

实际DOM缺陷:

  1. 牵一发而动全身,容易引起重绘、重排
  2. 引发强制同步布局、布局抖动问题

虚拟DOM解决事情:

  1. 页面内容修改应用虚拟DOM上,而不是直接应用在实际DOM上
  2. 变化应用在虚拟DOM上,不急着去渲染页面
  3. 虚拟DOM收集足够的改变时,变化一次性应用在实际DOM上

虚拟DOM运行:

  1. 创建阶段:依据JXS和基础数据创建,反映了真实的DOM,然后创建真是DOM树,再触发渲染流水线输出页面
  2. 更新阶段:数据发生改变时,创建新的虚拟DOM树,然后跟旧的虚拟DOM树比较,找出变化的地方,一次性更新到DOM树上,再渲染

渐进式网页应用pwa

WEB应用缺少:

  1. 缺少离线使用能力
  2. 缺少推送消息推送能力
  3. 缺少一级入口

PWA:

  1. Service Worker 解决离线存储和消息推送(Service Worker:
    主要思想是在页面和网络之间增加一个拦截器,用来缓存和拦截请求)
  2. manifest.Json 解决一级入口

WebComponent

组件化:对内高内聚、对外低耦合

实现:

  1. 使用Template属性来创建模板
  2. 需要创建一个GeekBang的类(查找模板内容;创建影子DOM将模板添加到影子DOM上)
  3. 正常使用HTML元素一样使用该组件元素

影子DOM:

  1. 对于整个网页是不可见的
  2. css不会影响整个网页的CSSOM,对于内部元素起作用

HTTP1:HTTP性能优化

HTTP/0.9:

  1. 只有一个请求行,并没有HTTP请求头和请求体
  2. 服务器没有返回头信息
  3. 返回的文件内容是以ASCII字符流传输的

HTTP/1.0:

  1. 引入请求头和响应头
  2. 引入状态码
  3. 减轻服务器的压力,提供了Cache机制,用来缓存已经下载过的数据
  4. 加入用户代理的字段

http/1.1:

  1. 改进持久连接
  2. 不成熟的HTTP管线化,解决队头阻塞问题
  3. 提供虚拟主机的支持
  4. 对动态生成的内容提供了完美支持
  5. 客户端Cookie,安全机制

HTTP2:提升

HTTP/1.1网络效率优化:

  1. 增加了持久连接
  2. 浏览器为每个域名最多同时维护6个TCP持久连接
  3. 使用cdn的实现域名分片机制

HTTP/1.1问题(对带宽的利用率并不理想):

  1. TCP的慢启动
  2. 同时启动了多条TCP连接,那么这些连接会竞争固定的带宽
  3. 队头阻塞

HTTP/2.0:

  1. 多路复用(二进制分帧层,请求头带有id编号)
  2. 可以设置请求的优先级
  3. 服务器推送
  4. 头部压缩

HTTP3.0

TCP问题:

  1. TCP队头阻塞
  2. TCP建立连接的延时

QUIC协议:

  1. 实现了类似TCP的流量控制、传输可靠性的功能
  2. 集成了TLS加密功能
  3. 实现了HTPP/2.0的多路复用功能
  4. 实现了快速握手功能

同源策略

浏览器安全:

  1. Web页面安全
  2. 浏览器网络安全
  3. 浏览器系统安全

同源策略表现(如果两个URL的协议、域名和端口都相同,那么这两个URL同源):

  1. DOM层面,限制来自不同源JS脚本对当前DOM对象操作
  2. 数据层面,限制不同源站点读取当前站点的Cookie,储存等数据
  3. 网络层面,限制接口请求,跨域问题。

安全和便利性权衡:

  1. 页面中可以嵌入第三方资源,CSP技术(思想是让服务器决定浏览器能够加载哪些资源)
  2. 跨域资源共享和跨文档消息机制。CORS跨域资源共享,window.postMessage跨文档消息机制。

跨站脚本攻击XSS

xss攻击(往HTML文件中或DOM中注入恶意脚本):

  1. 可以窃取Cookie信息
  2. 可以监听用户行为
  3. 可以修改DOM
  4. 可以在页面内生成浮窗广告

XSS攻击类型:

  1. 存储型(通过服务器对输入的内容进行过滤或者转码)
  2. 反射型(充分利用CSP)
  3. 基于DOM的XSS攻击(使用HttpOnly保护重要的Cookie信息)

CSRF攻击

CSRF攻击(利用用户登录状态,并通过第三方站点进行恶意操作):

  1. 自动发起GET请求
  2. 自动发起POST请求
  3. 引诱用户点击链接

防止:

  1. 充分利用好Cookie的SameSite属性
  2. 验证请求的来源站点(Referer)
  3. Csrf token

安全沙箱:页面和系统的隔离

操作系统和渲染进程隔离,渲染经常由于漏洞被攻击,也不会影响操作系统

影响:

  1. 持久存储
  2. 网络访问
  3. 用户交互

HTTPS:数据传输安全

安全层:

  1. 对发起的http请求的数据进行加密操作
  2. 对接收HTTP的内容进行解密操作

HTTPS协议:

第一版: 使用对称加密
第二版:使用非对称加密
第三版: 对称加密和非对称加密搭配使用
第四版:添加数字证书