目录
1. 如何理解 JavaScript 是单线程的
2. 进程与线程
2.1 在浏览器中,如何理解进程和线程的关系?
2.2 浏览器的五种进程
2.2.1 浏览器主进程(Browser 进程)
2.2.2 浏览器渲染进程(Renderer 进程)
2.2.3 GPU 进程
2.2.4 第三方插件进程
2.2.5 网络进程
2.3 浏览器渲染进程(Renderer 进程)包含的五种线程
2.3.1 GUI 渲染线程
2.3.2 JavaScript 引擎线程
2.3.3 事件触发线程
2.3.4 定时器触发线程
2.3.5 异步http请求线程
2.4 Chrome 打开一个页面需要启动多少进程?
3. script 标签上的 async、defer
3.1 为什么要使用 async、defer
3.2 async、defer 的区别
3.2.1 执行顺序的区别
3.2.2 使用场景的区别
3.2.3 window.onload 执行顺序的区别
4. DOM 事件流(事件执行顺序)
4.1 事件流的基本概念
4.2 事件委托(利用事件冒泡的原理)
4.3 addEventListener 的第三个参数
4.4 事件流的执行顺序(先捕获再冒泡)
5. 浏览器空闲时间
5.1 浏览器一帧内做了什么
5.2 requestIdleCallback
6. 浏览器缓存
6.1 协商缓存(Etag、Last-Modified)
6.1.1 协商缓存基本过程
6.1.2 Etag VS Last-Modified
6.2 强制缓存(Expires、Cache-Control)
6.2.1 Cache-Control VS Expires
6.2.2 Cache-Control: no-cache VS Cache-Control: no-store
7. 垃圾回收机制
7.1 GC 垃圾回收策略
7.1.1 标记清除
7.1.2 引用计数
7.2 分代式垃圾回收机制
7.2.1 新生代的垃圾回收方式
7.2.2 老生代的垃圾回收方式
7.2.3 新老生代在不同系统的内存大小
7.3 垃圾回收机制参考文章
1. 如何理解 JavaScript 是单线程的
JavaScript 是单线程的:
- 只是说 JavaScript 的执行是单线程的
- 但 JavaScript 的宿主环境(Node 或者 浏览器),都是多线程的
JavaScript 设计为单线程的原因:
- 如果有多个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时就存在冲突
- 为了避免冲突,JavaScript 就设计成了单线程的
2. 进程与线程
浏览器的线程和进程 - 简书前言 学习真的是一个很奇妙的过程。本意是学习状态管理工具Redux,其中涉及到Promise异步编程知识,发现不太熟悉,于是决定先学学Promise相关知识。Promise文...https://www.jianshu.com/p/c1808d0c1d45
2.1 在浏览器中,如何理解进程和线程的关系?
以 Chrome 浏览器为例:
- 当打开了一个页面时,其实就创建了一个进程
- 一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等
- 当发起一个请求时,其实就创建了一个线程;当请求结束时,该线程就会被销毁
- 进程是 CPU 资源分配的最小单位;线程是 CPU 调度的最小单位
为什么浏览器是多进程的?
- 对于简单的网页,一个进程处理多个网页是可行的;但是如果把 多个 复杂的网页放进一个进程,一个 Tab 页崩溃,将导致同进程的其他页面崩溃,极其影响用户体验
- 进程之间 不共享 资源和地址空间,所以使用多个进程,可以避免更多的安全问题;多进程相对于单进程,内存等资源消耗更大,有点空间换时间的意思;这大概也是浏览器中存在多个 Tab 页时共用一个进程的原因吧
2.2 浏览器的五种进程
打开 Google 中的任务管理器(Shift + Esc 或者 Chrome 更多工具 → 任务管理器)
可以看到展现了四种进程:
- 浏览器主进程
- GPU 进程
- 第三方插件进程
- 浏览器渲染进程
2.2.1 浏览器主进程(Browser 进程)
只有一个主进程,主要负责:
- 界面显示
- 用户交互(前进后退)
- 子进程管理(创建/销毁其他进程)
- 将 Renderer 进程得到的 内存中的 Bitmap,绘制到用户界面上
- 提供存储、下载、网络资源管理等
2.2.2 浏览器渲染进程(Renderer 进程)
就是通常所说的浏览器内核,排版引擎 Blink 和 JavaScript 引擎 V8 都运行在该进程中
核心任务:将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页(实现 —— 页面渲染、脚本执行、事件处理等)
出于安全考虑,渲染进程都是运行在沙箱模式下
默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程,互不影响;存在多个标签页时,会自动合并进程
2.2.3 GPU 进程
GPU 的使用初衷是为了实现 3D CSS 的效果
只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制
GPU 进程可以禁用,只有一个 GPU 进程
2.2.4 第三方插件进程
主要是负责插件的运行,每种类型的插件,对应一个进程
因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响
2.2.5 网络进程
主要负责页面的网络资源加载
之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程
2.3 浏览器渲染进程(Renderer 进程)包含的五种线程
前面说过,Renderer 进程的核心任务:将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页(实现 —— 页面渲染、脚本执行、事件处理等)
2.3.1 GUI 渲染线程
主要负责页面的渲染,解析 HTML、CSS,构建 DOM 树,布局和绘制等
当界面需要重绘(Repaint)或 由于某种操作 引发回流(reflow)时,该线程就会执行
2.3.2 JavaScript 引擎线程
该线程主要负责处理 JavaScript 脚本,执行代码
一个 Tab 页(Renderer进程)中,无论什么时候,都只有一个 JavaScript 线程在运行 JavaScript 程序
该线程与 GUI 渲染线程互斥,当 JavaScript 引擎线程执行 JavaScript 脚本时间过长,将导致页面渲染的阻塞
2.3.3 事件触发线程
用来控制事件循环,将准备好的事件交给 JavaScript 引擎线程执行
事件触发线程不属于 JavaScript 引擎线程,而是属于浏览器(JavaScript 引擎线程自己忙不过来,需要浏览器再开一个线程协助他)
比如 setTimeout 定时器计数结束, ajax 等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件,依次加入到任务队列的队尾,等待 JavaScript 引擎线程的执行
2.3.4 定时器触发线程
负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval
浏览器定时计数器,并不是由 JavaScript 引擎计数的,因为 JavaScript 引擎是单线程的,如果处于阻塞线程状态,就会影响记计时的准确性
2.3.5 异步http请求线程
负责执行异步请求一类的函数的线程,如: Promise,axios,ajax 等
2.4 Chrome 打开一个页面需要启动多少进程?
浏览器从关闭状态进行启动,然后新开 1 个页面,至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个进程;
后续再新开标签页,浏览器、网络进程、GPU进程是共享的,不会重新启动;
如果2个页面属于同一站点的话,并且从a页面中打开的b页面,那么他们也会共用一个渲染进程,否则新开一个渲染进程
—— 《浏览器工作原理与实践》
最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程。
浏览器相关原理(面试题)详细总结一 - 掘金1. Chrome 打开一个页面需要启动多少进程?分别有哪些进程? 浏览器从关闭状态进行启动,然后新开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个进程;后续再新开标签页,浏览器、网络进程、GPU进程是共享的,不会重…https://juejin.cn/post/6844903962216824839#heading-0
3. script 标签上的 async、defer
3.1 为什么要使用 async、defer
先来谈一下浏览器的渲染机制:浏览器有 GUI 渲染线程与 JavaScript 引擎线程,这两个线程是互斥的关系
JavaScrip t的加载、解析与执行会阻塞 DOM 的构建。也就是说,在构建 DOM 时,HTML 解析器若遇到了JavaScript,那么它会暂停构建 DOM,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复DOM构建
综上所述,直接使用 script 标签,会阻塞 DOM 渲染;但如果使用带有 async 和 defer 的 script 标签,就会异步请求这些资源,不会阻塞页面渲染
浏览器渲染过程分为:构建DOM -> 构建CSSOM -> 构建渲染树 -> layout布局 -> 绘制
3.2 async、defer 的区别
记忆要点:
- defer 有顺序,async 没顺序
- defer 在 window.onload 之前,async 都有可能
浅谈script标签中的async和defer - 贾顺名 - 博客园script标签用于加载脚本与执行脚本,在前端开发中可以说是非常重要的标签了。直接使用script脚本的话,html会按照顺序来加载并执行脚本,在脚本加载&执行的过程中,会阻塞后续的DOM渲染https://www.cnblogs.com/jiasm/p/7683930.html
3.2.1 执行顺序的区别
async 是无顺序的加载,而 defer 是有顺序的加载
async 的执行,并不会按照 script 在页面中的顺序来执行,而是谁先加载完谁执行
defer 的执行,则会按照引入的顺序执行,即便是后面的 script 资源先返回
3.2.2 使用场景的区别
defer 可以用来控制 JavaScript 文件的加载顺序;比如 jqery 和 Bootstrap,因为 Bootstrap 中的 JavaScript 插件依赖于 jqery,所以必须先引入jquery,再引入 Bootstrap 文件
如果你的脚本并不关心页面中的 DOM 元素(文档是否解析完毕),并且也不会产生其他脚本需要的数据,使用 async, 如统计、埋点等功能
3.2.3 window.onload 执行顺序的区别
使用 defer 的 script 标签,会在 window.onload 事件之前被执行
使用 async 的 script 标签,对 window.onload 事件没有影响,window.onload 可以在之前或之后执行
4. DOM 事件流(事件执行顺序)
4.1 事件流的基本概念
DOM 同时支持两种事件模型:
- 捕获型事件流
- 冒泡型事件流
DOM 事件流的三个阶段:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
DOM 事件捕获 的具体流程:window➡️document➡️html➡️body➡️目标元素;
DOM 事件冒泡 的具体流程:目标元素➡️body➡️html➡️document➡️window;
4.2 事件委托(利用事件冒泡的原理)
事件委托的原理:
- 当 一组元素 要添加相同的事件时,可以在父元素上绑定一个事件,利用事件冒泡,达到父元素代理子元素事件
- 点击子元素,通过 e.target || e.srcElement 可以获取点击的具体子元素
事件委托优点:可以减少事件的注册,节省内存;也可以实现新增对象时,无需再次绑定事件
4.3 addEventListener 的第三个参数
第三个参数默认是 false,表示在 事件冒泡 阶段调用;
第三个参数值为 true 时,表示在 事件捕获 阶段调用;
4.4 事件流的执行顺序(先捕获再冒泡)
// 鼠标点击子元素后,打印顺序为
// 父捕获
// 子捕获
// 子冒泡
// 父冒泡
<html>
<div class="parent">
<div class="child">子元素</div>
</div>
<script>
let parentDom = document.querySelector('.parent');
parentDom.addEventListener('click', function () {console.log('父捕获'); }, true)
parentDom.addEventListener('click', function () {console.log('父冒泡');}, false)
let childDom = document.querySelector('.child')
childDom.addEventListener('click', function () {console.log('子捕获');}, true)
childDom.addEventListener('click', function () {console.log('子冒泡');}, false)
</script>
</html>
5. 浏览器空闲时间
页面是一帧一帧绘制出来的,一般情况下,设备的屏幕刷新率为 1s 60次,而当 FPS 小于 60 时,会出现一定程度的卡顿现象
5.1 浏览器一帧内做了什么
首先需要处理输入事件,能够让用户得到最早的反馈
接下来是处理定时器,需要检查定时器是否到时间,并执行对应的回调
接下来处理 Begin Frame(开始帧),即每一帧的事件,包括 window.resize、scroll、media、query、change 等
接下来执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调
紧接着进行 Layout 操作,包括计算布局和更新布局,即这个元素的样式是怎样的,它应该在页面如何展示
接着进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充
到这时以上的六个阶段都已经完成了,接下来处于空闲阶段(Idle Peroid)
5.2 requestIdleCallback
在空闲阶段(Idle Peroid)时,可以执行 requestIdleCallback 里注册的任务
requestIdleCallback 接受了两个参数:
window.requestIdleCallback(callback, { timeout: 1000 })
1)第一个参数是一个函数,该函数的入参,可以获取:
- 当前帧的剩余时间
- 该任务是否超时
window.requestIdleCallback(deadline => {
// 返回当前帧还剩多少时间供用户使用
deadline.timeRamining;
// 返回 callback 任务是否超时
deadline.didTimeout;
});
2)第二个参数,传入 timeout 参数自定义超时时间,如果到了超时时间,浏览器必须立即执行
// 该函数的执行时间超过1s
function calc() {
let start = performance.now();
let sum = 0;
for (let i = 0; i < 10000; i++) {
for (let i = 0; i < 10000; i++) {
sum += Math.random();
}
}
let end = performance.now();
let totolTime = end - start;
// 得到该函数的计算用时
console.log(totolTime, "totolTime");
}
let tasks = [
() => {
calc();
console.log(1);
},
() => {
calc();
console.log(2);
},
() => {
console.log(3);
}
];
let work = deadline => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);
// 如果此帧剩余时间大于0,或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
// 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0
) {
let fn = tasks.shift();
fn();
}
// 如果还有未完成的任务,继续调用 requestIdleCallback 申请下一个时间片
if (tasks.length > 0) {
window.requestIdleCallback(work, { timeout: 500 });
}
};
window.requestIdleCallback(work, { timeout: 500 });
打印结果:
6. 浏览器缓存
6.1 协商缓存(Etag、Last-Modified)
6.1.1 协商缓存基本过程
1)第一次请求
- 客户端发送 GET 请求,去请求文件;
- 服务器处理请求,返回文件内容和一堆 Header,包括 Etag、Last-Modified,状态码 200
2)第二次请求
- 客户端发送 GET 请求,去请求文件(注意:这个时候客户端请求头上,会带 上 if-none-match 值为 Etag 和 if-modified-since 值为 Last-Modified)
- 服务器判断 Etag 和计算出来的 Etag 是否匹配,若返回状态为 304,则客户端继续使用本地缓存
6.1.2 Etag VS Last-Modified
当 ETag 和 Last-Modified 同时存在时,服务器优先检查 ETag
Etag 是服务器文件的唯一标识,当文件内容变化时,Etag 值也会发生变化
Etag 主要为了解决 Last-Modified 无法解决的一些问题。一些文件也许会周期性的更改,但是它的内容并不改变(也就是说,仅仅改变了修改时间),此时希望重用缓存,而不是重新请求
6.2 强制缓存(Expires、Cache-Control)
6.2.1 Cache-Control VS Expires
Cache-Control:max-age —— 表示缓存内容在 xxx 秒后失效;【优先级更高】
Expires —— 表示服务端返回的到期时间;也就是说,返回的是服务端时间,与客户端时间相比,可能会时间不一致
6.2.2 Cache-Control: no-cache VS Cache-Control: no-store
Cache-Control: no-cache —— 浏览器每次都会向服务器发起请求,来验证当前缓存的有效性
Cache-Control: no-store —— 响应不被缓存
7. 垃圾回收机制
7.1 GC 垃圾回收策略
7.1.1 标记清除
分为 标记 和 清除 两个阶段:
- 标记阶段 —— 把所有活动对象做上标记
- 清除阶段 —— 把没有标记(也就是非活动对象)销毁
基本步骤:
- 在运行时,会给内存中的所有变量都加上一个标记,假设他们都是垃圾,全标记为 0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成 1
- 清理所有标记为 0 的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为 0,等待下一轮垃圾回收
7.1.2 引用计数
一个对象,如果没有其他对象引用到它,这个对象就是 零引用,将被垃圾回收机制回收
它的策略是 —— 跟踪记录每个变量值被使用的次数
基本步骤:
- 一个对象被其他对象引用时,这个对象的引用次数就为 1,如果同一个值又被赋给另一个变量,那么引用数加 1,如果该变量的值被其他的值覆盖了,则引用次数减 1
- 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候,清理掉引用次数为 0 的值占用的内存
7.2 分代式垃圾回收机制
V8 采用了一种代回收的策略,将内存分为两个生代:
- 新生代 中的对象,为存活时间较短的对象
- 老生代 中的对象,为存活时间较长 或 常驻内存的对象
基本步骤:
- 新老生代,采用不同的垃圾回收算法,来提高效率
- 对象最开始都会先被分配到新生代(如果新生代内存空间不够,直接分配到老生代)
- 新生代中的对象会在满足某些条件后,被移动到老生代,这个过程也叫晋升
7.2.1 新生代的垃圾回收方式
将内存空间一分为二,分为:
- From 空间(使用状态)
- To 空间(闲置状态)
当新生代内存不足时,会将 From 空间中存活的对象,复制到到 To 空间,然后将 From 空间清空,交换 From 空间和 To 空间(将原来的 From 空间变为 To 空间),继续下一轮
7.2.2 老生代的垃圾回收方式
V8在老生代中,主要采用了 Mark-Sweep 和 Mark-Compact 相结合的方式
Mark-Sweep 遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象,这就导致了 —— 在进行清除回收以后,内存碎片化
Mark-Compact 用来解决内存碎片的问题,将将存活对象向内存一侧移动,清空内存的另一侧,这样空闲的内存都是连续的
7.2.3 新老生代在不同系统的内存大小
64 位系统,新生代内存大小为 32MB,老生代内存为 1.4G
32 位系统,新生代内存大小为 16MB,老生代内存为 0.7G
7.3 垃圾回收机制参考文章
「硬核JS」你真的了解垃圾回收机制吗 - 掘金JavaScript 是门魅力无限的语言,关于它的 GC(垃圾回收)方面,你了解多少呢?想来大部分人是因为面试才去看一些面试题从而了解的GC,当然,我们可不仅仅是为了面试,目的是一次性彻底搞懂GC!https://juejin.cn/post/6981588276356317214
V8 内存浅析 - 知乎这篇文章包括以下内容,阅读完大概需要 6 分钟。 简介V8 内存构成V8 垃圾生命周期(垃圾回收)使用 Chrome 调优前端代码使用 alinode 调优 node.js 进程 简介 V8 是谷歌开发的高性能 JavaScript 引擎,该引擎使用 …https://zhuanlan.zhihu.com/p/33816534