原理
事件循环
1、浏览器进程
进程是指在计算机中运行的一个程序实例,每个进程都有自己的内存空间和系统资源。
线程是进程中的一个执行单元,是进程的实际执行者。一个进程可以包含多个线程,它们共享进程的内存空间和系统资源。
现代浏览器通常是多进程的。浏览器将不同的任务分配给不同的进程来执行,每个进程都是相互独立的,拥有自己的内存空间。
常见的浏览器进程:
- 主进程(Main Process):也称为浏览器进程或渲染进程管理器,负责协调和控制其他进程的工作。它负责创建和销毁其他进程,并提供浏览器的用户界面。
- 渲染进程(Renderer Process):每个标签页通常都有一个独立的渲染进程,负责解析和渲染网页内容。它将HTML、CSS和JavaScript转换为可视化的网页,并将其显示在屏幕上。
- 网络进程(Network Process):负责处理网络请求和响应。它负责下载网页内容、图像、脚本等资源,并将其提供给渲染进程。
- GPU进程(GPU Process):负责处理浏览器中的图形操作,如绘制网页内容、执行CSS动画等。将这些任务交给GPU进程可以提高渲染的性能。
- 插件进程(Plugin Process):如果浏览器使用了插件(如Flash Player),每个插件通常都运行在独立的进程中,以增加安全性和稳定性。
每个进程都在操作系统级别独立运行,通过进程间通信(IPC)机制进行通信和协作。
2、渲染进程
渲染进程会包含如下线程:
- 主线程(Main Thread):主线程负责处理用户输入、执行JavaScript代码以及管理其他线程。事件循环在主线程上运行,它负责监听和分发各种事件,如用户输入事件、计时器事件、网络请求完成事件等。
- 渲染线程(Render Thread):渲染线程负责解析和渲染网页内容。它将HTML、CSS和JavaScript转换为可视化的网页,并将其显示在屏幕上。渲染线程会将渲染结果发送给主线程,以便主线程更新显示。
- 合成线程(Compositor Thread):合成线程负责将渲染线程生成的图像进行合成和绘制。它将各个图层合成为最终的屏幕图像,并将其显示在屏幕上。合成线程与主线程协作,确保渲染结果按正确的顺序显示在屏幕上。
事件循环:
- 在最开始的时候,渲染主线程进入一个无线循环
- 每次循环会检查任务队列,如果有任务,则从任务队列中取出第一个任务执行,执行完后进入下一次循环。
- 其他线程可以向任务队列末尾添加任务。
任务队列优先级:
- 任务有不同类型,同一个类型任务会添加到同一个队列中,意味着任务队列有多个。
- 在一次事件循环中,浏览器根据情况从不同队列中取出任务执行。
- 浏览器必须准备好一个微任务队列,该队列的执行优先级最高。
将任务添加到微任务队列可以使用Promise、MutationObserver等。
例:
// 将 fn 加入微任务队列
// 1、Promise
Promise.resolve().then(fn)
// 2、MutationObserver
// 当document对象的子节点发生变化时,触发回调函数,将任务添加到微任务队列中。
const observer = new MutationObserver(fn);
observer.observe(document, { childList: true });
// 3、queueMicrotask()
queueMicrotask(fn);
浏览器渲染原理
在通过网络获取到 HTML 之后,一个渲染任务被加入到任务队列中。浏览器通过事件循环从队列中取出该任务,执行渲染过程:
-
解析 HTML:解析 HTML 文本,生成DOM树及CSSOM树。
(1)CSSOM树大致结构如下:根节点 StyleSheetList,该节点的每个子节点即代表一个样式表对象CSSStyleSheet,如内部样式表style标签,外部样式表,行内样式表,浏览器默认样式表等。
(2)因为执行js可能会更改DOM树,在解析到 script 标签时,会停止HTML的解析,转而下载该js文件并执行全局代码,完成后再继续解析HTML,因此 js 的执行会阻塞HTML的解析过程。
-
样式计算:根据前一步生成的DOM树和CSSOM树计算每一个DOM元素的样式,并分别得到它们的计算后(Computed)的样式(可在浏览器控制台的Computed选项中查看),即每个元素都会包含所有样式属性,且属性值的单位为绝对单位(rem -> px,red -> rgb(255,0,0)等)。初始的时候,所有元素的所有样式属性都没有值,需要经过计算得到值。计算过程如下:
(1)确定声明值:对比作者样式表和浏览器默认样式表,找到没有冲突的样式,直接作为计算后的样式。
(2)层叠:通过层叠规则确定有冲突的样式的值。
第一步:比较重要性,带有 !important 的作者样式 > 带有 !important 的默认样式 > 作者样式 > 默认样式。如果作者样式表中存在冲突,继续第二步。
第二步:比较特殊性,对每个样式属性分别计数:
是否内联样式 id选择器数量 类、伪类、属性选择器数量 元素、伪元素选择器数量 是1,否0 例如一个:1 如一个类选择器、一个伪类选择器:2 如一个元素选择器、一个伪元素选择器:2 例如:
.bold { font-size: 40px; // 特殊性值为 0010 } h1 { font-size: 26px; // 特殊性值为 0001 } #div div h1.bold:first-child { font-size: 34px; // 特殊性值为 0122 }从前往后比,如:0132 > 0032。如果仍存在冲突,继续第三步。
第三步:比较顺序。靠后的覆盖靠前的,如:
div { font-size: 23px; font-size: 30px; // 最终样式 }(3)继承:经过前两步计算后仍然没有值的属性,若可继承,则使用继承值。可继承的CSS属性:字体相关属性如font-size、font-weight,文本相关属性如color等等。
(4)使用默认值:经过前三步仍然没有值的属性,使用默认值。CSS中所有属性都有一个默认值,如
background-color: transparent; text-align: left; ... -
布局:遍历DOM树,计算每个节点的几何信息(宽、高和相对包含块位置等),得到布局树。一般DOM树和布局树并非一一对应,比如某个DOM元素
display:none没有几何信息,则布局树中将没有该元素;比如使用了伪元素选择器,虽然在DOM树中不存在,但它们拥有几何信息,所以会生成到布局树中;还有匿名行盒、匿名块盒等都只存在于布局树中。 -
分层:主线程会使用一套策略对布局树进行分层。分层的好处在于,某一个层改变后,仅会对该层进行后续处理,从而提升效率。will-change属性可以很大程度影响分层结果。
-
绘制:为每一层生成绘制指令(类似canvas画图)集,用于描述该层如何绘制。
(以下并非在渲染主线程中执行)
-
分块:主线程将每个图层信息交给合成线程,它会对每个图层进行分块,划分为更多小区域,这里它会从线程池取多个线程来完成分块工作。
-
光栅化:合成线程将块信息交给GPU进程,GPU会开启多个线程完成光栅化,将每个块变成位图,优先处理靠近视口的块。
-
画:合成线程拿到每个层、每个块的位图后,生成一个个指引(quad)信息,并提交给GPU进程,由GPU硬件完成屏幕成像。
回流(reflow)会使渲染主线程从“样式计算”步骤开始往后执行,而重绘(repaint)一般会从“绘制”步骤往后执行,所以reflow一定引起repaint。
使用transform变换效率高的原因:不由渲染主线程执行,只涉及后几个步骤。例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.ball {
width: 200px;
height: 200px;
border-radius: 50%;
background-color: red;
margin-bottom: 30px;
}
.ball1 {
animation: move1 1s alternate infinite;
}
.ball2 {
position: fixed;
left: 0;
animation: move2 1s alternate infinite;
}
@keyframes move1 {
to {
transform: translate(100px);
}
}
@keyframes move2 {
to {
left: 100px;
}
}
</style>
</head>
<body>
<button onclick="makeDelay()">死循环3s</button>
<div class="ball1 ball"></div>
<div class="ball2 ball"></div>
<script>
function delay(duration) {
const start = Date.now();
while ((Date.now() - start) < duration) {}
}
function makeDelay() {
delay(3000)
}
</script>
</body>
</html>
网络
http和https
- http是明文传输,https是加密传输。
- http默认端口80,https默认端口443。
http1.0、http1.1、http2.0协议的区别
http1.0
每次请求和响应完毕后都会销毁 TCP 连接,同时规定前一个响应完成后才能发送下一个请求。这样做有两个问题:
1、无法复用连接
每次请求都要创建新的 TCP 连接,完成三次握手和四次挥手,网络利用率低
2、队头阻塞
如果前一个请求被某种原因阻塞了,会导致后续请求无法发送。
http1.1
http1.1 是 http1.0 的改进版,它做出了以下改进:
- 长连接
http1.1 允许在请求时增加请求头connection:keep-alive,这样便允许后续的客户端请求在一段时间内复用之前的 TCP 连接
- 管道化
基于长连接的基础,管道化可以不等第一个请求响应继续发送后面的请求,但响应的顺序还是按照请求的顺序返回。
- 缓存处理
新增响应头 cache-control,用于实现客户端缓存。
- 断点传输
在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率
http2.0
http2.0 进一步优化了传输效率,它主要有以下改进:
- 二进制分帧
将传输的消息分为更小的二进制帧,每帧有自己的标识序号,即便被随意打乱也能在另一端正确组装
- 多路复用
基于二进制分帧,在同一域名下所有访问都是从同一个 tcp 连接中走,并且不再有队头阻塞问题,也无须遵守响应顺序
- 头部压缩
http2.0 通过字典的形式,将头部中的常见信息替换为更少的字符,极大的减少了头部的数据量,从而实现更小的传输量
- 服务器推送
http2.0 允许服务器直接推送消息给客户端,无须客户端明确的请求
多路复用
为什么 HTTP1.1 不能实现多路复用
HTTP/1.1 不是二进制传输,而是通过文本进行传输。由于没有流的概念,在使用并行传输(多路复用)传递数据时,接收端在接收到响应后,并不能区分多个响应分别对应的请求,所以无法将多个响应的结果重新进行组装,也就实现不了多路复用。
http2 的多路复用
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。 多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
Http 状态码 301 和 302 的应用场景
301 表示永久重定向,302 表示临时重定向。
如果浏览器收到的是 301,则会缓存重定向的地址,之后不会再重新请求服务器,直接使用缓存的地址请求,这样可以减少请求次数。但如果浏览器收到的是 302,则不会缓存重定向地址,浏览器将来会继续以原有地址请求。
因此,301 适合地址永久转移的场景,比如域名变更;而 302 适合临时转移的场景,比如首页临时跳转到活动页
文件上传如何做断点续传
客户端将文件的二进制内容进行分片,每片数据按顺序进行序号标识,上传每片数据时同时附带其序号。服务器接收到每片数据时,将其保存成一个临时分片文件,并记录每个文件的 hash 和序号。
若上传中止,将来再次上传时,可以向服务器索要已上传的分片序号,客户端仅需上传剩余分片即可。
当全部分片上传完成后,服务器按照分片的顺序组装成完整的文件,并删除分片文件。
SSL 和 TLS
它们都是用于保证传输安全的协议,介于传输层和应用层之间,TLS 是 SSL 的升级版。
它们的基本流程一致:
- 客户端向服务器端索要公钥,并使用数字证书验证公钥。
- 客户端使用公钥加密会话密钥,服务端用私钥解密会话密钥,于是得到一个双方都认可的会话密钥
- 传输的数据使用会话密钥加密,然后再传输,接收消息方使用会话密钥解密得到原始数据
GET 和 POST 的区别
- 浏览器在发送 GET 请求时,不会附带请求体
- GET 请求的传递信息量有限,适合传递少量数据;POST 请求的传递信息量是没有限制的,适合传输大量数据。
- GET 请求只能传递 ASCII 数据,遇到非 ASCII 数据需要进行编码;POST 请求没有限制
- 大部分 GET 请求传递的数据都附带在 path 参数中,能够通过分享地址完整的重现页面,但同时也暴露了数据,若有敏感数据传递,不应该使用 GET 请求,至少不应该放到 path 中
- 刷新页面时,若当前的页面是通过 POST 请求得到的,则浏览器会提示用户是否重新提交。若是 GET 请求得到的页面则没有提示。
- GET 请求的地址可以被保存为浏览器书签,POST 不可以
webSocket 与传统的 http 有什么优势
当需要前后端实时通信时,过去我们往往使用两种方式完成:
第一种是短轮询,即客户端每隔一段时间就向服务器发送消息,询问有没有新的数据
第二种是长轮询,发起一次请求询问服务器,服务器可以将该请求挂起,等到有新消息时再进行响应。响应后,客户端立即又发起一次请求,重复整个流程。
无论是哪一种方式,都暴露了 http 协议的弱点,即响应必须在请求之后发生,服务器是被动的,无法主动推送消息。而让客户端不断的发起请求又白白的占用了资源。
websocket 的出现就是为了解决这个问题,它利用 http 协议完成握手之后,就可以与服务器建立持久的连接,服务器可以在任何需要的时候,主动推送消息给客户端,这样占用的资源最少,同时实时性也最高。
基础
css
瀑布流布局
使用CSS多列布局实现,元素会优先从上到下排列:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
column-count: 3;
column-gap: 20px;
}
.item {
break-inside: avoid;
margin-bottom: 20px;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
</div>
<script>
const items = document.getElementsByClassName('item')
function getH() {
const r = Math.ceil(Math.random() * 200)
return r + 100
}
for(const e of items) {
e.style.height = getH() + 'px'
}
</script>
</body>
</html>
使用grid布局实现,元素会优先从左到右排列:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 10px;
grid-auto-rows: 1px;
}
.item {
grid-row-start: auto;
border: 1px solid black;
}
</style>
</head>
<body>
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
</div>
<script>
const items = document.getElementsByClassName('item')
const mb = 10
// 获取100~300的随机高度
function getH() {
const r = Math.ceil(Math.random() * 200)
return r + 100
}
for(const e of items) {
e.style.gridRowEnd = 'span ' + (getH() + mb) // "span 1"相当于"grid-auto-rows * 1",且一个span会包含外边距在内
e.style.marginBottom = mb + 'px'
}
</script>
</body>
</html>
还可使用flex布局实现,需要对数据数组进行按列分组,示例略。
虚拟列表
使用CSS属性 "content-visibility: auto;" 实现隐藏可见区域外的元素内容(高度为0),请注意其浏览器兼容性,且会有滚动条抖动问题:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container::-webkit-scrollbar {
display: none;
}
.item {
border: 1px solid black;
margin-bottom: 30px;
content-visibility: auto;
line-height: 1.5em;
/* contain-intrinsic-height: 500px; */
}
</style>
</head>
<body>
<div class="container">
<div class="item">长内容</div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</body>
</html>
js
原型链
每个构造函数都有其对应的原型对象,由构造函数创建的实例会有一个内部属性指向该原型对象,实例会继承该原型对象的属性。原型对象本身也有一个内部属性指向一个原型对象,依此类推,终点为null。访问对象的某个属性时会沿原型链往上查找,直到找到或返回 undefined。
原型链示意图:
例:
function A() {}
const a = new A();
// 以下都输出 true
console.log(A.__proto__ === Function.prototype) // A.__proto__ 可替换为 Object.getPrototypeOf(A)
console.log(Object.__proto__ === Function.prototype)
console.log(Function.__proto__ === Function.prototype)
console.log(A.prototype.__proto__ === Object.prototype)
console.log(Function.prototype.__proto__ === Object.prototype)
console.log(Object.prototype.__proto__ === null)
展开运算符是深拷贝还是浅拷贝?
当使用展开运算符复制数组或对象时,它会创建一个新的数组或对象。但是对于嵌套的对象或数组,它复制的仅仅是引用值而非创建新的堆内存。故展开运算符是浅拷贝。如:
// 对象
const obj = {
name: 'a',
innerObj: {
name: 'b',
}
}
const obj1 = { ...obj }
console.log(obj === obj1) // false
console.log(obj.innerObj === obj1.innerObj) // true
// 数组
const arr = [1, 2, [3]]
const arr1 = [...arr]
console.log(arr === arr1) // false
console.log(arr[2] === arr1[2]) // true
深拷贝的几种方法
1、递归
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
使用递归时可能会因为循环引用而导致无限递归,故可使用Set对象存储已访问过的属性并添加重复访问的判断:
function deepCopy(obj, cache = new Set()) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (cache.has(obj)) {
return obj; // 已经处理过该对象,直接返回
}
cache.add(obj);
let copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], cache);
}
}
return copy;
}
2、JSON序列化与反序列化
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
此方法无法复制函数和特殊对象(如正则表达式、Date 对象)的属性和方法;此方法会忽略对象的原型链和构造函数。
3、第三方库
例如 Lodash 的 cloneDeep() 方法:
// 使用 Lodash 的 cloneDeep 方法
const clone = _.cloneDeep(obj);
new运算符做了什么
使用new调用构造函数时,做了如下工作:
- 创建一个新的空对象({})。
- 将新创建的对象的
__proto__属性设置为构造函数的prototype属性。 - 将构造函数的
this关键字绑定到新创建的对象。 - 执行构造函数内部的代码。在构造函数内部,可以使用
this来引用新创建的对象,并对其进行属性和方法的设置。 - 如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象。
const a = new A()
// new 做了如下工作
const obj = {}
obj.__proto__ = A.prototype
A.call(obj)
return obj
Map、Set、WeakMap、WeakSet
Map
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值,一个键只能出现一次,意味着对已存在的相同的键设置值时,会覆盖其值:
const map = new Map()
const objKey = { aaa: 'bbb' }
map.set(objKey, '222')
map.set(objKey, '333')
const keys = map.keys() // 返回一个迭代器对象
console.log(keys.next().value) // { aaa: 'bbb' }
console.log(keys.next().value) // undefined
console.log(map.get(objKey)) // '333'
键的比较使用零值相等(NaN与NaN相等,-0和+0相等,其他与 "===" 表现相同),例:
const map = new Map()
map.set(0, '0000')
map.set(-0, '-0000')
map.set(NaN, 'NaN1')
map.set(NaN, 'NaN2')
console.log(map.get(0)) // "-0000"
console.log(map.get(-0)) // "-0000"
console.log(map.get(NaN)) // "NaN2"
Map与Object不同之处:
1、Map默认情况不包含任何键,只包含显式插入的键。而一个 Object 有一个原型,原型链上的键名有可能和你自己在对象上设置的键名产生冲突。例:
console.log(Map.prototype.__proto__ === Object.prototype) // true
Object.prototype.a = 1
const map = new Map()
console.log(map.get('a')) // undefined
console.log(map.a) // 1
console.log(map['a']) // 1
2、一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。一个 Object 的键必须是一个 String 或是 Symbol。
3、Map 是可迭代的,可以直接被迭代。而 Object 不可直接被迭代。
4、Map 不支持序列化和解析。而 Object 原生支持。例:
// Object
const obj = {
a: 1,
b: {
bb: '222'
}
}
const obj1 = JSON.parse(JSON.stringify(obj))
console.log(obj) // { a: 1, b: { bb: '222' }}
console.log(obj1) // { a: 1, b: { bb: '222' }}
console.log(obj.b === obj1.b) // false
// Map
const map = new Map([['a', 111], ['b', {
c: {
d: new Map([['dd', 'text']])
}
}]])
const map1 = JSON.parse(JSON.stringify(map))
console.log(map) // Map(2)
console.log(map1) // {}
console.log(Object.prototype.toString.call(map1)) // [object Object]
不过,可以通过传入额外的参数使Map可以进行序列化及解析:
function replacer(key, value) {
if(value instanceof Map) {
return {
dataType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
}
} else {
return value
}
}
function reviver(key, value) {
if(typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value)
}
}
return value
}
const map = new Map([['a', 111], ['b', {
c: {
d: new Map([['dd', 'text']])
}
}]])
const str = JSON.stringify(map, replacer)
const map2 = JSON.parse(str, reviver)
console.log(map2) // Map(2)
console.log(JSON.stringify(map) === JSON.stringify(map2)) // true
console.log(Object.prototype.toString.call(map2)) // [object Map]
WeakMap
WeakMap 对象是一组键/值对的集合。其键必须是对象,而值可以是任意的。其中的键是弱引用的,意味着在没有其他引用存在时垃圾回收能正确进行,避免内存泄漏。正由于这样的弱引用,WeakMap 的 key 是不可枚举的(没有方法能给出所有的 key)。
Set
Set对象用于存储任何类型的唯一值,你可以按照插入的顺序迭代它的元素。例:
const arr = [1, 1, 1, 2, 2, 2]
const arr1 = Array.from(new Set(arr))
const arr2 = [...new Set(arr)]
console.log(arr1) // [1, 2]
console.log(arr2) // [1, 2]
WeakSet
它和 Set 的主要区别有:
WeakSet只能是对象的集合,而 Set 可以是任何类型的任意值。WeakSet持弱引用:集合中对象的引用为弱引用。如果没有其他的对WeakSet中对象的引用,那么这些对象会被当成垃圾回收掉。
这也意味着 WeakSet 中没有存储当前对象的列表,故WeakSet 是不可枚举的。一般用于成员的存在性检查。
WeakSet 可用于检测循环引用:
// 对 传入的 subject 对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
// 避免无限递归
if (_refs.has(subject)) {
return
}
fn(subject)
if (typeof subject === "object") {
_refs.add(subject)
for (const key in subject) {
execRecursively(fn, subject[key], _refs)
}
}
}
const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
}
foo.bar.baz = foo // 循环引用!
execRecursively((obj) => console.log(obj), foo)
常见手写题
手写Promise
实现Promise的构造函数及then方法:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
#state = PENDING // "#"定义为私有属性
#result = undefined
#handlers = []
constructor(executor) {
const resolve = (data) => {
this.#changeState(FULFILLED, data)
}
const reject = (err) => {
this.#changeState(REJECTED, err)
}
try {
executor(resolve, reject)
}
catch(err) {
reject(err)
}
}
#changeState(state, result) {
if(this.#state !== PENDING) return
this.#state = state
this.#result = result
this.#run()
}
// 判断传入值是否为promise对象
#isPromise(value) {
if(value !== null && (typeof value === 'object' || typeof value === 'function')) {
return (typeof value.then) === 'function'
}
return false
}
// 将传入值加入微任务队列执行
#addToMicrotaskRun(task) {
// Node环境
if(typeof process === 'object' && typeof process.nextTick === 'function') {
process.nextTick(task)
}
// 浏览器环境
else {
// 此处可增加对浏览器环境下不同API的检查,这里没有做判断,直接使用了queueMicrotask
queueMicrotask(task)
// 或使用 MutationObserver
// const m = new MutationObserver(task)
// const text = document.createTextNode('1')
// m.observe(text, {
// characterData: true
// })
// text.data = '2'
}
}
#runCb(callback, resolve, reject) {
this.#addToMicrotaskRun(() => {
// 1、传入then的不是函数,直接透传结果给下一个Promise
if(typeof callback !== 'function') {
if(this.#state === FULFILLED) {
resolve(this.#result)
}
else {
reject(this.#result)
}
}
// 2、是函数,则传入当前promise结果执行函数,并将执行的返回值传递给下一个promise
// 如果执行期间报错,直接reject错误
else {
try {
const result = callback(this.#result)
// 判断执行then中函数的返回result是否为promise
if(this.#isPromise(result)) {
result.then(resolve, reject)
}
else {
resolve(result)
}
}
catch(err) {
reject(err)
}
}
})
}
// 引入此方法处理Promise传入构造函数的参数内部异步改变状态的情况:
// new MyPromise((res, rej) => {
// setTimeout(() => { res(123) }, 1000)
// })
#run() {
if(this.#state === PENDING) return
while (this.#handlers.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handlers.shift()
if(this.#state === FULFILLED) {
this.#runCb(onFulfilled, resolve, reject)
}
else {
this.#runCb(onRejected, resolve, reject)
}
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#handlers.push({
onFulfilled,
onRejected,
resolve,
reject
})
this.#run()
})
}
catch(onRejected) {
return this.then(undefined, onRejected)
}
finally(onFinally) {
return this.then(res => {
onFinally()
return res
}, reason => {
onFinally()
throw reason
})
}
static resolve(value) {
if(value instanceof MyPromise) return value // 如果是一个Promise,直接返回
let _resolve, _reject
const p = new MyPromise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
if(p.#isThenable(value)) {
value.then(_resolve, _reject)
} else {
_resolve(value)
}
return p
}
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason)
})
}
}
使用示例:
const p = new MyPromise((resolve, reject) => {
resolve(1111)
})
p.then((res) => {
console.log(res) // 1111
return new MyPromise(resolve => {
setTimeout(() => {
resolve(2222)
}, 1000)
})
}).then(res => {
console.log(res) // 2222
})
setTimeout(() => {
console.log(4444)
}, 2000)
console.log(3333)
// 输出 3333 1111 2222 4444
Promise.resolve、Promise.reject示例:
const p = new MyPromise((resolve, reject) => {
resolve(1111)
})
const p1 = new Promise((resolve, reject) => {
resolve(2222)
})
console.log(MyPromise.resolve(p) === p) // true
console.log(MyPromise.resolve(p1) === p1) // false
MyPromise.resolve(1233).then(res => {
console.log(res) // 1233
})
MyPromise.resolve(p1).then(res => {
console.log(res) // 2222
})
MyPromise.reject('errr').catch(reason => {
console.log(reason) // errr
})
防抖、节流
防抖,一段时间内重复触发会重新计时,只执行最后一次。常见防抖应用有:响应窗口尺寸变化的函数、输入框搜索请求函数、响应按钮点击的函数等等。
function debounce(fn, delay) {
let timer = null
return function() {
if(timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
节流,一段时间内重复触发只执行一次。常见节流应用:页面滚动事件、鼠标移动事件等等。
function throttle(fn, delay) {
let timer = null
return function() {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}
排序
冒泡排序
比较相邻的两个元素,如果顺序错误则交换它们的位置,重复执行这个过程直到整个数组排序完成。
function bubbleSort(arr) {
const len = arr.length
for(let i=0; i<len-1; i++) { // 总比较轮数len-1,每一轮将待排序列表中的最大数排到末尾,已排序元素个数i
for(let j=0; j<len-1-i; j++) {
if(arr[j] > arr[j+1]) {
const temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}
快速排序
选择一个基准元素,将数组分为两部分,一部分小于基准元素,一部分大于基准元素,对这两部分递归地应用快速排序,直到整个数组排序完成。
function quickSort(arr) {
if(arr.length<=1) {
return arr
}
const pIndex = Math.floor((arr.length-1)/2)
const p = arr.splice(pIndex, 1)[0] // splice返回包含删除元素的数组
const left = []
const right = []
for(let i=0; i<arr.length; i++) {
if(arr[i] < p) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return [...quickSort(left), p, ...quickSort(right)]
}
发布订阅模式
发布订阅模式是一种行为设计模式。发布订阅模式:存在一个消息代理对象,其内部维护了一个消息类型与订阅者列表的映射,订阅者通过代理对象订阅特定类型的消息来接收相应的通知,代理对象通过发布指定类型消息广播给所有该类型订阅者。可以取消一个类型的所有订阅,或只取消一个类型的特定订阅。
const msProxy = {
subs: new Map(),
// 订阅
subscribe(type, cb) {
if(!this.subs.get(type)) {
this.subs.set(type, [])
}
this.subs.get(type).push(cb)
},
// 发布
publish(type, data) {
const cbs = this.subs.get(type)
if(cbs) {
for(const cb of cbs) {
cb(data)
}
} else {
console.log('不存在此类型消息')
}
},
// 取消订阅
unsubscribe(type, cb) {
if(this.subs.get(type)) {
if(cb) {
const newSubs = this.subs.get(type).filter(item => item !== cb)
this.subs.set(type, newSubs)
}
else {
this.subs.delete(type)
}
}
}
}
使用示例:
// 定义订阅者
function sub1(data) {
console.log('sub1: ', data)
}
function sub2(data) {
console.log('sub2: ', data)
}
// 订阅
msProxy.subscribe('type1', sub1)
msProxy.subscribe('type1', sub2)
// 发布
msProxy.publish('type1', {a: 123, b: '666'})
// 取消订阅
msProxy.unsubscribe('type1')
// msProxy.unsubscribe('type1', sub2)
// 再次发布
msProxy.publish('type1', {a: 123, b: '666'})
观察者模式
观察者模式是一种行为设计模式,用于在对象之间建立一种一对多的依赖关系,当一个对象的状态发生变化时,它的所有依赖对象都会收到通知并自动更新。
// 主题对象(被观察者)
class Subject {
constructor() {
this.observers = new Set() // 观察者列表
}
// 注册观察者
addObserver(observer) {
this.observers.add(observer)
}
// 注销观察者
removeObserver(observer) {
if(this.observers.has(observer)) {
this.observers.delete(observer)
}
}
// 通知观察者
notify(data) {
this.observers.forEach(observer => {
observer.update(data)
})
}
}
// 观察者对象
class Observer {
constructor(name) {
this.name = name
}
update(data) {
console.log(`${this.name} update, data: ${data}`)
}
}
const subject = new Subject()
const observer1 = new Observer('observer1')
const observer2 = new Observer('observer2')
// 注册观察者
subject.addObserver(observer1)
subject.addObserver(observer2)
subject.addObserver(observer2)
// 通知观察者
subject.notify('notify data')
单例模式
单例模式是一种创建型模式。单例模式中,多次实例化将得到相同的对象,可通过将第一次创建的实例化对象存储为类静态属性实现:
class Singleton {
constructor() {
if (!Singleton.instance) {
// 初始化单例对象
Singleton.instance = this
}
return Singleton.instance
}
// 单例对象的方法...
}
// 使用单例模式
const instance1 = new Singleton()
const instance2 = new Singleton()
const instance3 = new Singleton()
console.log(instance1 === instance2) // true
console.log(instance2 === instance3) // true
console.log(instance1 === Singleton.instance) // true