# 前端监控上报数据
重点解析在 页面实例关闭 时,如何将监控数据上传到服务端的解决方案。涉及到 4 种方案,分别为:
- 同步
XMLHttpRequest
img.src
navigator.sendBeacon
fetch keepalive
# 同步 XMLHttpRequest
已废弃。
为什么同步 XMLHttpRequest 可以在页面关闭时上传数据?同步请求阻止代码的执行,这会导致屏幕上出现“冻结”和无响应的用户体验。
缺点:
- 用户体验差,会阻塞页面切换
- 只有旧版的浏览器支持 Chrome<80
- 无法读取 reponse 的返回值
# img.src
创建一个<img>
元素,并设置src
。大部分的浏览器,都会延迟卸载当前页面,优先加载图像。
var data = JSON.stringify({
time: performance.now(),
});
const img = new Image();
img.src = `http://api.wangxiaokai.vip/test?${JSON.stringify(data)}`;
2
3
4
5
6
缺点:
- 数据传输不可靠,有可能浏览器卸载当前页面,直接杀掉图像请求
- 只能发起 GET 请求
- 数据大小有限制
# navigator.sendBeacon
通过 HTTP POST 请求,将少量数据使用异步的方式,发送到服务端。
function reportEvent() {
const url = "http://api.wangxiaokai.vip/test";
const data = JSON.stringify({
time: performance.now(),
});
navigator.sendBeacon(url, data);
}
document.addEventListener("visibilitychange", function () {
if (document.visiblityState === "hidden") {
reportEvent();
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
- 浏览器端自动判断合适的时机进行发送。
- 不会产生阻塞,影响当前页面的卸载。
- 不影响下个新页面的加载,不存在性能问题。
- 另外,数据传输可靠。
- 当浏览器将数据成功加入传输队列时,sendBeacon 方法会返回 true,否则返回 false。注意返回值的时机:成功加入传输队列,而不是服务端的处理成功后的返回。
缺点:
- 只能发起 POST 请求
- 无法自定义请求头参数
- 数据大小有限制 (Chrome 限制大小为 64kb)
- 只能在 window 事件 visibilitychange 和 beforeunload 中使用,其他事件中回调,会丢失数据
出于兼容性原因,请确保使用 document.addEventListener 而不是 window.addEventListener 来注册
visibilitychange
回调。
# fetch keepalive
The keepalive option can be used to allow the request to outlive the page. Fetch with the keepalive flag is a replacement for the Navigator.sendBeacon() API. keepalive 选项可用来允许一个请求在页面关闭后继续存在。带 keepalive 标志的 Fetch 可以替代 Navigator.sendBeacon() API。
标记 keepalive 的 fetch 请求允许在页面卸载后执行。
const url = "http://api.wangxiaokai.vip/test";
const data = JSON.stringify({
time: performance.now(),
});
fetch(url, {
method: "POST",
body: data,
headers: {
"Content-Type": "application/json",
},
keepalive: true,
});
2
3
4
5
6
7
8
9
10
11
12
13
# undefined
不要再直接写 undefined,因为可以局部也起一个叫 undefined 的变量并给它赋值,如果此时用到了 undefined,那么可能出现 bug。示例如下:
function test(value) {
let undefined = "hello world";
if (value === undefined) {
return `value is undefined`;
}
return `value is not undefined`;
}
let value;
test(value); // 'value is not undefined'
2
3
4
5
6
7
8
9
推荐的做法:使用void 0
或void(0)
代替undefined
。
void 运算符是对给定的表达式进行求值,然后返回 undefined 。而且, void 是不能重新定义的,不然会报语法错误,这样也保证了用 void 来代替 undefined 的不会出现被重定义而造成的 bug。
# js 判断图片是否被缓存
/**
url 测试图片路径
被缓存返回true,没被缓存返回false
*/
function testCache(url) {
alert("执行");
var url = "http://www.8chedao.com/page/images/webIndex-logo.png";
var myImg = new Image();
myImg.src = url;
if (myImg.complete) {
alert("图片被缓存");
return true;
} else {
alert("图片没被缓存");
myImg.onload = function () {
alert("图片已经下载成功!");
};
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 正则-捕获组
const str = "2022-07";
const reg = /(?<year>[0-9]{4})-(?<month>[0-9]{2})/;
const group = str.match(reg).groups;
console.log(group.year);
2
3
4
5
6
# css 中 html 和 body 的区别
- 在 html 中,
<html>
包含<body>
- 在 html 文档中,
<html>
是根元素。 - 在 css 中有一个
:root
选择器,和html
选择器作用一样,甚至:root
具有更高的优先级! - 以下内联属性 Inline Attribute 最初在规范 spec 中分配给了
<body>
:background
、bgcolor
、marginbottom
、marginleft
、marginright
、margintop
、text
- 相应的 CSS 属性:
background
、background/background-color
、margin-bottom
、margin-left
、margin-right
、margin-top
、font
rem
单位是相对于文档根目录的。是相对于<html>
元素的字体大小的。- 如何将
<html>
上的字体大小设置为百分比,以便在使用 rem 单位时用作重置:1 (opens new window) - 在 body 上设置
background-color
, - 在 html 上设置
background-color
, - JavaScript 也存在差异。例如,html 是
document.rootElement
,body 是document.body
。 - body 元素实际上没有浏览器窗口那么高。它只和里面的内容一样高,就像 div 或其他任何东西一样。所以在 body 上设置 background 时要特别注意,背景有可能撑不起来:如果 html 元素没有背景,body 背景将覆盖页面。如果 html 元素上有背景,则 body 背景的行为与任何其他元素一样。所以背景色要么设置在 html 上,要么给 body 设置一个高度,以防万一。
- 将基本字体大小定义为 62.5%,以便以类似于使用 px 的方式方便地调整 rems 大小。
/* 不考虑兼容性 */
html {
font-size: 62.5%;
}
body {
font-size: 1.4rem;
} /* =14px */
h1 {
font-size: 2.4rem;
} /* =24px */
/* 考虑兼容性 */
html {
font-size: 62.5%;
}
body {
font-size: 14px;
font-size: 1.4rem;
} /* =14px */
h1 {
font-size: 24px;
font-size: 2.4rem;
} /* =24px */
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 关键在于区分那些属性是可覆盖的,然后设置在不同的标签上
# Why Not Iframe
为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 "炫技" 或者刻意追求 "特立独行"。
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中。
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免- 登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题 1),有的问题我们可以睁一只眼闭一只眼(问题 4),但有的问题我们则很难解决(问题 3)甚至无法解决(问题 2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。
# 为 iframe 正名
# JavaScript 深拷贝性能分析
# 引用传值
function mutate(obj) {
obj.a = true;
}
const obj = { a: false };
mutate(obj);
console.log(obj.a); // 输出 true
2
3
4
5
6
7
- 对于基本类型,是进行值传递;对于引用类型,传递的是这个对象的指针。
- 在值传递的场景中,函数的形参只是实参的一个副本 ——a copy—— 当函数调用完成后,并不改变实参。但是在 JavaScript 这种引用传递的场景中,函数的形参和实参指向同一个对象,当参数内部改变形参的时候,函数外面的实参也被改变了。
# 浅拷贝:Object.assign ()
function mutateDeepObject(obj) {
obj.a.thing = true;
}
const obj = { a: { thing: false } };
const copy = Object.assign({}, obj);
mutateDeepObject(copy);
console.log(obj.a.thing); // prints true
const a = { a: 1, b: 2, c: 3, d: { e: 5 } };
const b = { ...a };
console.log(b.d.e); // 5
b.d.e = 8;
console.log(a.d.e); // 8
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.assign(target, sources...)
。它接受任意数量的源对象,枚举它们的所有属性并分配给 target。如果我们使用一个新的空对象 target,那么我们就可以实现对象的复制。注意,第一个参数 target,会被改变!- 该方法只适合拷贝没有嵌套对象的情况,否则要注意:他只是个浅拷贝,对于深层的对象只是拷贝的指针。修改新的对象可能会导致源对象发生变化!
- 注意:对象解构运算,这也是浅拷贝。
# 深拷贝的几种方法
深拷贝是一个常见需求,我们可以通过 JSON
转换、递归、lodash 的_.cloneDeep()
等方式实现。
# JSON.parse()
+JSON.stringify()
将该对象转换为其 JSON 字符串表示形式,然后将其解析回对象。
缺点
- 需要创建一个临时的,可能很大的字符串,只是为了把它重新放回解析器。
- 不能处理循环对象。而且循环对象经常发生。
- 诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失。
- 不能克隆 function。
- 对于值为 undefined 的属性,克隆之后会被丢弃。值为 null 的属性会被保留。可以通过改写
JSON.stringify()
的第二个参数进行「修复」。 - 原理:
JSON.stringify
只能处理基本对象、数组和基本类型,而其他类型的值在转换之后都可能出现出乎意料的结果,例如 Date 会转化为字符串, Set 会转化为 {}。JSON.stringify
甚至完全忽略某些内容,比如 undefined 或函数。 - 除此之外,
JSON.parse(JSON.stringify(x))
无法对包含循环引用的对象进行深克隆
# Structured Clone 结构化克隆算法
Structured cloning 是一种现有的算法,用于将值从一个地方转移到另一地方。例如,每当您调用 postMessage 将消息发送到另一个窗口或 WebWorker 时,都会使用它。关于结构化克隆的好处在于它处理循环对象并 支持大量的内置类型。
实际上,JavaScript 中提供了一个原生 API 来执行对象的深拷贝:structuredClone()
。它可以通过结构化克隆算法创建一个给定值的深拷贝,并且还可以传输原始值的可转移对象。
structuredClone()
的实用方式很简单,只需将原始对象传递给该函数,它将返回具有不同引用和对象属性引用的深层副本。
注意事项:
- 拷贝无限嵌套的对象和数组;
- 拷贝循环引用;当对象中存在循环引用时,仍然可以通过
structuredClone()
进行深拷贝。 - 拷贝各种 JavaScript 类型,例如 Date、Set、Map、Error、RegExp、ArrayBuffer、Blob、File、ImageData 等;
- 拷贝使用 structuredClone()得到的对象;
structuredClone()
不能克隆 DOM 元素。将 HTMLElement 对象传递给structuredClone()
将导致错误。- 可以拷贝任何可转移的对象。要注意的是,使用可转移对象时必须小心处理,因为一旦对象被转移,原线程将不再拥有该对象的所有权,因此在发送线程中不能再访问该对象。此外,在接收线程中使用可转移对象时,也需要根据需求进行显式释放,否则可能会导致内存泄漏和其他问题。
const originalObject = {
set: new Set([1, 3, 3]),
map: new Map([[1, 2]]),
regex: /foo/,
deep: { array: [new File(someBlobData, "file.txt")] },
error: new Error("Hello!"),
};
originalObject.circular = originalObject;
const copied = structuredClone(originalObject);
2
3
4
5
6
7
8
9
10
在 JavaScript 中,可转移对象(Transferable Objects)是指 ArrayBuffer 和 MessagePort 等类型的对象,它们可以在主线程和 Web Worker 线程之间相互传递,同时还可以实现零拷贝内存共享,提高性能。这是由于可转移对象具有两个特点:
- 可共享:可转移对象本身没有所有权,可以在多个线程之间共享,实现零拷贝内存共享。
- 可转移:调用 Transferable API 时,可转移对象会从发送方(发送线程)转移到接收方(接收线程),不再存在于原始线程中,因此可以避免内存拷贝和分配等开销。
structuredClone()
不能拷贝的数据类型:- 函数或方法,会抛出异常。
- DOM 节点,也会抛出异常。
- 属性描述符、setter 和 getter,以及类似的元数据都不能被克隆。
- 对象原型,原型链不能被遍历或拷贝。所以如果克隆一个实例 MyClass,克隆的对象将不再是这个类的一个实例(但是这个类的所有有效属性都将被拷贝)。
支持拷贝的类型如下:
- JS 内置对象:Array(数组)、ArrayBuffer(数据缓冲区)、Boolean(布尔类型)、DataView(数据视图)、Date(日期类型)、Error(错误类型,包括下面列出的具体类型)、Map(映射类型)、Object (仅指纯对象,如从对象字面量中创建的对象)、原始类型(除 symbol 外,即 number、string、null、undefined、boolean、BigInt)、RegExp(正则表达式)、Set(集合类型)、TypedArray(类型化数组)。
- Error 类型:Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError。
- Web/API 类型:AudioData、Blob、CryptoKey、DOMException、DOMMatrix、DOMMatrixReadOnly、DOMPoint、DomQuad、DomRect、File、FileList、FileSystemDirectoryHandle、FileSystemFileHandle、FileSystemHandle、ImageBitmap、ImageData、RTCCertificate、VideoFrame。
目前主流浏览器都支持 structuredClone API。
# MessageChannel
可以创建一个 MessageChannel 并发送消息。在接收端,消息包含我们原始数据对象的结构化克隆。
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
const obj = /* ... */;
const clone = await structuralClone(obj);
2
3
4
5
6
7
8
9
10
缺点是它是异步的。虽然这并无大碍,但是有时候你需要使用同步的方式来深度拷贝一个对象。
# History API
history.pushState()
可以提供一个状态对象来保存 URL。这个状态对象使用结构化克隆 - 而且是同步的。
为了防止发生任何意外,请使用 history.replaceState()
而不是 history.pushState()
。
function structuralClone(obj) {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);
return copy;
}
const obj = /* ... */;
const clone = structuralClone(obj);
2
3
4
5
6
7
8
9
10
# Notification API
function structuralClone(obj) {
return new Notification('', {data: obj, silent: true}).data;
}
const obj = /* ... */;
const clone = structuralClone(obj);
2
3
4
5
6
浏览器支持度不够。
# 常规的遍历对象并赋值
for..in
+Object.hasOwnProperty()
# 总结
- 如果没有循环对象,并且不需要保留内置类型,则可以使用跨浏览器
JSON.parse(JSON.stringify())
获得最快的克隆性能。 - 如果你想要一个适当的结构化克隆,
MessageChannel
是你唯一可靠的跨浏览器的选择。
# console.log 与内存泄漏
console.log
在 devtools 打开的时候是有内存泄漏的,因为控制台打印的是对象引用,但是不打开 devtools 是不会有内存泄漏的。- 通过
performance.memory.totalJSHeapSize/1024/1024
,配合console.log
打印可以验证。 string
基本类型因为常量池的存在,同样的字符串实际只会创建一次。而new String
的话才会在堆中创建一个对象,然后指向常量池中的字符串字面量。- nodejs 打印的是序列化后的对象,所以没有内存泄露。
- 生产环境是可以使用
console.log
的,没有内存泄漏问题。
# peerDependency
peerDependency 可以避免核心依赖库被重复下载的问题。比如:
...
├── helloWorld
│ └── node_modules
│ ├── packageA
│ ├── plugin1
│ │ └── nodule_modules
│ │ └── packageA
│ └── plugin2
│ │ └── nodule_modules
│ │ └── packageA
...
2
3
4
5
6
7
8
9
10
11
- 此时 helloWorld 本身已经安装了一次 packageA,但是因为因为在 plugin1 和 plugin2 中的 dependencies 也声明了 packageA,所以最后 packageA 会被安装三次,有两次安装是冗余的。
- 如果在 plugin1 和 plugin2 的 package.json 中使用 peerDependency 来声明核心依赖库,例如:
// plugin1/package.json
{
"peerDependencies": {
"packageA": "1.0.1"
}
}
// plugin2/package.json
{
"peerDependencies": {
"packageA": "1.0.1"
}
}
// helloWorld/package.json
{
"dependencies": {
"packageA": "1.0.1"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 此时在主系统中执行
npm install
生成的依赖图就是这样的:可以看到这时候生成的依赖图是扁平的,packageA 也只会被安装一次。
...
├── helloWorld
│ └── node_modules
│ ├── packageA
│ ├── plugin1
│ └── plugin2
...
2
3
4
5
6
7
- 总结:在插件使用 dependencies 声明依赖库的特点:
- 如果用户显式依赖了核心库,则可以忽略各插件的 peerDependency 声明;
- 如果用户没有显式依赖核心库,则按照插件 peerDependencies 中声明的版本将库安装到项目根目录中;
- 当用户依赖的版本、各插件依赖的版本之间不相互兼容,会报错让用户自行修复;
# WebGL
# Vue3diff
# iframe 防止他人嵌套自己的网站
TODO
- 通过 parent 判断 origin
# 几个框架对比总结
- 不同框架编译之后的差异:
- 🚀 React 编译之后是 Jsx 函数返回的虚拟 DOM
- 🚀 Vue 编译之后是 render 函数返回的虚拟 DOM
- 🚀 SolidJS 编译之后返回的真实 DOM 字符串
- 🚀 Svelte 编译之后返回的是真实 DOM 片段
React 由于架构机制决定了每当状态发生改变,从当前组件开始一直到叶子组件重新加载。
Vue 由于给每个组件建立了 watchEffect 监听机制,每当组件依赖的状态发生改变,当前组件重新加载。
SolidJS 和 Svelte 由于在编译之后就确定了当状态发生改变 UI 随之变化的关系,所以仅仅是具体 DOM 的重新加载。
相对来说,react 更新粒度最粗,但是配合 useMemo/memo 之后可以跟 Vue 差不多,Vue 更新粒度中等,SolidJS 和 Svelte 更新粒度相对最细。
# TOTP
基于时间的一次性密码算法(Time-based One-Time Password,简称:TOTP)是一种根据预共享的密钥与当前时间计算一次性密码的算法。
- 前端库:
@nest-public/totp
,默认使用 SHA1 散列算法和 30 秒的时间步长。
# 本地快速启动 server
有时候我们需要给静态页面或文件,启动服务在浏览器中查看。推荐两个启动服务的 npm 包。
npx http-server [path] [options]
# 比如
npx http-server . -p 8090
npm repo http-server 可直达仓库地址
npx serve [path] [options]
# 比如
npx serve . -p 9090
npm repo serve 可直达仓库地址
2
3
4
5
6
7
8
9
10
11
12
# JS 实现函数重载
不同于 TS,JS 没有类型系统
function createOverload() {
const fnMap = new Map();
function overload(...args) {
const keys = args.map((item) => typeof item).join(",");
const fn = fnMap.get(keys);
if (!fn) {
throw new TypeError("没有找到对应的实现");
}
return fn.apply(this, args);
}
overload.addImpl = function (...args) {
const fn = args.pop();
if (typeof fn !== "function") {
throw new TypeError("最后一个参数必须是函数");
}
const key = args.join(",");
fnMap.set(key, fn);
};
return overload;
}
// usage
const getUsers = createOverload();
getUsers.addImpl(() => console.log("查询所有用户"));
const searchPage = (page, size = 10) => console.log("按照页码和数量查询用户");
getUsers.addImpl("number", searchPage);
getUsers.addImpl("string", () => console.log("按照姓名查询用户"));
getUsers();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 画中画
<body>
<video
id="video"
height="500px"
width="800px"
src="../Downloads//Have the superpower of fixing containers.mp4"></video>
<button id="pipButton">Open Picture-in-Picture</button>
<script>
const video = document.getElementById("video");
const pipButton = document.getElementById("pipButton");
pipButton.disabled = !document.pictureInPictureEnabled;
pipButton.addEventListener("click", async () => {
try {
if (video !== document.pictureInPictureElement) {
await video.requestPictureInPicture();
} else {
await document.exitPictureInPicture();
}
} catch (error) {
console.error(error);
} finally {
updateVideoButton();
}
});
video.addEventListener("enterpictureinpicture", () => {
pipButton.textContent = "Exit Picture-in-Picture";
});
video.addEventListener("leavepictureinpicture", () => {
pipButton.textContent = "Enter Picture-in-Picture";
});
function updateVideoButton() {
pipButton.textContent =
video === document.pictureInPictureElement ? "Exit Picture-in-Picture" : "Enter Picture-in-Picture";
}
</script>
</body>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# for 和 forEach 的区别
for
是一个语句,forEach
是一个方法。for
循环是 js 提出时就有的循环方法。forEach
是 ES5 提出的,挂载在可迭代对象原型上的方法,例如Array
Set
Map
。forEach
是一个迭代器,负责遍历可迭代对象。forEach
是负责遍历(Array Set Map
)可迭代对象的,而for
循环是一种循环机制,只是能通过它遍历出数组。- 可迭代对象:ES6 中引入了
iterable
类型,Array Set Map String arguments NodeList
都属于iterable
,他们特点就是都拥有[Symbol.iterator]
方法,包含他的对象被认为是可迭代的iterable
。 for
可以使用break
和continue
,forEach
不能。for
可以使用return
,直接在 Chrome 控制台会报错,用在函数中就行;forEach
中使用 return 的效果类似于 for 循环中的 continue,会跳过当前迭代进入下一个迭代,但不会停止整个 forEach 循环。forEach
属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。- 一定要在
forEach
中跳出循环呢?其实是有办法的,借助try/catch
,在catch
中捕获异常,然后return
,这样就可以实现跳出循环的效果。但是这样做不太好,因为try/catch
会影响性能,所以不推荐这样做。try { arr.forEach((item) => { if (item === 3) { throw new Error("end"); } }); } catch (e) { if (e.message === "end") { return; } }
1
2
3
4
5
6
7
8
9
10
11 for
可以使用let
和const
,forEach
不能。for
可以使用var
,forEach
不能。for
可以使用for...of
,forEach
不能。for
可以使用for...in
,forEach
不能。for
可以使用for await...of
,forEach
不能。for
可以使用for await...in
,forEach
不能。for
可以使用for...await...of
,forEach
不能。- 只要是可迭代对象,调用内部的
Symbol.iterator
都会提供一个迭代器,并根据迭代器返回的next
方法来访问内部,这也是for...of
的实现原理。 - 完整用法:
arr.forEach((self,index,arr) =>{},this)
forEach
删除自身元素,index
不可被重置,在forEach
中我们无法控制index
的值,它只会无脑的自增直至大于数组的length
跳出循环。所以也无法删除自身进行index
重置。- 在实际开发中,遍历数组同时删除某项的操作十分常见,在使用
forEach
删除时要注意。 for
循环可以控制循环起点,forEach
的循环起点只能为 0 不能进行人为干预- 性能比较:
for > forEach > map
在 Chrome 62 和 Node.js v9.1.0 环境下:for
循环比forEach
快 1 倍,forEach
比map
快 20%左右。 - 原因分析
for
:for 循环没有额外的函数调用栈和上下文,所以它的实现最为简单。forEach
:对于 forEach 来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。map
:map 最慢的原因是因为 map 会返回一个新的数组,数组的创建和赋值会导致分配内存空间,因此会带来较大的性能开销。
# 有用的 JS 函数片段
# 如何平滑滚动到元素视图中
const smoothScroll = (element) =>
document.querySelector(element).scrollIntoView({
behavior: "smooth",
});
smoothScroll("#fooBar"); // 平滑滚动到id为fooBar的元素
smoothScroll(".fooBar");
// 平滑滚动到class为fooBar的第一个元素
2
3
4
5
6
7
8
# 如何生成 UUID?
const UUIDGeneratorBrowser = () =>
([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)
);
UUIDGeneratorBrowser(); // '7982fcfe-5721-4632-bede-6000885be57d'
2
3
4
5
6
# 如何获取选定的文本
const getSelectedText = () => window.getSelection().toString();
getSelectedText(); // 'Lorem ipsum'
2
3
# 如何将文本复制到剪贴板
const copyToClipboard = (str) => {
if (navigator && navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(str);
return Promise.reject("The Clipboard API is not available.");
};
2
3
4
# 如何切换全屏模式
const fullscreen = (mode = true, el = "body") =>
mode ? document.querySelector(el).requestFullscreen() : document.exitFullscreen();
fullscreen(); // 将body以全屏模式打开
fullscreen(false); // 退出全屏模式
2
3
4
5
# 如何检测大写锁定是否打开
const el = document.getElementById("password");
const msg = document.getElementById("password-message");
el.addEventListener("keyup", (e) => {
msg.style = e.getModifierState("CapsLock") ? "display: block" : "display: none";
});
2
3
4
5
6
# 如何检查日期是否有效
const isDateValid = (...val) => !Number.isNaN(new Date(...val).valueOf());
isDateValid("December 17, 1995 03:24:00"); // true
isDateValid("1995-12-17T03:24:00"); // true
isDateValid("1995-12-17 T03:24:00"); // false
isDateValid("Duck"); // false
isDateValid(1995, 11, 17); // true
isDateValid(1995, 11, 17, "Duck"); // false
isDateValid({}); // false
2
3
4
5
6
7
8
9
# 如何检查当前用户的首选语言
const detectLanguage = (defaultLang = "en-US") =>
navigator.language || (Array.isArray(navigator.languages) && navigator.languages[0]) || defaultLang;
detectLanguage(); // 'nl-NL'
2
3
4
# 如何检查当前用户的首选颜色方案
const prefersDarkColorScheme = () =>
window && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
prefersDarkColorScheme(); // true
2
3
4
# 如何检查设备是否支持触摸事件
const supportsTouchEvents = () => window && "ontouchstart" in window;
supportsTouchEvents(); // true
2
3
# 在浏览器分析 npm 包
[pkg-size.dev](https://pkg-size.dev/)
,可以直接在浏览器对 npm 包进行分析(包括占用大小、打包大小、间接依赖项等等)。它的目标是让像我们可以更轻松地探索 npm 生态系统。
# 前端监听手机键盘是否弹起
安卓和 ios 判断手机键盘是否弹起的写法是有所不同的:
- IOS 端可以通过
focusin
、focusout
这两个事件来监听
window.addEventListener("focusin", () => {
// 键盘弹出事件处理
alert("ios键盘弹出事件处理");
});
window.addEventListener("focusout", () => {
// 键盘收起事件处理
alert("ios键盘收起事件处理");
});
2
3
4
5
6
7
8
- 安卓只能通过 resize 来判断屏幕大小是否发生变化来判断
由于某些 Android 手机收起键盘,输入框不会失去焦点,所以不能通过聚焦和失焦事件来判断。但由于窗口会变化,所以可以通过监听窗口高度的变化来间接监听键盘的弹起与收回。
const innerHeight = window.innerHeight;
window.addEventListener("resize", () => {
const newInnerHeight = window.innerHeight;
if (innerHeight > newInnerHeight) {
// 键盘弹出事件处理
alert("android 键盘弹出事件");
} else {
// 键盘收起事件处理
alert("android 键盘收起事件处理");
}
});
2
3
4
5
6
7
8
9
10
11
- 因为 ios 和安卓的处理不一样,所以还需要判断系统的代码
const ua = typeof window === "object" ? window.navigator.userAgent : "";
let _isIOS = -1;
let _isAndroid = -1;
export function isIOS() {
if (_isIOS === -1) {
_isIOS = /iPhone|iPod|iPad/i.test(ua) ? 1 : 0;
}
return _isIOS === 1;
}
export function isAndroid() {
if (_isAndroid === -1) {
_isAndroid = /Android/i.test(ua) ? 1 : 0;
}
return _isAndroid === 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 复选框、单选框的点击也会导致
focusin
和focusout
的触发,我们需要处理一下,使其点击复选框、单选框这类标签的时候不触发我们的回调函数
// 主要是通过判断一下当前被focus的dom类型
// document.activeElement.tagName
// tagName为输入框的时候才算触发键盘弹起
const activeDom = document.activeElement.tagName;
if (!["INPUT", "TEXTAREA"].includes(activeDom)) {
console.log("只有");
}
2
3
4
5
6
7
- 当有横屏功能的时候,resize 也会被触发,增加宽度是否有改变的判断,没有改变,才是真正的键盘弹起
//初始化的时候获取一次原始宽度
const originWidth = document.documentElement.clientWidth || document.body.clientWidth;
//结合处理复选框、单选框的点击也会导致`focusin` 和`focusout` 的触发问题的完整回调写法
function callbackHook(cb) {
const resizeWeight = document.documentElement.clientWidth || document.body.clientWidth;
const activeDom = document.activeElement.tagName;
if (resizeWeight !== originWidth || !["INPUT", "TEXTAREA"].includes(activeDom)) {
return (isFocus = false);
}
cb && cb();
}
2
3
4
5
6
7
8
9
10
11
- 使用应当要注意销毁,也需要尽量减少绑定指令的次数,一般在 form 表单上绑定一个,即可监听这个表单下的所有输入框是否触发手机键盘唤起了
# 页面滚动的代码
window.scroll({ top: document.body.scrollHeight, behavior: "smooth" });
window.scrollTo(0, document.body.scrollHeight);
2
3
#
#
#
#
#