一、浏览器进程与线程

1、进程(process)、线程(thread)

进程:cpu资源分配的最小单位(是拥有资源如代码、内存等能独立运行的最小单位) 线程:cpu调度的最小单位,进程里的执行上下文或执行序列

进程可理解为应用程序的执行程序,线程则是存在于其内部的某功能函数,执行进程程序的部分功能。

  • 进程可以通过操作系统,启动另一个进程来执行不同的任务。此时,系统将为新进程分配不同的内存。进程间可利用 IPC(Inter Process Communication)方式进行通信
  • 多个线程之间共享内存

2、浏览器进程管理

使用微软 Edge 浏览器打开一个页面,并使用浏览器任务管理器,得到如下5种类型进程:

Snipaste_2020-04-07_12-06-16.png

  • 浏览器进程: 主要负责界面显示、用户交互(前进/后退)、子进程管理,同时提供存储等功能
    • UI thread:绘制浏览器按钮和输入文本
    • Network thread:处理网络堆栈及从互联网接受数据
    • Storage thread:控制对文件的访问等等

browserprocesses.png

  • GPU进程:网页、UI 界面都选择采用 GPU 来3D 绘制
  • 网络进程:面向渲染进程、浏览器进程等提供网络下载功能
  • 渲染进程:渲染进程会显示多个图层,表示为每个选项卡运行多个渲染器进程。用于把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。为防恶意代码,渲染进程是运行在安全沙箱里的
  • 插件进程:每种类型的扩展对应一个进程

2.1 服务化

将浏览器程序的每个部分作为一项服务运行。在强性能硬件上运行时,将每个服务拆分为不同的进程,提供更高的稳定性。在弱性能硬件上运行时,将相应服务(UI、Network)作为线程整合到一个进程中,以节省内存占用

20191231101820617.gif

2.2 站点隔离

Tab 选项卡有一独立的渲染器进程,它允许跨站点 iframe 在单个渲染器进程中运行,不同站点之间共享内存空间。通过进程隔离,确保一个站点在未经同意的情况下无法访问其他站点的数据

20191231103708996.png

2.3 Chromium 默认进程模式

Chromium 提供了四种进程模式对 tab 进程做不同的处理,默认为Process-per-site-instance,同一个 site-instance (connected pages from the same site)使用一个进程。满足以下情况且打开的新旧页面属于同一 site

  • 用户通过 <a target="_blank"> 这种方式点击打开的新页面
  • JavaScript code 打开的新页面(比如 window.open)

2.4 简单导航

1036988-20191120173101203-1068546320.png

A、处理输入:Browser Process 的 UI thread 判断地址栏输入是搜索查询还是URL,解析并确定是将请求发送到搜索引擎,还是发送到待请求的网站。
16fac4952de0fcc6.png

B、开始导航:用户点击 “Enter”,UI 线程展示加载中 Spinner,Network thread得到请求后负责后续网络处理:

  • DNS lookup、establishing TLS Connection
  • 重定向:当其收到 HTTP 301 重定向时,会通知 UI 线程修改输入框的 URL ,启动另一个URL请求

16fac492f79d72f0.png

C、读取 response :接收响应体(payload)后,网络线程会在必要时查看数据流的前几个字节,获取响应报文头

  • 解析返回类型:根据 Content-Type(HTTP报文头) 和文件的 MIME type 来对不同返回做不同处理。如 HTML 交给 Renderer process,zip交给 Download manager

response.png

  • 安全检查:检查响应数据是否是来自 Safe Browsing(安全站点)的 HTML,若域或响应数据与已知的恶意网站相匹配,则网络线程会发出警告。此外还可能触发 CORB(Cross Origin Read block)检查,确保敏感的跨站点数据无法进入渲染器进程。CORB 发生在 subDownloads 阶段,不会发生在顶级导航中

微信图片_20200407164735.jpg

D、查找渲染器进程:UI 线程向网络线程发送请求时开始并行查找复用或启动渲染器进程。当网络线程开始接收数据前,网络线程通知 UI 线程数据准备就绪,UI 线程通知渲染器进程进行网页渲染。

微信图片_20200407171721.png

E、提交导航:IPC 将从浏览器进程发送一个数据流到指定渲染器进程。渲染器进程可以持续从数据流中接收 HTML 数据。渲染进程在开始接收 HTML 后,会返回确认信息,浏览器进程监听到后一次导航就算完成,进入文档加载阶段。此时地址栏安全锁、站点设置 UI 会显示新页面站点信息,选项卡的历史记录更新并被存储在磁盘上

16fac498dc9cbd70.png

F、初始加载完成:渲染进程结束且所有 onload 事件触发后,由 IPC 通知浏览器进程,tab 页 spinner 停止

16fac498df6d427c.png

2.5 导航到不同站点

新导航请求发起时,浏览器进程必须检查当前的渲染器进程是否处理 beforeunload事件。当你尝试新导航或关闭选项卡时,beforeunload 可触发显示“离开该网站吗?”弹窗以提示用户。不要无条件添加 beforeunload,它会产生更多的延迟,应该仅在需要时才监听此事件。

beforeunload.png

当新导航进行到与当前渲染的网站不同的网站时,会调用单独的渲染进程来处理新导航,同时保持当前渲染进程用于处理类似 unload 事件。

unload.png

二、渲染进程(浏览器内核)

1、渲染进程处理Web内容

渲染进程的核心工作是将 HTML,CSS 和 JavaScript 转换为用户可与之交互的网页。所有选项卡内发生的逻辑,都由渲染进程负责。

  • 主线程处理了服务器发送给用户的大部分代码;
  • 若使用到 Web Workder 或Service Worker,则 JavaScript 中这部分代码由工作线程处理;
  • Compositor(合成器)线程、Raster(光栅) 线程也在渲染进程内运行,从而实现高效、流畅的渲染页面。

renderer.png

  • GUI渲染线程:负责渲染浏览器界面,解析 HTML、CSS、构建 DOM 树和 RenderObject 树,布局和绘制等。可用于重绘(Repaint)、重排(Reflow)
  • JS引擎线程:js 内核,负责处理 javascript 脚本程序(V8 引擎)
  • 事件触发线程:用来控制事件循环,当对应的事件符合触发条件被触发时,事件线程会把事件添加到待处理事件队列的队尾,等待 js 引擎的处理
  • 定时触发器线程:负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval
    • 主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待 JS 引擎线程执行
  • 异步http请求线程:负责执行异步请求类函数的线程,如: Promise,axios,ajax
    • 主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待 JS 引擎线程执行。

注意:GUI 渲染线程和 JS 引擎线程是互斥的,后者执行时前者会被挂起;JS 执行时间过长,会造成页面渲染不连贯,导致页面渲染加载阻塞

2、渲染流程

2.1 构建 DOM 树

当渲染器进程收到一个导航请求,并开始接收 HTML 数据时,主线程使用 HTML 解析器将 HTML 元素解析成 DOM(Document Object Model)最终构建成 DOM 树。开发者工具 console 控制台输入 document 可查看

  • 子资源加载:逐个请求图片、CSS 和 JS 等外部资源时,使用预加载扫描,若有类似 <img><link> 的标签时,会由 HTML 解析器对该资源生成一个 Tokens,通过网络或本地缓存来加载,提供 loaderror 事件
  • 提示浏览器优雅加载资源:一般HTML 解析器遇到 <script> 标签时会暂停解析 HTML 文档,然后对这个 JS 脚本进行加载、解析和执行。可通过多种方式的配置,告知浏览器如何更优雅的加载资源:
    • 通过 JS 脚本使用到类似 document.write() 方法
    • script 标签中添加 asyncdefer 标记,可使浏览器异步加载和运行此 JS 脚本,不会阻断解析
    • 使用 JavaScript Modules,或通过 <link rel="preload"> 标签明确标记此为优先加载资源

dom.png

2.2 样式计算

主线程使用 CSS 解析器,解析外部及内联样式为 ComputedStyle( styleSheets->属性值标准化->继承、层叠),开发者工具 elements 的 Styles 项右栏的 Filter 下即是元素对应的 ComputedStyle

computedstyle.png

2.3 布局

主线程遍历 DOM 并计算样式,然后创建布局树(Layout Tree),布局树中包含 X、Y 坐标和边框大小等信息。布局树是一个与 DOM 树类似的结构,但它仅仅包含页面上可见内容相关的信息

  • 某元素被设置为 display:none,则该元素将不会出现在布局树中,但是它会出现在 DOM 树中;
  • 某元素被设置为 visibility:hidden ,则它会存在于布局树中;
  • p::before{content:"Hi!"} 这样的伪类,它会存在于布局树中,而不会存在于 DOM 树中

layout.png

2.4 分层

主线程遍历布局树以创建层树(Layer Tree),确定每个元素所在的图层。并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。开发者工具的 Layers 选项可查看

*单独图层

不同的图层渲染互不影响。对于某些频繁渲染的节点建议单独生成一个新图层,提高性能。但不能生成过多图层。

  • 拥有层叠上下文属性的元素会被提升为单独的新图层
    • 定位属性—— position: fixed
    • 透明属性——通过 CSS 动画或 Element.animate() 实现的 opacity 动画转换
    • 3D 变换——transform: translateZ(0); transform: translate3d(0,0,0)
    • will-change:若某元素是单独的图层(如侧滑菜单),则 CSS 中使用 will-change 属性提示浏览器
    • video、iframe、canvas等元素
    • 应用 animation/transition 给 opacity、transform、filter、backdropfilter
  • 需要被裁剪 Clip 的元素也会被创建为图层

layer.png

2.5 绘制

主线程遍历布局树生成绘制记录。绘制记录是绘制过程的注释,包含具体的绘制指令。开发者工具 Layers 选项选择 document 层的 Profiler,左栏即绘制记录,右栏为重现绘制过程耗时
paint.png

2.6 合成

光栅化 (rasterizing):将文档的结构,元素的样式,页面的形状和绘制顺序转换为屏幕上的像素。即图块->位图 合成(compositing):将页面的各个元素进行分层,分别光栅化,并在合成器线程中以一个单独的线程合成新页面的技术。如果页面发生滚动,由于图层已经光栅化,可通过移动图层同时合成新帧,以相同的方式实现动画

合成无需涉及主线程,无需等待样式计算或JavaScript执行。重排(回流)或重绘则必须涉及主线程

  • 创建光栅位图发送给GPU:主线程将创建的层树、绘制记录提交给合成器线程,合成器线程将图层 (layer) 分成各个图块 (tile) 发送到光栅线程。光栅线程将图块 GPU 栅格化为光栅位图,存储在 GPU 内存中

raster.png

  • 创建合成帧,发送到浏览器进程、GPU:元素光栅化后,合成器线程会收集绘制矩形(Draw Quads)信息,并创建一个合成帧(Compositor Frame),通过 IPC 将合成帧提交给浏览器进程。此时可从 UI 线程添加另一个合成帧用于浏览器的 UI 更新或从其他渲染器进程中添加扩展。这些合成帧被发送到 GPU 中,用以在屏幕上显示。如果触发滚动事件,合成器线程会创建另一个合成帧发送到 GPU

composit.png

*高成本更新渲染管道

1036988-20191125105606014-601035838.png
渲染管道(Rendering Pipeline)中最重要的任务是在每个步骤开始前,根据前一次操作的结果来创建新的数据。例如,如果布局树中的某些内容发生变动,则需要为文档中受影响的部分重新生成“绘制记录”

微信图片_20200407234104.gif

  • 浏览器必须在每一帧之间执行为元素设置的动画
    • 大多数显示器每秒刷新 60 次(60fps),若对每一帧都做了处理,那动画对人眼而言就是平滑的,否则若某些帧没有被处理到或者丢失,则会导致动画不连贯,页面卡顿
    • 即使渲染的计算可以跟上屏幕的刷新速度,可因为此计算是在主线程上执行的,这就意味着 JS 代码的执行,也可能导致它被阻断
  • 解决办法:将 JavaScript 操作划分成小块,并在每帧上执行requestAnimationFrame(),还可以将非dom操作的 JavaScript 运行任务在 Web Workers 中运行 ,以避免阻塞主线程。

raf.png

*Javascript 动画

JavaScript 动画可以处理 CSS 无法处理的事情。例如,沿着具有与 Bezier 曲线不同的时序函数的复杂路径移动,或者实现画布上的动画。基于 requestAnimationFrame 的结构化动画:

  1. function animate({timing, draw, duration}) {
  2. //返回值表示为从time origin之后到当前调用时经过的时间
  3. let start = performance.now();
  4. requestAnimationFrame(function animate(time) {
  5. // timeFraction 从 0 增加到 1
  6. let timeFraction = (time - start) / duration;
  7. if (timeFraction > 1) timeFraction = 1;
  8. // 计算当前动画状态
  9. let progress = timing(timeFraction);
  10. draw(progress); // 绘制
  11. if (timeFraction < 1) {
  12. requestAnimationFrame(animate);
  13. }
  14. });
  15. }
  • animate 函数接受 3 个描述动画的基本参数:

    • duration:动画总时间,比如 1000
    • timing(timeFraction):时序函数,类似 CSS 属性 transition-timing-function,获取从 0 到 1 的小数时间,返回动画进度,通常也是从 0 到 1。

      • 加速动画:progressn 次幂。如return Math.pow(timeFraction, 2)
      • 圆弧动画:return 1 - Math.sin(Math.acos(timeFraction));
      • 弹跳动画:球落下弹起,“bouncing”立即启动。它使用了几个特殊的系数

        1. function bounce(timeFraction) {
        2. for (let a = 0, b = 1, result; 1; a += b, b /= 2) {
        3. if (timeFraction >= (7 - 4 * a) / 11) {
        4. return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
        5. }
        6. }
        7. }
      • *ease:时序函数的直接常规应用称为“easeIn”

      • easeOut:动画结束时执行,timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction);
      • easeInOut:在动画的开头和结尾都显示效果
        1. if (timeFraction <= 0.5) { // 动画前半部分
        2. return timing(2 * timeFraction) / 2;
        3. } else { // 动画后半部分
        4. return (2 - timing(2 * (1 - timeFraction))) / 2;
        5. }
    • draw(progress):获取动画进度并绘制的函数。值 progress = 0 表示开始,progress = 1 表示结束

      3、页面生命周期

  • DOMContentLoaded:浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成。

    • 监听事件:document.addEventListener(“DOMContentLoaded”, ()=>{alert(“DOM ready!”)})
    • 脚本阻塞 DOMContentLoaded,以下除外
      • 具有 async 特性(attribute)的脚本
      • 使用 document.createElement('script') 动态生成并添加到网页的脚本
  • load:浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等
    • 监听事件:window.onload = function() { alert(‘Page loaded’);}
  • beforeunload:访问者触发了离开页面的导航(navigation)或试图关闭窗口,可以检查用户是否保存了更改,并询问他是否真的要离开。
    • 监听事件:window.onbeforeunload = function() {return false;};
  • unload:用户几乎已经离开但仍可启动一些操作,如发送统计分析数据,鼠标点击,滚动,被查看的页面区域等
    • 监听事件:window.addEventListener(“unload”,()=>{…}) ```javascript let analyticsData = { / 带有收集的数据的对象 / };

window.addEventListener(“unload”, function() { //使用 post 方法,数据大小限制在 64kb navigator.sendBeacon(“/analytics”, JSON.stringify(analyticsData)); //当 sendBeacon 请求完成时,浏览器可能已经离开了文档,所以就无法获取服务器响应 }; ```

  • readyStatedocument.readyState 属性可以为我们提供当前加载状态的信息
    • loading —— 文档正在被加载。
    • interactive —— 文档被全部读取。此时下一刻 DOMContentLoaded
    • complete —— 文档被全部读取,且所有资源(如图片等)都已加载完成。此时下一刻 window.onload

4、重排、重绘、直接合成

4.1 重排/回流(reflow)

引入更改迫使浏览器重新计算元素的布局位置或者几何形状,表现为重新布局。如通过改变 display 属性、文档插入新的元素、动画改变元素大小或位置
1036988-20191125105910560-1403915583.png

4.2 重绘(repaint)

引入更改仅影响绘制属性但不影响布局,表现为重新绘制及合成。如改变背景色、盒子阴影等属性

1036988-20191125110130912-1385069290.png

4.3 直接合成

渲染引擎可跳过布局和绘制,只在非主线程上合成,不占用主线程的资源,相对于重绘和重排,合成能提升绘制效率

1036988-20191125110252077-1470578389.png

4.4 减少重排、重绘

  • CSS
    • 动画使用 transform 替代 top、left
    • 使用 visibility 替换 display: none
    • 避免使用table布局,可能很小的一个小改动会造成整个 table 的重新布局
    • 避免CSS选择符层级太多。CSS 选择符从右往左匹配查找,尽量平级类名
    • 为频繁重绘或重排的节点设置图层:合成层位图交由 GPU 处理,阻止该节点的渲染行为影响别的节点
      • 定位属性—— position: fixed
      • 透明属性——通过 CSS 动画或 Element.animate() 实现的 opacity 动画转换
      • 3D 变换——transform: translateZ(0); transform: translate3d(0,0,0)
      • will-change:CSS 中使用 will-change 属性提示浏览器元素变化
      • video、iframe、canvas等元素
      • 应用 animation/transition 给 opacity、transform、filter、backdropfilter
    • 减少图层隐式合成:z-index 属性值设置大一些;调整文档中节点的先后顺序
    • 减少纯色图层物理尺寸:width\height属性值减少,再 scale 放大
  • JS
    • 避免逐项更改样式。一次性更改style属性,或者直接定义class属性
    • 避免频繁操作DOM。选择在documentFragment上操作,然后再插入document中
    • 避免循环读取offsetWidth等属性。循环外存取

三、Event Loop

为了协调事件、用户交互、脚本、渲染、网络等等,用户代理(浏览器或Node)必须使用事件循环。每个代理都有一个关联的事件循环,这对于该代理是唯一的。

  • 事件循环,是指浏览器或Node的一种解决js单线程运行时不会阻塞的一种机制。
  • JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

1、事件循环与浏览器渲染

1.1 同步任务、异步任务

浏览器工作原理💔 - 图29

    • 函数调用形成了一个由若干帧组成的栈
    • 对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语
  • 队列
    • 一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
    • 函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)
  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  • 异步任务:不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
  • 事件循环(Event Loop):指主线程会循环的从任务队列中读取任务,直到任务队列中的所有任务都被执行了才停止。

    1.2 宏任务、微任务

  • 宏任务:macrotask,也叫 task,任务队列( task queue)。

    事件循环有一个或多个任务队列,任务队列是一组任务的集合,而不是真的队列,因为事件循环处理模型的第一步是从选择的队列中抓取第一个可运行的任务,而不是让第一个任务出队列。

    • script(主代码块)
    • setTimeout、setInterval、setImmediate、MessageChannel
    • requestAnimationFrame(rAF)、UI Rendering、I/O
  • 微任务:microtask,也叫 jobs。

    当前宏任务执行后立即执行(渲染前)的任务,微任务队列(microtask queue)不是任务队列

    • Promise.then(Async/await)、MutationObserver(监听 DOM 修改事件)
    • Process.nextTick(Node 独有,优先级高于Promise.then)
  • 注意:
    • Promise对象当创建完之后会立即执行
    • Promise 遇到内部的resolve,将回调函数作为微任务,添加到microtask queue微任务队列

      1.3 流程

      《深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调》https://juejin.im/post/5ec73026f265da76da29cb25#heading-1

取出一个宏任务开始->执行栈执行->执行栈为空->微任务队列执行->微任务队列为空->是否渲染->完成

  1. 从任务队列中取出一个宏任务并执行,如果在执行过程中又产生了宏任务,那么这个任务将在下次事件循环中才能执行。
  2. 检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入新的微任务,也会在这一步一起执行
  3. 进入更新渲染阶段,判断是否需要渲染,这里有一个 rendering opportunity 的概念,也就是说不一定每一轮 event loop 都会对应一次浏览器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task 很可能在一次渲染之间执行)
    1. 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持 60fps(每 16.66ms 渲染一次)的话,那么浏览器就会选择 30fps 的更新速率,而不是偶尔丢帧。
    2. 如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低。
    3. 如果满足以下条件,也会跳过渲染:
      1. 浏览器判断更新渲染不会带来视觉上的改变。
      2. map of animation frame callbacks 为空,也就是帧动画回调为空,可以通过 requestAnimationFrame 来请求帧动画。
  4. 如果上述的判断决定本轮不需要渲染,那么下面的几步也不会继续运行
    1. 对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的 resize 方法。
    2. 对于需要渲染的文档,如果页面发生了滚动,执行 scroll 方法。
    3. 对于需要渲染的文档,执行帧动画回调,也就是 **requestAnimationFrame** 的回调。
    4. 对于需要渲染的文档, 执行 IntersectionObserver 的回调。
    5. 对于需要渲染的文档,重新渲染绘制用户界面。
    6. 判断 task queuemicroTask queue是否都为空,如果是的话,则进行 Idle 空闲周期的算法,判断是否要执行 **requestIdleCallback** 的回调函数。

1.4 动态过程

  • CallStack :函数被推进调用栈里直到被调用并返回一个值后,函数被推出调用栈
  • Web API:包括 DOM API、setTimeOut、HTTP requests等等,负责处理传递过去的回调函数,同时被调用函数被推出调用栈。

web.gif

  • QUEUE:定时器时间到,Web API 将回调函数放入宏任务队列中;微任务回调函数被放入微任务队列中

on.gif

  • EventLoop:若调用栈为空,则将队列中首项回调函数推入调用栈并执行(优先微任务),返回一个值后,推出调用栈

eve.gif
mq.gif

  • 关于 async/await 的微任务队列:async 函数依次执行直到遇到 await 关键字,而后被挂起放入 microtask queue,引擎跳出 async 函数并在全局执行上下文中继续执行代码

async.gif

2、事件流(事件传播)

  • 捕获阶段(capture phase):window往事件触发处传播,遇到注册的捕获事件触发
  • 目标阶段(target phase):传播到引发事件的嵌套最深的元素event.target
  • 冒泡阶段(bubbling phase):targetwindow传播,遇到注册的冒泡事件触发

3、注册事件

target.addEventListener(type, listener[, options|useCapture])

  • optionsobject,指定有关 listener 属性的可选参数对象
    • capture:Boolean,决定注册事件是捕获事件还是冒泡事件
    • once:Boolean,表示 listener 在添加之后最多只调用一次
    • passive:Boolean,设置为true时表示 listener 永远不会调用 preventDefault()。调用了客户端将会忽略它并抛出一个控制台警告
  • useCaptureBoolean,决定了注册的事件是捕获事件还是冒泡事件

4、事件委托

  • 定义:利用冒泡的原理把原本需绑定在子元素的响应事件委托给父类元素,触发执行效果
  • 作用:许多相似的元素在其共同的祖先上面添加一个处理器;单个处理器作为许多不同事件的入口点
  • 特点:
    • 省内存,效率高,减少事件注册
    • 新增子对象时无需再次对其绑定事件,适合动态添加元素
    • focus、blur之类事件无法委托,mousemove、mouseout不适合事件委托

四、跨域

1、什么是跨域

  • 什么是同源策略

同源策略是一种用于隔离潜在恶意文件的重要安全机制。同源指”协议+域名+端口”三者相同,即便两不同域名指向同一IP地址,也非同源 浏览器工作原理💔 - 图35

  • 同源策略限制内容(img、link、script等标签允许跨域)
    • Cookie、LocalStorage、IndexedDB等存储性内容
    • DOM节点
    • AJAX请求发送后,结果被浏览器拦截

——当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作”跨域”

2、跨域解决方案

2.1 JSONP

  • 原理:利用<script>标签无跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。
  • 特点:
    • 仅支持get方法,不安全可能会遭受XSS攻击
    • JSONP和AJAX均为客户端发送请求,从服务器端获取数据。但AJAX属于同源策略,JSONP属于跨域请求

2.2 CORS(Cross-Origin Resource Sharing)

  • 原理:浏览器向服务器发出Fetch/XHR请求,服务器使用额外的HTTP头部告诉浏览器让web应用进行跨域资源请求
  • 特点:
    • CORS要求服务端设置一些头部字段,最重要的就是 Access-Control-Allow-Origin
    • 生产环境中建议用成熟的开源中间件:如前端使用axios进行 http传输,后端以koa作为服务端框架,可使用CORS中间件 koa2-cors

2.3 Nginx

反向代理:代理后端服务器响应客户端请求的一个中介服务器,代理的对象是服务器

  • 原理:反向代理,即所有客户端的请求都必须先经过nginx的处理,nginx作为代理服务器再将请求转发给node或者java服务,规避了同源策略
  • 特点:
    • 保护和隐藏原始资源服务器
    • 负载均衡

2.4 Websocket

特点:

WebSocket:是一种网络传输协议,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的链接,并进行双向数据传输。

  • WebSocket复用了HTTP的握手通道:客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成之后,后续的数据交换则遵照WebSocket的协议
  • 数据传递:基于数据帧的传递
  • 封装好的接口:socket.io

五、存储

1、cookie

cookie:直接保存在浏览器上的小数据串,HTTP协议的一部分,基于域名。大多数情况cookies是由 web 服务器设置,并自动添加到相同域名下的每次请求中。主要用来保存登陆信息,保存用户登录状态

  • 访问cookies:使用document.cookie属性,name/value 必须编码
  • cookies选项:列在 key=value 后面,使用;间隔
    • path=/mypath:可访问到 cookie 的 url 路径前缀。必须是绝对路径
    • domain=site.com:默认 cookie 仅在当前域名下可见,如果明确设置了域名,可以让 cookie 在子域名下也可见
    • expires, max-age:设置 cookie 过期时间,若没有设置,则当浏览器关闭时 cookie 就失效了(session cookies)
    • secure:使 cookie 仅在 HTTPS 下有效
    • samesite:如果请求来自外部网站,禁止浏览器发送 cookie,这样有助于防止 CSRF 攻击
    • httpOnly:禁止任何 JavaScript 操作访问 cookie。我们使用 document.cookie 不能看到或操作 cookie
  • 数据存储大小:一个 cookie 最大 4kb,每个网站最多 20+ 个 cookies(取决于浏览器)

2、localStorage、sessionStorage

web storage:HTML5中专门为浏览器存储而提供的数据存储机制,不与服务端发生通信。localStorage 和 sessionStorage 允许我们在浏览器上保存键值对

  • 所有的 key 和 value 都必须是字符串
  • 数据绑定在同源下,不会随着每次请求发送到服务端。允许保存至少 2M 字节的数据,取决于浏览器也会有所不同
  • localStorage
    • 同源的数据在所有浏览器标签页和窗口之间共享
    • 数据不会过期。浏览器重启甚至系统重启后仍然保留
  • sessionStorage
    • 数据只存在于当前浏览器标签页
    • 数据在页面刷新后仍保留。关闭重新打开浏览器标签页后不会被保留
  • API(两个存储对象都提供相同的方法和属性)
    • setItem(key, value) – 存储键值对
    • getItem(key) – 根据键名获取值
    • removeItem(key) – 删除单个数据
    • clear() – 删除所有数据
    • key(index) – 获取该索引下的键名
    • length – 存储数据的长度
    • 使用 Object.keys 获取所有的键
    • 注意:使用对象属性的形式来访问键,则 storage 事件不会被触发
  • Storage 事件:
    • 在调用 setItem,removeItem,clear方法后触发
    • 所有能访问到存储对象的 window 对象上都会被触发
    • event.storageArea 会返回数据发生改变的存储对象

3、IndexedDB

定义:一个简单的键值对数据库,用于客户端存储大量结构化数据(包括文件和blobs)。它不仅可以存储字符串,还可以存储二进制数据。提供查找接口,还能建立索引,可看作运行在浏览器上的非关系型数据库

  • 键值对存储:IndexedDB 内部采用对象仓库(object store)存放数据
  • 异步:IndexedDB 操作时不会锁死浏览器,用户依然可进行其他操作(LocalStorage的操作是同步的)。异步设计是为防止大量数据的读写,拖慢网页
  • 同源限制:每一个数据库对应创建它的域名,网页只能访问自身域名下的数据库

4、Service worker

Service worker:浏览器在后台独立于网页运行的、用JavaScript编写的脚本。本质上充当 Web 应用程序、浏览器、网络(可用时)间的代理服务器。

  • 作用:
    • 实现离线应用
    • 数据 mock:与 Fetch 搭配,可以从浏览器层面拦截请求
    • 用于缓存静态资源:利用CacheStorage API来缓存js、css、字体、图片等
    • 实现消息的主动推送。与 Push 和 Notification 搭配
  • 特点:
    • 独立于JavaScript主线程,不能直接访问DOM,window对象,可访问navigator对象,也可通过消息传递的方式(postMessage)与JavaScript主线程进行通信
    • 一个网络代理,可以控制Web页面的所有网络请求
    • 具有自身的生命周期
    • 只能由HTTPS承载
    • 设计为完全异步,大量使用Promise,同步API不能在service worker中使用

从整体上来说,应用获取一个资源的缓存类型分为Service WorkerMemory CacheDisk CacheNo Cache。资源查找顺序为从左向右,找到资源则返回,未找到则继续寻找,直至最终获取资源。

浏览器工作原理💔 - 图36

六、参考资料

1、《图解浏览器的基本工作原理》
2、《一名【合格】前端工程师的自检清单》
3、《Inside look at modern web browser (part 1) 》
4、MDN:Promise.prototype.then()
5、JS中的Event Loop(事件循环)机制及Promise、async、await执行顺序