浏览器(内核,同源策略原理,渲染...)

元气小坏坏 提交于 2020-01-19 02:37:30

浏览器存储

特点 cookie localStorage sessionStorage indexDb
生命周期 可过期 除非清理,否则一直存在 页面关闭就清理 除非清理,否则一直存在
存储大小 4K 5M 5M
与服务端通信 请求携带在 header 头部 no no no

浏览器内核

浏览器最重要或者说核心的部分是“Rendering Engine”,可大概译为“渲染引擎”,不过我们一般习惯将之称为“浏览器内核”。

通常所谓的浏览器内核也就是浏览器所采用的渲染引擎

浏览器内核主要包括三个分支技术:排版渲染引擎JavaScript引擎以及其他

  1. Trident IE 内核
    其中 IE8 的 JavaScript 引擎是 JScript 引擎, IE9 开始使用 Chakra
  2. Gecko FF 内核
    JavaScript 引擎使用 Spider Monkey 第一款 JavaScript 引擎
  3. Webkit Safari 内核 Chrome 内核原型
    Android 默认浏览器使用 Webkit 内核
  4. Blink Chrome 最新的内核(Safari 目前也使用的内核)
    而谷歌方面,则使用了自己研发的 V8 引擎
内核 是否开源 插件支持 应用浏览器 支持操作系统
Trident 否,但提供接口调用 ActiveX IE Windows
Gecko 是,多种协议授权发行 MPL、GPL、LGPL NPAPI Firefox Windows,Mac,Linux/BSD
Blink NPAPI Chrome,Opera Windows,Mac,Linux/BSD
Webkit 是,遵从LGPL协议 NPAPI Chrome,Safar Windows,Mac,Linux/BSD

可这样理
解,浏览器内核虽然包括三个分支,但其主要就是完成页面渲染的排版引擎

因为网页浏览器的排版引擎(Layout Engine或Rendering Engine)也被称为浏览器内核、页面渲染引擎或模板引擎,它负责取得网页的内容(HTML、XML、图像等等)、整理消息(例如加入CSS等),以及计算网页的显示方式,然后会输出至显示器或打印机。所有网页浏览器、电子邮件客户端以及其它需要根据表示性的标记语言(Presentational markup)来显示内容的应用程序都需要排版引擎

所以说排版引擎就是用来渲染 HTML CSS 等页面的

而另一个很重要的就是 JavaScript 引擎

排版引擎

  1. WebCore
    该引擎是在 KHTML 引擎基础上而来的
  2. KHTML

浏览器解析 HTML

目前浏览器的排版引擎有三种模式:

  1. 怪异模式(Quirks mode)
  2. 接近标准模式(Almost standards mode)
  3. 标准模式(Standards mode)

对HTML文件来说,浏览器使用文件开头的 DOCTYPE 来决定用怪异模式处理或标准模式处理

渲染原理

网页的生成过程,大致可以分成五步:

  1. HTML代码转化成DOM

    • 当服务器返回一个HTML文件给浏览器的时候, 浏览器接受到的是一些字节数据
    • 根据请求头部信息的编码方式, 对字节流进行编码, 得到 HTML 字符串
    • 当我们浏览器获得HTML文件后,会自上而下的加载,并在加载过程中进行解析和渲染
    • 加载说的就是获取资源文件的过程,如果在加载过程中遇到外部 CSS 文件和图片,浏览器会另外发送一个请求,去获取 CSS 文件和相应的图片,这个请求是异步的,并不会影响 HTML 文件的加载
    • DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点
    • HTML 解析器解析
  2. CSS代码转化成CSSOM(CSS Object Model)

    • DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model. 这样的方式生成最终的数据
    • display:none 的节点不会被加入 Render Tree,而 visibility: hidden 则会,所以,如果某个节点最开始是不显示的,设为 display:none 是更优的
  3. 结合 DOM 和 CSSOM, 生成一棵渲染树(Render Tree 包含每个节点的视觉信息)

    • 根据 DOM 和 CSSOM 来构建 Render Tree(渲染树)
    • 注意渲染树,并不等于 DOM 树,因为一些像 headdisplay:none 的东西,就没有必要放在渲染树中了
    • 得到 Render Tree ,然后计算出每个节点在 layout 上的位置
  4. 生成布局(layout), 也叫 flow

    • 即将所有渲染树的所有节点进行平面合成
    • 前三步都很快, 麻烦点的就是最后这两步
  5. 将布局绘制(paint)在屏幕上

    • 按照算出来的规则,通过显卡,把 layout 画到屏幕上
    • 当最后一个节点被绘制, 事件 DomContentloaded 就会发生
    • 如果此时的, link css 或者是 img 或是 script 引用的外部资源未从服务器返回
    • 生成布局(flow)和绘制(paint)这两步,合称为"渲染"(render)

以上的五步只是浏览器在第一时间渲染的情况

JavaScript被认为是解析阻塞资源,所以可以加上 async 属性给 script 标签达到异步加载避免阻塞

在网页生成的时候, 至少会渲染一次

重新生成布局&重新绘制(reflow & repaint)

一旦网页在生成之后发生了以下三种情况就会重新渲染一次

  1. 修改 DOM
  2. 修改 CSSOM
  3. 用户事件

重新渲染,就需要重新生成布局和重新绘制。前者叫做"重排"(reflow),后者叫做"重绘"(repaint)。

repaint 不一定需要 reflow,比如改变某个网页元素的颜色,就只会触发 repaint,不会触发 reflow ,因为布局没有改变
reflow 必然导致 repaint,比如改变一个网页元素的位置,就会同时触发 reflow 和 repaint,因为布局改变了

display:none 会触发 reflow, 而 visibility:hidden 只会触发 repaint, 因为没有发现位置变化

  • reflow
    • 元件的几何尺寸变了,我们需要重新验证并计算Render Tree。是Render Tree的一部分或全部发生了变化
    • reflow 几乎是无法避免的
    • 9 种主要触发 reflow 的动作
      1. 调整窗口大小(Resizing the window)
      2. 改变字体(Changing the font)
      3. 增加或者移除样式表(Adding or removing a stylesheet)
      4. 内容变化,比如用户在input框中输入文字(Content changes, such as a user typing text inan input box)
      5. 激活 CSS 伪类,比如 :hover (IE 中为兄弟结点伪类的激活)(Activation of CSS pseudo classes such as :hover (in IE the activation of the pseudo class of a sibling))
      6. 操作 class 属性(Manipulating the class attribute)
      7. 脚本操作 DOM(A script manipulating the DOM)
      8. 计算 offsetWidth 和 offsetHeight 属性(Calculating offsetWidth and offsetHeight) 根据此可以实现一个jquery插件,让元素回流并重绘。ex. el.style.left=20px; a = el.offsetHeight;el.style.left=22px;
      9. 设置 style 属性的值 (Setting a property of the style attribute)
    • CSS 避免 reflow
      1. 通过改变元素的类名改变样式,并尽可能在子节点中改变
      2. 避免设置多项内联样式
      3. 使用 fixed || absolute 应用动画元素
      4. 权衡平滑和速度
      5. 避免使用 table 布局
      6. 避免使用 CSS 的 JavaScript 表达式 (仅 IE 浏览器)
      7. 精简 CSS,避免复杂 CSS 或 CSS 选择器, 并避免使用运算式
  • repaint
    • 改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变

加载首屏

·首屏时间和DomContentLoad事件没有必然的先后关系
·所有CSS尽早加载是减少首屏时间的最关键
·js的下载和执行会阻塞Dom树的构建(严谨地说是中断了Dom树的更新),所以script标签放在首屏范围内的HTML代码段里会截断首屏的内容。
·script标签放在body底部,做与不做async或者defer处理,都不会影响首屏时间,但影响DomContentLoad和load的时间,进而影响依赖他们的代码的执行的开始时间。

一个关于加载首屏

性能提升

  1. 尽量不要把读操作和写操作放在一个语句里面, 不要两个读(写)操作之间,加入一个写(读)操作
  2. 样式表越简单,重排和重绘就越快
  3. 重排和重绘的 DOM 元素层级越高,成本就越高
  4. table 元素的重排和重绘成本,要高于 div 元素
  5. 如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排
  6. 不要一条条地改变样式,而要通过改变class,或者csstext属性,一次性地改变样式
  7. 尽量使用离线DOM,而不是真实的网面DOM,来改变元素样式
  • 操作 Document Fragment对象,完成后再把这个对象加入DOM
  • 使用 cloneNode() 方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点
  1. 先将元素设为 display: none(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染
  2. position 属性为 absolutefixed 的元素,reflow 的开销会比较小,因为不用考虑它对其他元素的影响
  3. 只在必要的时候,才 diaplay 显示因为不可见的元素不影响重排和重绘, visibility : hidden 的元素只对 repaint 有影响,不影响 reflow
  4. 使用虚拟 DOM 的脚本库,比如 React,Vue
  5. 使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法调节重新渲染,IE8-不支持

刷新率

网页动画的每一帧(frame)都是一次重新渲染

每秒低于24帧的动画,人眼就能感受到停顿。一般的网页动画,需要达到每秒30帧到60帧的频率,才能比较流畅。如果能达到每秒70帧甚至80帧,就会极其流畅

大多数显示器的刷新频率是60Hz,为了与系统一致,以及节省电力,浏览器会自动按照这个频率,刷新动画(如果可以做到的话

所以,如果网页动画能够做到每秒60帧,就会跟显示器同步刷新,达到最佳的视觉效果。这意味着,一秒之内进行60次重新渲染,每次重新渲染的时间不能超过16.66毫秒

一秒之间能够完成多少次重新渲染,这个指标就被称为"刷新率",英文为FPS(frame per second)

Script 之 async/defer

  1. 默认 script 加载情况
  • 浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行
  • 加载
    • 同步加载
    • 会阻塞渲染引擎
  • 执行
    • 同步加载完成立即执行
    • 会阻塞渲染引擎
    • 执行完成继续渲染引擎
  1. async 加载
  • 异步执行引入的 JavaScript
  • 加载
    • 异步无序加载
    • 不会阻塞渲染引擎
  • 执行
    • 一旦加载完成就进行执行
    • 会阻塞渲染引擎
    • 会阻塞 load 事件
    • DOMContentLoaded 事件前后执行
  1. defer 加载
  • 延迟执行引入的 JavaScript
  • 加载
    • 与渲染引擎并行顺序加载
    • 不会阻塞渲染引擎
    • 阻塞 DOMContentLoaded 事件
  • 执行
    • 不会阻塞渲染引擎
    • 当渲染引擎与 defer 加载都完成加载顺序执行

ECMAScript

ECMAScript是一种由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言

它往往被称为JavaScript或JScript

所以它可以理解为是 JavaScript 的一个标准

但 JScript 和 JavaScript 实际上是 ECMA-262 标准的实现和扩展

1995年Netscape公司发布的Netscape Navigator 2.0中,发布了与Sun联合开发的JavaScript 1.0并且大获成功, 并且随后的3.0版本中发布了JavaScript1.1,恰巧这时微软进军浏览器市场,IE 3.0搭载了一个JavaScript的克隆版-JScript, 再加上Cenvi的ScriptEase(也是一种客户端脚本语言),导致了三种不同版本的客户端脚本语言同时存在。为了建立语言的标准化,1997年JavaScript 1.1作为草案提交给欧洲计算机制造商协会(ECMA),第三十九技术委员会(TC39)被委派来“标准化一个通用的,跨平台的,中立于厂商的脚本语言的语法和语意标准”。最后在Netscape、Sun、微软、Borland等公司的参与下制订了ECMA-262,该标准定义了叫做ECMAScript的全新脚本语言。

ECMAScript实际上是一种脚本在语法和语义上的标准。实际上 JavaScript 是由 ECMAScriptDOMBOM 三者组成的

JScript

JScript 是由微软公司开发的活动脚本语言,是微软对 ECMAScript 规范的实现

JScript8.0 是 基于 ECMAScript 的一个新版本, 功能更加强大

JavaScript

JavaScript一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型

解析 JavaScript 的解释器就被称为 JavaScript 引擎

因 JavaScript 与其它的 JScript, 或者 ActionScript 更兼容 ECMA 标准,所以 JavaScript 也叫做 ECMAScript

JavaScript 组成部分分三种

  1. ECMAScript 描述语法与基本对象
  2. DOM 描述处理网页内容的方法和接口
  3. BOM 描述与浏览器进行交互的方法和接口

它最初由 Netscape 的 Brendan Eich 设计。JavaScript是甲骨文公司的注册商标

现在 JavaScript 已经被 Netscape 公司提交给 ECMA 制定为标准,称之为 ECMAScript,标准编号ECMA-262

符合ECMA-262 3rd Edition标准的实现有:

  1. Microsoft公司的JScript.
  2. Mozilla的JavaScript-C(C语言实现),现名SpiderMonkey
  3. Mozilla的Rhino(Java实现)
  4. Digital Mars公司的DMDScript
  5. Google公司的V8
  6. WebKit

子集与超集

大多数语言都会定义它们的子集,用以更安全地执行不信任的第三方代码。

Douglas Crockford曾写过一本很簿的书《JavaScript: The Good Parts》,专门介绍JavaScript中值得发扬光大的精华部分。

这个语言子集的目标是规避语言中的怪癖、缺陷部分,最终编程更轻松、程序更健壮。

子集的设计目的是能在一个容器或"沙箱"中更安全地运行不可信的第三方JavaScript代码。所有能破坏这个沙箱并影响全局执行环境的语言特性和API在这个安全子集中都是禁止的。
为了让JavaScript代码静态地通过安全检查,必须移除一些JavaScript特性:

· 禁止使用this关键字,因为函数(在非严格模式中)可能通过this访问全局对象。
· 禁止使用with语句,因为with语句增加了静态代码检查的难度。
· 静态分析可以有效地防止带有点(.)运算符的属性存取表达式去读写特殊属性。但我们无法对方括号([])内的字符串表达式做静态分析。基于这个原因,安全子集禁止使用方括号,除非括号内是一个数字或字符串直接量。
· eval()和Function()构造函数在任何子集里都是禁止使用的,因为它们可以执行任意代码,而且JavaScript无法对这些代码做静态分析。
· 禁止使用全局变量,因此代码中不能有对Window对象的引用和对Document的引用。
· 禁止使用某些属性和方法,以免在水箱中的代码拥有过多的权限。如:arguments对象的两个属性caller和callee、函数的call()和apply()方法、以及constructor和prototype两个属性。

而相对于当前 JavaScript 的超集来说,主要就是 TypeScriptCoffeeScript
TypeScrip 始于JavaScript,归于JavaScript

浏览器的 JavaScript 引擎就是为了解释 JavaScript 而存在

可以这样理解 JavaScript JSscript ActionScript 分别可以是 ECMAScript 的子集

既然浏览器的 JavaScript 引擎是运行 JavaScript 的地方

那么 Babel 就是将浏览器未实现的 ECMAScript 规范语法转化为浏览器可运行的低版本代码

同源策略

  1. URL 端口一样(IE除外)
  2. 域名一样
  3. 协议一样

about:blankjavascript: 会打开该 UTL 的文档源,因为这两个 URL 没有明确的包含有关原始服务器的信息

使用 document.domain 必须将父域和子域中设置相同的 document.domain

检测 Cross-Site Request Forgery (CSRF) 可以阻止跨源访问

HTTP访问控制(CORS)

CORS 跨域资源共享,使用额外的 HTTP 请求头来允许不同源服务器上指定的资源

XHR 和 Fetch 遵循同源策略

什么情况需要 CORS

  • 前文提到的由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求。
  • Web 字体 (CSS 中通过 @font-face 使用跨域字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨站调用。
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas
  • 样式表(使用 CSSOM)

跨域资源共享在 HTTP 头部声明一组字段,使其能够通过浏览器有权限访问哪些资源

如果 HTTP 请求会对服务器有副作用, 浏览器则必须先用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求,是否需要谁也会在该请求方式中得到响应

req 发送 origin 字段, res 响应 Access-Control-Allow-Origin,如果 origin 来源在 Access-Control-Allow-Origin 中则是达成 CORS,这也是简单请求完成的最简单的访问控制

简单请求

完全满足五个条件就是简单请求

  1. HTTP 请求方法是 GET | HEAD | POST
  2. 不得人为设置 CORS 安全的首部字段集合 之外的首部字段
    Accept,Accept-Language,Content-Language,Content-Type (需要注意额外的限制),DPR,Downlink,Save-Data,Viewport-Width,Width
  3. Content-Type text/palin | multipart/form-data | application/x-www-form-urlencoded
  4. XMLHttpRequestUpload 没有任何监听事件
  5. 请求不包括 ReadableStream 对象

预检请求

当 HTTP 请求不再是一次简单请求时(对服务器有副作用),也就是不满足简单请求的条件时,浏览器必须先完成预检请求
完全满足以下五个条件

  1. PUT | DELETE | CONNECT |OPTIONS | TRACE | PATCH
  2. 人为设置 CORS 安全的首部字段集合 之外的首部字段
    Accept,Accept-Language,Content-Language,Content-Type (需要注意额外的限制),DPR,Downlink,Save-Data,Viewport-Width,Width
  3. Content-Type 不属于 application/x-www-form-urlencoded | multipart/form-data | text/plain 其中之一
  4. XMLHttpRequestUpload 注册任意多个监听事件
  5. 使用了 ReadableStream 对象

预检请求与响应

# 请求
OPTIONS /resources/post-here/ HTTP/1.1 # 以 options 方式发送第一次请求
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST # 告知服务器实际请求时用 POST 方法
Access-Control-Request-Headers: X-PINGOTHER, Content-Type # 告知服务器实际请求将会携带两个自定义请求头部字段

# 响应


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS # 预检响应,服务器允许POST GET OPTIONS 
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type # 允许
Access-Control-Max-Age: 86400 # 该预检请求有效期 86400s
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

不再是简单请求时,浏览器这时会在实际请求前对服务器发送一次 OPTION 方式的请求来验证服务器是否允许该实际请求

实际请求与响应

# 请求
POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache

<?xml version="1.0"?><person><name>Arun</name></person> # 发送的非简单请求的内容
# content-type 为 xml
# 响应


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

预检重定向被 CORS 废弃

CORS req 字段

# 源站,不管是否跨域该字段都会被发送
Origin: <origin>
# 告知实际请求时使用的 HTTP 请求方法
Access-Control-Request-Method: <method>
# 告知实际请求时携带的头
Access-Control-Request-Headers: <field-name>[, <field-name>]*

CORS res 字段

Access-Control-Allow-Origin: <origin> | *
# 允许将访问的头放入白名单
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
# 指定预检请求缓存多长时间
Access-Control-Max-Age: <delta-seconds>
# 允许浏览器访问响应内容, 如果不设置浏览器将不会把响应内容返回给请求的发送者
Access-Control-Allow-Credentials: true
# 允许的 HTTP 请求方法
Access-Control-Allow-Methods: <method>[, <method>]*
# 允许的 HTTP 请求头
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!