2023-06-28
浏览器
00
请注意,本文编写于 395 天前,最后修改于 194 天前,其中某些信息可能已经过时。

目录

常见面试题
一些计算机基础
并发、并行与串行
进程/线程/协程
死锁
chrome浏览器的多进程架构
单进程浏览器
多进程浏览器
面向服务的架构(未来版本)
浏览器中有哪些线程
浏览器内核
移动端Webview
IOS的webview
Android机的webview
EventLoop机制
关于JS的单线程
单线程如何执行任务?
如何处理高优先级的任务?-微任务
如何解决耽搁任务执行过久的问题?
总结
setTimeout
宏微任务
PWA
ServiceWorker
离线应用小案例
缓存实战(24年1月补充)

本文将以chrome为基准讲述浏览器的多进程架构、事件循环、移动端webview、浏览器渲染原理以及浏览器下的JS。

常见面试题

  • 浏览器有哪些进程?哪些线程?进程与线程协程的关系
  • 说一下浏览器的事件循环机制,做一些宏任务与微任务的题目
  • 说一下你对PWA的理解,ServiceWorker与WebWorker的异同

一些计算机基础

并发、并行与串行

并发

  • 只有一个CPU,不可能真正的同时运行一个以上的线程,它只能把CPU运行事件划分若干个时间片再将时间片分配给各个线程执行
  • 在同一个时间段的一些线程代码运行时,其他的线程处于挂起状态。这种方式称之为并发。

并行

  • 假如有两个线程和两个CPU, 一个线程在CPU-A上执行,另一个线程在CPU-B上执行,两个线程互不抢占CPU资源,可以同时进行,这种方式我们成为并行。

串行 指任务按顺序一个一个的执行,只有当前一个任务执行完成,才能执行下一个任务。

并发与并行的关系

  • 并发时逻辑上同时发生,并行更多侧重于物理上的同时发生。
  • 并发往往是指程序代码结构支持并发,并发的程序在多个CPU上运行起来才有可能达到并行,并行往往描述程序运行的状态。

其他:

  • 集群
    • 在同一台机器上同时处理多个任务或在多台机器上同时处理多个任务
  • 并发编程
    • 目标是充分利用处理器的每一个内核,以达到最高的处理性能。

进程/线程/协程

进程与线程的区别

  • 线程不是独立存在的,它由进程启动和管理的
  • 一个线程就是一个程序运行实例,进程中使用线程提升运行效率
  • 进程中任意一个线程执行出错,都会导致整个进程的崩溃
  • 进程是操作系统分配资源的最小单位,多个线程可以共享同一个进程中的资源
  • 当一个进程关闭之后没操作系统会回收进程中所占用的内存
  • 多个进程之间的资源相互隔离

协程与线程的区别

  • 协程是一种比线程更轻量级的存在,可以把写成看作跑在线程上的任务
  • 一个线程可以有多个协程单个线程上只能同时运行一个协程
  • 协程不是被操作系统内核所管理,而完全由程序所控制(也就是在运行台执行)。好处是不会像线程切换那样消耗资源

使用协程的示例

  1. 生成器函数
function* genDemo() { console.log(" 开始执行第一段 ") yield 'generator 2' console.log(" 开始执行第二段 ") yield 'generator 2' console.log(" 开始执行第三段 ") yield 'generator 2' console.log(" 执行结束 ") return 'generator 2' } console.log('main 0') let gen = genDemo() console.log(gen.next().value) console.log('main 1') console.log(gen.next().value) console.log('main 2') console.log(gen.next().value) console.log('main 3') console.log(gen.next().value) console.log('main 4')

image.png

  1. async函数也是协程
js
async function foo() { console.log(1) let a = await 100 console.log(a) console.log(2) } console.log(0) foo() console.log(3)

image.png 父协程: A协程启动B协程,那么A是B的父协程

死锁

所谓死锁是值多个进程在运行过程中因争抢资源而造成的一种僵局,当线程处于这种僵持的状态时,无外力的作用,他们将无法再向前推进。系统中的资源可以分为两类

  1. 可剥夺资源,是指某进程在获得这类资源后,该资源可以被其他进程或系统剥夺,CPU和主存均是可剥夺性资源;
  2. 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完之后自动释放,如磁带、打印机等。

产生死锁的原因:

  1. 竞争不可剥夺资源(如打印机),或竞争临时资源(硬件中断、信号、消息、缓冲区内的消息等)
  2. 进程推进顺序非法,P1进程占用了R1资源、P2进程占用了R2资源,这用无论P1请求R2资源还是P2请求R1资源都会造成死锁。

产生死锁的必要条件

  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
  4. 环路等待条件:在发生死锁时,必然存在一个进程——资源的环形链

预防死锁的方法:

  1. 资源一次性分配:一次性分配所有资源,这样就不会再有请求了(破坏请求条件)
  2. 先释放已占有的资源才能请求其他资源
  3. 如果申请的资源得不到那么释放已占有的资源(破坏不可剥夺条件)
  4. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

chrome浏览器的多进程架构

先贴一张图,大致了解一下浏览器的组成 image.png

下面将从浏览器的发展历程捋一捋,了解浏览器架构为什么这么设计

单进程浏览器

单进程是指浏览器内所有功能和模块(网络、插件、JS运行环境、渲染引擎和页面)都是运行在一个进程中

产生的问题: 不稳定 不安全 不流畅

  • 不稳定
    • 一个插件意外崩溃导致整个浏览器的崩溃
    • 渲染引擎模块不稳定,JS导致渲染引擎崩溃
  • 不流畅
    • 因为所有的模块都运行在同一进程中
    • 内存泄漏也会导致单进程浏览器变慢
  • 不安全
    • 恶意插件
    • web漏洞

多进程浏览器

image.png 那么多进程浏览器是如何解决上述问题的?

  • 不稳定
    • 一个插件一个进程,利用进程间的隔离环境
    • 多个渲染进程(一般一个页面一个渲染进程)
  • 不流畅
    • JS运行在渲染进程中,即使JS阻塞,阻塞的只有当前页面的渲染进程,一个tab页面一个进程
  • 不安全
    • 内存泄漏的解决:关闭一个页面时整个渲染进程也被关闭
    • 多进程的好处是提供了一个安全沙箱环境

目前的多进程架构

image.png

image.png

关于渲染进程,也可能会出现多个页面共享同一个渲染进程中

  • 虽然chrome的默认策略是一个标签页对应一个渲染进程,但如果同一个页面打开另一个页面,而且新页面与当前页面属于同一站点的话,新页面会复用父页面的渲染进程,官方把这个策略叫做process-per-site-instance。
  • 同一站点 根域名+协议+端口

面向服务的架构(未来版本)

虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,同样也带来了一些问题

  • 更高的资源占用 每个进程都会包含公共基础结构的副本
  • 更复杂的体系架构 耦合性高,扩展性差

chrome团队一直在寻找一种弹性方案,既可以解决资源占用高的问题,也可以解决复杂体系架构的问题----》面向服务的架构

chrome整体架构会朝向现在操作系统所采用的“面向服务架构”方向发展,原来的各模块会重构成独立的服务,每个服务都会在独立的进程中运行,访问服务必须使用定义好的接口,通过IPC来通信,从而构建一个更内聚、松耦合易于维护和扩展的系统

浏览器中有哪些线程

浏览器有哪些进程.png

浏览器内核

浏览器渲染引擎(浏览器内核)有两个最主要的部分 渲染引擎和JS引擎 它决定了如何现实网页内容及网页格式的信息

主流浏览器的引擎列表

浏览器渲染内核JS引擎
IE/EdgeTrident(<10) ; EdgeHTMLJScript(<IE9);
Chakra(IE9+及Edge)
SafariWebkit/Webkit2JSCore/Nitro(4+)
ChromeChromium(WebKit);
Blink
V8
FireFoxGeckoSpiderMonkey(<3.0);
TraceMonkey(<3/6);
JaegerMonkey(4.0+)
OperaPresto;
Blink
Futhark(9.5~10.2)
CaraKan(10.5)

补充知识点:

  • Blink其实是Webkit的一个分支,二次开发
  • Webkit的鼻祖其实是Safari

移动端Webview

IOS的webview

  • iOS的webview有uiwebview和wkwebview两种
  • 从iOS8开始 Apple公司推出了wkwebview,Safari默认使用wkwebview

uiwebview 目前已经推出历史舞台

2020年4月起App Store将不再接受使用UIWebView的新App上架、2020年12月起将不再接受使用UIWebView的App更新。

wkwebview的问题

  • 不支持js原生加密
  • 在联网及本地文件读取等有各种跨域限制
  • wk第一次渲染速度略慢于uiwebview;

wkwebview的好处

  • 节省内存、
  • 滚动时懒加载的图片也可以实时渲染,而uiwebview在滚动停止后懒加载的图片才能显示(滚动前就加载图片不受影响)
  • wkwebview的video播放支持AirPlay

Android机的webview

android系统的webview分系统webview和x5两种 android系统的webview即 google的 Android system webview,它自带手机rom中,所有依赖系统webview的应用都调用这个webview

  • 在android 4.4以前 webview使用的是 webkit内核
  • 4.4起,变成了chromiun内核对应chrome30
  • 5.0起, webview脱离rom,可单独更新,伴随着chrome发版,google会在google play store上同步更新
    • 由于 google play store国内访问不了,国内用户可以通过华为应用市场的的镜像下载,
    • 也有个别国产rom改坏了这块规则,使得自身的system webview无法更新

如何查看android机webview系统版本?

  • 在日志里查看ua
  • 系统设置 >> 应用管理 >> 应用列表 >> 点击右上角两个点 >> 显示系统程序 >> 搜索 Android System Webview

手机默认浏览器和webview的区别

  • 国外品牌的android手机,自带浏览器就是chrome。而国内手机自带浏览器大多厂商自研的浏览器,并不等于Android System Webview

Android机app内嵌h5更换 Webview方案

  • 引导用户安装最新版的 Android System Webview,应用宝、华为应用市场、小米应用市场均可以下载这个apk, 可以设置wifi下自动更新
  • 打包时直接集成新版Android system webview (该包>50M, 不建议)
  • 使用腾讯的x5内核

EventLoop机制

image.png 本段将以JS的发展讲起浏览器事件循环机制为什么这样设计。

关于JS的单线程

首先JS诞生之处 是为了一个需求:解决表单校验问题, 那个年代网络很慢, 为了避免用户表单时,因后端校验失败,而反复等待网路的问题,想通过脚本在前端校验通过在提交表单,JS的作者把js设计的非常简单(单线程、没有块级作用域)。

Q:为什要设计成单线程呢? A:简单!如果设计成多线程,那么就想后端操作数据库一样,需要引入锁和事物等机制。

单线程带来的问题就是JS阻塞DOM渲染,DOM解析渲染阻塞JS。

单线程如何执行任务?

有三种方案

  1. 第一版划分任务依次执行。

image.png 缺点: 并不是所有的任务都是在执行前统一安排好的,大部分情况下,新的任务是在线程运行过程中产生的。

  1. 第二版事件循环机制
  • 引入循环机制, 在线程语句最后加一个for 循环语句,线程会一直执行
  • 引入了事件,在线程执行过程中等待用户输入,等待过程线程处于暂停状态,一旦接收用户输入信息,线程被激活,然后执行相加运算,最后输出结果

image.png

虽然引入了事件循环,可以在执行过程中接受新的任务。但是所有的任务都是来自线程内部的。如果另外一个线程(比如IO线程)想让主线程执行一个任务,利用第二版线程模型是无法做到的。

image.png

  1. 队列+事件循环 消息队列

image.png

渲染进程专门有一个IO线程用来接收其他进程传进来的消息,接收到消息后会将这些消息组装成任务发送给渲染主进程。

消息队列中有哪些任务类型呢?

  • 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript定时器等等。
  • 除此之外,消息队列中还包含有很多与页面相关的事件,如JavaScript执行、解析DOM、样式计算、布局计算 、CSS动画等。

如何处理高优先级的任务?-微任务

比如监听DOM节点变化(增删改),然后根据这些变化来处理响应的业务逻辑。如果采用同步通知的方式,会影响到当前任务的执行效率,如果采用异步方式会影响到监控的实时性 通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含一个微任务队列,在执行宏任务的过程中,如果DOM有变化,就会将该变化添加到微任务队列中,这样就不会影响宏任务继续执行,因而解决了执行效率的问题。 等到宏任务中的主要任务都执行完成之后,这时候渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务因为DOM变化都保存在微任务中,这样也就解决了实时性问题

如何解决耽搁任务执行过久的问题?

image.png JavaScript可以通过回调功能来规避这种问题,也就是让要执行的JavaScript滞后执行

总结

eventLoop.png

setTimeout

一般情况下浏览器接受到消息(DOM解析、JS执行、触发垃圾回收、窗口改变)会将事件添加到消息队列中,事件循环系统就会按照消息队列中的顺序来执行事件。

但是定时器这个比较特别,不能直接添加到消息队列中。其实浏览器中还有一个延时队列,存储了包括定时器及chromium内部一些需要延时执行的任务。

具体实现是 有一个ProcessDelayTask函数(专门用来处理延时执行任务函数的),处理完消息队列中的任务就会执行该函数,该函数会根据发起时间和延迟时间计算出要到期的任务,然后一次执行这些到期的任务。执行完成后再继续下一个循环过程。

设置一个定时器,JavaScript 引擎会返回一个定时器的 ID,在没有执行前可以通过clearTimeout函数取消该定时器

由此,setTimeout会有一些问题

  1. 如果当前任务执行事件过久,会影响延迟到期定时器的执行
  2. 如果setTimeout存在嵌套调用,那么系统会设置最短时间间隔为4ms Chromium 实现 4 毫秒延迟的代码
  3. 处在后台的页面setTimeout执行最小间隔是1000ms
  4. 延迟执行时间有最大值 约24.8天
  5. setTimout导致了this隐式调用踩坑
    1. setTimeout(MyObj.showName.bind(MyObj), 1000)

宏微任务

简单总结:

  • 宏微任务是浏览器事件循环的机制,浏览器事件循环机制的核心是 消息队列+循环, - 消息队列中存放的是宏任务, 每个宏任务中都有一个微任务队列。
  • 只有执行完当前宏任务(包括该宏任务中的微任务队列)才能执行下一个宏任务
  • 如果执行完宏任务收到垂直同步信号,会去渲染一帧后再执行下一个宏任务
    • 收到垂直同步信号: 当前宏任务 --> requestAnimationFrame --> 渲染一帧(重排、重绘、合成)--> 下一个宏任务
    • 未收到垂直同步信号: 当前宏任务 --> 下一个宏任务

宏任务:

#浏览器Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame
UI交互事件
postMessage

补充说明

  • 有些地方会列出来UI Rendering,说这个也是宏任务,可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤
  • requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行
  • I/O这一项感觉有点儿笼统,有太多的东西都可以称之为I/O,点击一次button,上传一个文件,与程序产生交互的这些都可以称之为I/O。

微任务

#浏览器Node
process.nextTick
MutationObserver
Promise.then catch finally
await 后面
Object.observe
  • 几个执行顺序的小案例 这篇文章有一些案例, 可以测一下你宏微任务掌握的程度,(tip: 有点难度建议画图做标记)

PWA

  • PWA全称Progressive Web App,翻译过来就是 渐进式 + Web应用
  • 它是一套理念,渐进式增强Web的优势,并通过技术渐进式缩短和本地应用或者小程序的距离。

思考: 相对于APP,web缺少哪些能力?

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

针对上述问题 PWA提出 了两种解决方案

  1. 通过ServiceWorker来试着解决离线存储和消息推送的问题

早期有个AppCache的方案,因暴露问题比较多&多方吐槽,最后被废弃了

  1. 通过引入manifest.json 来解决一级入口的问题

ServiceWorker

  • 在页面和网页之间增加一个拦截器,用来缓存和拦截请求

Service Worker来自于Web Worker的一个核心思想。(Web Worker是在渲染进程中在开辟一个新线程它的生命周期是和页面关联的)。区别有以下两点

  1. Web Worker是临时的,每次JS执行完成会退出其执行结果不能保存,ServiceWorker在Web Worker的基础上增加了存储功能
  2. Service Worker 还需要为多个页面提供服务,所以不能把ServiceWorker和单个页面绑定起来。在目前的 Chrome 架构中,Service Worker 是运行在浏览 器进程中的,因为浏览器进程生命周期是最长的,所以在浏览器的生命周期内,能够为所有的页面提供服务。

离线推送 消息推送也是通过Service Worker来实现的。因为消息推送过程中,浏览器页面或许并没有启动,这是就需要ServiceWorker来接收服务器推送的结果,并将消息一定程度展示给用户

安全

为避免HTTP明文传输存在的被窃听、篡改和劫持的风险,所以Service Worker必须采用https协议或者运行或在localhost域名下运行

除此之外 ServiceWoker 还需要同时支持Web页面默认的安全策略、存储同源策略、内容安全策略等

Service Worker生命周期

image.png

特点:

  • 不能操作DOM
  • 可以通过postMessage接口把数据传递给其他JS文件
  • ServiceWorker与JS主线程互不阻塞(JS主线程在渲染进程里,而ServiceWorker是浏览器进程中的线程)
  • 只能运行在https或localhost环境下

用途

  1. 静态资源缓存及离线应用
  2. 性能优化,将一些计算移至ServiceWorker
  3. 翻墙
  4. 浏览器多tab页面通信

离线应用小案例

  1. 如果配置了manifest.json 导航栏 会出现 “安装”
  1. 安装后如果是mac会创建应用图标

并打开

image.png

  1. 如果这时候 卸载浏览器SW & 关闭网络 再次访问
  1. 可以通过第二步创建的应用 右上角菜单 卸载应用

完整案例点这里

缓存实战(24年1月补充)

除了http浏览器缓存外,还有Server Worker缓存,但是SW缓存如果使用不当会造成永久无法更新问题, 只能更换域名 重定向了,下面是一个示例

js
const CACHE_NAME = 'project-xx-cache' self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(function (cache) { return cache.addAll([ // ... 要缓存资源 ]); }) ); }); self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request) .then(function (response) { if (response) { return response; } return fetch(event.request).then(function (response) { if (!response || response.status !== 200) { return response; } // 缓存ajax const toCache = response.clone(); caches.open(CACHE_NAME).then(function (cache) { cache.put(event.request, toCache); }); return response; }); } ) ); });

如果你的ServerWorker.js没有被浏览器或代理服务器强缓存,那么还有补救错误,代价是需要刷新浏览器两次。

用户第一次刷新:等待页面加载完成,新的serverworker要去卸载老的servicework及缓存清理,但由于页面已经加载完成这时候老的缓存依旧有效
用户第二次刷新:启动新的serviceworker 这是才看到页面更新,下面是补救代码

js
const CACHE_NAME = 'project-xx-cache' self.addEventListener("install", (event) => { event.waitUntil( caches.open(CACHE_NAME).then(function (cache) { return cache.addAll([ // 你要缓存的资源 ]); }), ); self.skipWaiting(); // 1. 立即接管 }); // 2. 激活Service Worker并清理旧缓存 self.addEventListener("activate", (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cache) => { if (cache !== PANDORA_NEXT_CACHE_NAME) { console.log("Service Worker: Clearing Old Cache"); return caches.delete(cache); } }), ); }), ); self.clients.claim(); // 3. 接管旧的Service Worker控制的客户端 }); self.addEventListener("fetch", function (event) { // 如果需要你可以在这里限制那些资源不走sw缓存 // if () { // return event.respondWith(fetch(event.request)); // } event.respondWith( caches.match(event.request).then(function (response) { if (response) { return response; } return fetch(event.request).then(function (response) { if (!response || response.status !== 200) { return response; } const toCache = response.clone(); const cacheControl = response.headers.get("cache-control"); // 排除 no-cache不使用强缓存 no-store 不缓存 的情况 if ( cacheControl && !cacheControl.includes("no-cache") && !cacheControl.includes("no-store") ) { caches.open(CACHE_NAME).then(function (cache) { cache.put(event.request, toCache); }); } return response; }); }), ); });

如果你一上来用的就是这个代码, 那么serverworker就完美实现了缓存,也不用担心更新问题了。

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!