fox的博客

github地址 -> https://github.com/FoxDaxian


  • Home

  • Archives

js <=> ast

Posted on 2021-05-09 In 原理

语言一般分为编译型语言和解释性语言

  • 编译型语言:先编译在执行,例如做好饭在吃
    大致步骤为:词法分析 -> 语法分析 -> 语义检查 -> 代码优化和字节码生成
  • 解释性语言:涮火锅
    大致步骤为:词法分析 -> 语法分析 -> 语法树,然后直接解释执行了

加深理解,学习了一下用js写一个解析器,转换成ast抽象结构树,在写编译器转换成汇编语言的过程。
包括下面几个功能:

  • 解析
  • 汇编代码生成
  • 系统调用

解析

解析函数应该返回ast,一个代表输入的数据结构,比如我们想要

1 (+ 2 3))```转换成```['+', 1 ['+', 2, 3]]```
1
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

```javascript
module.exports.parse = function parse(program) {
const tokens = [];
let currentToken = '';

for (let i = 0; i < program.length; i++) {
const char = program.charAt(i);

switch (char) {
case '(':
// 递归
const [parsed, rest] = parse(program.substring(i + 1));
// 把已经解析好的塞入数组中
tokens.push(parsed);
// 置空剩余参数,因为递归
program = rest;
i = 0;
break;
case ')':
// 去掉多余的右括号
tokens.push(+currentToken || currentToken);
// return 直接阻止函数继续运行,直接返回
return [tokens, program.substring(i + 1)];
break;
case ' ':
// 遇到空格则全部塞入tokens
tokens.push(+currentToken || currentToken);
currentToken = '';
break;
default:
currentToken += char;
break;
}
}
// 如果第二个参数不为'',这解析过程有误
return [tokens, '']
}

简单测试:

1
JSON.stringify(parse('(- 2 (+ 4 5))'), null, 0) // [[["-",2,["+",4,5]]],""]

汇编相关

汇编使我们能使用的最低级的编程语言,是兼顾可读性和1:1对应相应二进制的,cpu可直接解释的语言。
可使用

1
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
汇编主要的数据结构是寄存器(cpu存储的临时变量)和程序堆栈。程序中的每个函数都会访问相同的寄存器,但是也有一些更实用更耐用的寄存器,比如```RAX```、```RDI```等等。
来了解一下我们会用到的一些汇编功能:
- MOV:移动寄存器内容到另外一个,或者存储字面量到寄存器中
- ADD:合并两个寄存器的内容到第一个寄存器
- PUSH:将寄存器中的内容放置到堆栈中
- POP:移除堆栈里最顶层的内容,并存储到寄存器中
- CALL:访问堆栈中的一部分,并且开始执行
- RET:访问并调用一个堆栈并返回调用之后的下一个指令的评估
- SYSCALL:和call差不多,不过有```kernel```处理


#### 汇编代码生成

话不多说,上代码
```javascript
// 代码转换部分
function emit(depth, code) {
const indent = new Array(depth + 1).map(() => '').join(' ');
console.log(indent + code);
}

function compile_argument(arg, destination) {
// 如果是数组,则递归
if (Array.isArray(arg)) {
compile_call(arg[0], arg.slice(1), destination);
return;
}

// 否则直接存储代码
emit(1, `MOV ${destination}, ${arg}`);
}

const BUILTIN_FUNCTIONS = { '+': 'plus' };
const PARAM_REGISTERS = ['RDI', 'RSI', 'RDX'];

function compile_call(fun, args, destination) {
// 入栈
args.forEach((_, i) => emit(1, `PUSH ${PARAM_REGISTERS[i]}`));
// 递归
args.forEach((arg, i) => compile_argument(arg, PARAM_REGISTERS[i]));
// 执行部分
emit(1, `CALL ${BUILTIN_FUNCTIONS[fun] || fun}`);
// 出栈
args.forEach((_, i) => emit(1, `POP ${PARAM_REGISTERS[args.length - i - 1]}`));
// 如果提供,这转移到相应的寄存器
if (destination) {
emit(1, `MOV ${destination}, RAX`);
}

emit(0, ''); // 优化格式
}

function emit_prefix() {
// 常规前缀
emit(1, '.global _main\n');

emit(1, '.text\n');

emit(0, 'plus:');
emit(1, 'ADD RDI, RSI');
emit(1, 'MOV RAX, RDI');
emit(1, 'RET\n');

emit(0, '_main:');
}

const os = require('os');

const SYSCALL_MAP = os.platform() === 'darwin' ? {
'exit': '0x2000001',
} : {
'exit': 60,
};

function emit_postfix() {
// 常规后缀
emit(1, 'MOV RDI, RAX'); // Set exit arg
emit(1, `MOV RAX, ${SYSCALL_MAP['exit']}`); // Set syscall number
emit(1, 'SYSCALL');
}

module.exports.compile = function parse(ast) {
emit_prefix();
compile_call(ast[0], ast.slice(1));
emit_postfix();
};

简单测试:

1
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
node ulisp.js '(+ 3 (+ 2 1))'
.global _main

.text

plus:
ADD RDI, RSI
MOV RAX, RDI
RET

_main:
PUSH RDI
PUSH RSI
MOV RDI, 3
PUSH RDI
PUSH RSI
MOV RDI, 2
MOV RSI, 1
CALL plus
POP RSI
POP RDI
MOV RSI, RAX

CALL plus
POP RSI
POP RDI

MOV RDI, RAX
MOV RAX, 0x2000001
SYSCALL

整合并调用

我们可以将生成的汇编代码输出到文件中,并使用gcc进行调用

1
2
3
4
5
$ node ulisp.js '(+ 3 (+ 2 1))' > program.S
$ gcc -mstackrealign -masm=intel -o program program.s
$ ./program
$ echo $?
6

大概就是这样,中的来说可以对编译语言有一个浅显的理解,或许日后会用到
参考链接

qps和tps

Posted on 2021-05-09 In 原理

几个概念

  • 吞吐率(RPS)
    计算公式: 吞吐率 = 总请求数 / 处理这些请求的总完成时间
  • 最大吞吐率(QPS)
    计算公式: qps = 请求查询数 / 秒
    每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准
  • 并发连接数
    就是服务器某个时刻所接受的所有请求数目
  • 并发用户数
    一个用户可能产生多个回话,所以并发用户数和并发连接数并不重复,并发用户数是指服务器某个时刻所能接受的用户数(类似uv)
  • TPS(每秒传输的事物数)
    TPS也就是单位时间内,服务器能处理的最大事务数
    一个事务是指一个客户机想服务器发送请求,然后服务器做出反应并进行响应的过程

概念浅析

  • QPS: Queries Per Second,意思是 每秒查询率,是一台服务器每秒能够响应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
  • TPS: Transactions Per Second的缩写,也就是事物数/每秒,这个完整的事务包括了用户请求服务器、服务器内部处理、服务器返回信息给用户三个过程。它是软件测试结果的衡量单位,一个事务是指一个客户端,想服务器发送请求,然后服务器做出反应的过程。客户端发送请求时开始计时,收到服务器响应后,结束计时,以此来计算使用的时间和完成的事务个数,最终利用这些信息来估计得分。

计算QPS

  • 原理:每天80%的访问集中在20%的时间里,这20%的时间叫做峰值时间。
  • 公式: (总pv数 * 80%) / (每天秒数 * 20%) = 峰值时间每秒请求数(QPS)
  • 机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器数量
    问: 每天300w PV 的单台机器上,这台机器需要多少QPS
    答: (3000000 * 0.8) / ( 86400 * 0.2) = 139(QPS) 至少需要 139 QPS
    问: 如果一台机器的QPS是58,需要几台机器支持
    答: 139 / 58 = 3 至少三台

如何对node进行评测、压测

node可读流

Posted on 2021-05-09 In 初出茅庐

概念

流(stream)是 Node.js 中处理流式数据的抽象接口。 stream 模块用于构建实现了流接口的对象。

Node.js 提供了多种流对象。 例如,HTTP 服务器的请求和 process.stdout 都是流的实例。

流可以是可读的、可写的、或者可读可写的。 所有的流都是 EventEmitter 的实例。

访问 stream 模块:

1
const stream = require('stream');

尽管理解流的工作方式很重要,但是 stream 模块主要用于开发者创建新类型的流实例。 对于以消费流对象为主的开发者,极少需要直接使用 stream 模块。

Node.js 中有四种基本的流类型:

  • Writable - 可写入数据的流(例如 fs.createWriteStream())。
  • Readable - 可读取数据的流(例如 fs.createReadStream())。
  • Duplex - 可读又可写的流(例如 net.Socket)。
  • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())。

此外,该模块还包括实用函数 stream.pipeline()、stream.finished() 和 stream.Readable.from()。

盗图

如何获取内存中的流

可写流和可读流都会在内部的缓冲器中存储数据,可以分别使用的 writable.writableBuffer 或 readable.readableBuffer 来获取。
细节

可读流

两种模式

  • 流动模式(不用打,自己动):数据自动从底层系统读取,并通过EventEmitter接口的事件尽可能快的提供刚给应用程序
  • 暂停模式(打一下,动一下):必须显示调用stream.read()读取数据块

其实,所有可读流初始的时候都处于暂停模式,不过可以通过以下方法切换到流动模式

  • 添加 data 事件句柄。
  • 调用 stream.resume() 方法。
  • 调用 stream.pipe() 方法将数据发送到可写流。

当然,能切到暂停模式,肯定也能切到流动模式

  • 如果没有管道目标,则调用 stream.pause()
  • 如果有管道目标,则移除所有的管道目标。调用stream.unpipe()可以移除多个管道目标。

为了向后兼容,移除 ‘data’ 事件句柄不会自动地暂停流。

如果有管道目标,一旦目标变为 drain 状态并请求接收数据时,则调用 stream.pause() 也不能保证流会保持暂停模式。

如果可读流切换到流动模式,且没有可用的消费者来处理数据,则数据将会丢失。 例如,当调用 readable.resume() 时,没有监听 ‘data’ 事件或 ‘data’ 事件句柄已移除。

添加 readable 事件句柄会使流自动停止流动,并通过 readable.read() 消费数据。 如果 readable 事件句柄被移除,且存在 data 事件句柄,则流会再次开始流动

demo

流动模式

1
2
3
4
5
6
7
8
9
const fs = require('fs')
const path = require('path')
const rs = fs.createReadStream(path.join(__dirname, './1.txt'))

rs.setEncoding('utf8')

rs.on('data', (data) => {
console.log(data)
})

暂停模式

1
2
3
4
5
6
7
8
9
10
const fs = require('fs')
const path = require('path')
const rs = fs.createReadStream(path.join(__dirname, './1.txt'))

rs.setEncoding('utf8')

rs.on('readable', () => {
let d = rs.read(1) // 要读取的数据的字节数。
console.log(d)
})

read方法: 参数 可选 [size]

如果没有指定 size 参数,则返回内部缓冲中的所有数据。

使用 readable.read() 处理数据时, while 循环是必需的。
read方法消耗的是内存中的数据

当read方法返回的是null的时候,会触发 流监听的 end 事件
不使用read消耗内存中的流数据,则不会触发end

预览一波

遇到的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
process.stdin.setEncoding('utf8');

process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
process.stdout.write(`数据: ${chunk}长度${chunk.length}\n`);
}
});

process.stdin.on('end', () => {
process.stdout.write('结束\n');
process.exit(1);
});

上面的代码作用是读取用户在terminal上的输出,然后输出内容和长度,但是执行的时候,总是无法执行到end,不仅如此,就算内容为空,返回的长度也不为0,这让我很疑惑。

最后发现是坚挺的end事件,只有当read方法返回为null的时候才会触发,所以没有触发,而问题就在于返回的内容是换行符,不同系统下换行符不一样,mac下是\n,所以我们需要处理一下read返回的内容,然后手动end
查看字符串中回车符可以使用:

1
console.log(JSON.stringify(chunk));

修正后的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
process.stdin.setEncoding('utf8');

process.stdin.on('readable', () => {
let chunk;
while ((chunk = process.stdin.read()) !== null) {
chunk = chunk.replace(/\n/g, '');
if (!chunk.length) {
console.log('输入为空');
return process.stdin.emit('end');
}
process.stdout.write(`数据: ${chunk}长度${chunk.length}\n`);
}
});

process.stdin.on('end', () => {
process.stdout.write('结束\n');
process.exit(1);
});

执行结果:

中文文档

webpack - 优化阻塞渲染的css

Posted on 2021-05-09 In 原理
1
2
3
随着浏览器的日新月异,网页的性能和速度越来越好,并且对于用户体验来说也越来越重要。
现在有很多优化页面的办法,比如:静态资源的合并和压缩,code splitting,DNS预读取等等。
本文介绍的是另一种优化方法:首屏阻塞css优化
原理:

首先我们了解一下页面的基本渲染流程(参考):
webkit渲染过程:
webkit渲染过程
Gecko渲染过程:
gecko渲染过程
那么,为什么要做这种优化呢?上面的流程图就是原因:首先解析html生成dom树,同时解析css生成css树,之后结合两者生成渲染树,然后渲染到屏幕上。不但如此,如果css后面有其他javascript,并且css加载时间过长,也会阻塞后面的js执行,因为js可能会操作dom节点或者css样式,所以需要等待render树完成。那么,如果我们能优化css,那么就能大大减少页面渲染出来的时间,从而提升pv,增加黏性,走向编码巅峰。。。


怎么做呢:

目前我知道的比较实用的办法是webpack集成critical,critical是一个提取关键css,内联到html中,并且使用preload和noscript兼容加载非关键css的工具。
那么,我们开门见山,直接从webpack配置开始:

1
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
42
43
44
45
46
47
48
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 创建html来服务你的资源
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 提取css到分离的文件,需要webpack4
const HtmlCriticalWebpackPlugin = require('html-critical-webpack-plugin'); // 集成critical的html-webpack-plugin版本
const path = require('path');

// 用于设置Chromium,因为Chromium使用npm或者yarn经常有问题
process.env['PUPPETEER_EXECUTABLE_PATH'] =
'你电脑中的Chromium地址';

module.exports = {
mode: 'none',
module: {
rules: [
{
test: /\.css$/,
// 使用MiniCssExtractPlugin.loader代替style-loader
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({ template: './index.html' }),
new MiniCssExtractPlugin({}),
new HtmlCriticalWebpackPlugin({
base: path.resolve(__dirname, 'dist'),
src: 'index.html',
dest: 'index.html',
inline: true,
minify: true,
extract: true,
width: 375,
height: 565,
// 确保调用打包后的JS文件
penthouse: {
blockJSRequests: false
}
})
]
};

然后是html文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div class="div"></div>
<h2>hello world</h2>
<div class="mask">这是一个弹窗</div>
</body>
</html>

接着是css文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.div {
width: 200px;
height: 100vh;
background-color: red;
}
h2 {
color: blue;
}
.mask {
width: 500px;
height: 500px;
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
background-color: yellowgreen;
}

运行webpack后,查看打包后的html文件:

1
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
// 省略...
<style>
.div {
width: 200px;
height: 100vh;
background-color: red;
}
.mask {
width: 500px;
height: 500px;
display: none;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
background-color: #9acd32;
}
</style>
<link
href="main.80dc2a9c.css"
rel="preload"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link href="main.80dc2a9c.css" rel="stylesheet"/></noscript>
// 省略...

代码仓库在此,点击fork进行实战练习

可以看到,h2标签的css样式没有出现在内联style里,而是出现在main.[hash].css中,因为它不再所设置首屏范围内,这就是所谓的首屏css优化。

相关内容

在上面打包后的html文件里,我们看到了有一个link内有rel="preload" as="style"字段,紧接着下面就有一个noscript标签,这两个是做什么的呢?

  • rel="preload" as="style": 用于进行页面预加载,rel="preload"通知浏览器开始获取非关键CSS以供之后用。其关键在于,preload不阻塞渲染,无论资源是否加载完成,浏览器都会接着绘制页面。并且,搭配as使用,可以指定将要预加载内容的类型,可以让浏览器:
    1. 更精确地优化资源加载优先级。
    2. 匹配未来的加载需求,在适当的情况下,重复利用同一资源。
    3. 为资源应用正确的内容安全策略。
    4. 为资源设置正确的 Accept 请求头。
  • noscript:如果页面上的脚本类型不受支持或者当前在浏览器中关闭了脚本,则在HTML <noscript> 元素中定义脚本未被执行时的替代内容。换句话说,就是当浏览器不支持js脚本或者用户主动关闭脚本,那么就会展示noscript里的内容,而critical则是利用这一点做了向后兼容
总结

利用critical可以大大提高页面渲染速度,但是由于其使用puppeteer,所以下载安装比较麻烦,上面的webpack中使用设置env中puppeteer位置的方法解决了这一问题。

文中如若有不对的地方,还望之处,共同交流。

正则表达式

Posted on 2021-05-09 In 介绍

所有的语言的正则表达式还有一些更强大的功能,比如

1、子表达式的索引和回溯
2、回溯引用在replace中的应用
3、(肯定 | 否定)向前查找
4、(肯定 | 否定)向后查找 ===> JS不支持
5、条件查找 ===>JS不支持 Orz……

下面说的均是JS的正则


一、子表达式的索引和回溯

JS中用括号括起来的大部分都可以成为一个子表达式(按照先后顺序从1到n),而整个正则则是第0个子表达式

例如:var reg = /(好).*?\1/g; ===> 这里的“(好)”就是正个reg的 第一个 子表达式,后面的“\1”就代表着前面的第一个子表达式,因为是 1 嘛!

让我们来匹配点什么:var str =”你好吗?我很好!”;

str.match(reg) ===> 返回结果:[“好吗?我很好”]

这里看到了,从第一个“好”,匹配到第二个好,这也就是我理解的子表达式的索引和回溯(我是这么叫它的)。

补一句,reg里面的“*?”是懒惰匹配,就是尽量少的匹配,懒惰匹配的原理就是在修饰符的后面几个“?”,因为“?”是匹配0次或者1次嘛,so你懂得。

二、回溯引用在replace中的应用

直接从简单的例子入手:

正则表达式:var reg = /-(\w)/g;

匹配str:var str =”my-love”;

输出:console.log(str.replace(reg,function($0,$1) {

console.log($0);

console.log($1);

}));

结果如下图:

控制台结果

个人分析图如下:

分析图

相信看了上面的分析,大家也能明白个七八,只要在replace中的函数中 return $1.toLowerCase(),是不是就完成了将带横线的字符转为驼峰字符的功能?大家试一下就会了解。

这里面的“$1”我理解为正则里面的“/1”,也就是说和“/1”的思路是差不多的,只不过应该用到replace中,不过好像也只能用到replace中,其他方面我还没接触到。

三、(肯定 | 否定)向前查找

先问大家一个问题,如果给大家一段话,里面有数字、中文和40$,要求匹配$前面的数字并且不能匹配后面没有$符号的数字,大家会如何匹配呢?(先想一想)

就上面的问题来举例子,从例子入手:

匹配的str:var str =”我出50$买灵能100%第二季”;

正则表达式:var reg = /\d+(?=$)/g;

输出:console.log(str.match(reg));

结果为:[“50”]

这个结果就是我们想要的,而“(?=$)”就是所谓的(肯定)正向查找。

我的理解是:“(?=$)”的“?=”的“=”后面是要匹配的关键字,如果匹配到,就用“\d+”(也就是(?=$)之前的)来匹配关键字之前的字符串或者其他。但是最后不会包含这个关键字哦。现在看起来是不是特别贴合这个名字“向前查找”。

那么(否定)向前查找呢?

还是那个例子只不过把正则表达式换一下

正则表达式:var reg = /\d+(?!$)/g; ===> 注意 这里 将“?=”变为“?!”

返回结果:[“5”, “100”]

为什么是这个结果呢?因为是否定的,也就是取反,从“?!”中的“!”可以看出,匹配的是 数字 加 $ 之外的数字,那么为什么会返回5呢,这个我猜想是懒惰匹配的,所以会匹配“0$”,将前面的“5”抛弃了,并且返回了被匹配字符串的所以其他数字,就是这样喵。

四、(肯定 | 否定)向后查找 和 条件查找不支持,所以就没必要说了。有兴趣的可以自己去看看。

以上内容均属个人简洁,不对的地方望指出,最后祝大家在前端之路上越来越顺利。

没有了~

rem移动端适配

Posted on 2021-05-09 In 原理

移动端适配,老生常谈的问题,这次再谈一次。
闲话少说,直奔正题。

一些像素概念

  1. 物理像素:即实际的每一个物理像素,也就是移动设备上每一个物理显示单元(点)
  2. 设备逻辑像素(css中的px):可以理解为一个虚拟的相对的显示块,与物理像素有着一定的比例关系,也就是下面的设备像素比
  3. 设备像素比(dpr):= 物理像素 / 设备独立像素(px)
    如果dpr为1的话,那么1px = 1物理像素,x轴y轴加起来就是1
    如果dpr为2的话,那么1px = 2物理像素,x轴y轴加起来就是4

    以此类推
    在js中可以通过window.devicePixelRatio获取当前设备的dpr。

这里说明一下,无论dpr多大,1px的大小通常来说是一致的,这也就意味着,随着dpr的增大,物理像素点会越来越小,这样才能容纳更多的物理像素,才能更高清,更retina


说完基本概念,来说一下几个问题:

retina屏图片模糊

首先普及一下位图像素:一个位图像素是图片的最小数据单元,每一个单元都包含具体的显示信息(色彩,透明度,位置等等)
那为什么在dpr高的retina屏上反而会模糊呢?看图~

在1dpr的屏幕上,位图像素和物理像素一一对应没什么问题,但是在retina屏上,由于一个px由4个甚至更多的物理像素组成,并且单个位图像素不能进一步分割,所以会出现就近取色的情况,如果取色不均,那么就会导致图片模糊。
对于这种情况,只能采用@2x、@3x这样的倍图来适配高清展示,这样侧向说明了为什么照着iphone6做的ui稿不是375,而是750的问题。
虽然这样在dpr为1的屏幕上会导致1个物理像素上有4个位图像素,但是这种情况的取色算法更优,影响不大,不做讨论。

1px的粗细问题

由于1px的实际大小是一样的,只是里面的物理像素数量不同,所以如果直接写1px是没问题的,不会出现粗细不同的情况,但是这样一来retina的优势也rem的作用也就没了,其实还是dpr的问题,dpr为1,那么1px就是一个物理像素,但是在retina中。1px实际可能有4、9个物理像素,ui想要的其实是1个物理像素,而不是1px,不过由于不是素所有的手机都能适配0.x,所以曲线救国,采用scale缩放或者设置meta都可以


viewport

三个概念:

  1. layout viewport
  2. visual viewport
  3. ideal viewport
layout viewport

最开始,pc上的页面是无法再移动端正常显示的,因为屏幕太小,会挤作一团,所以就有了viewport的概念,又称布局视口(虚拟视口),这个视口大小接近于pc,大部分都是980px。

visual viewport

有了布局视口,还缺一个承载它的真是视口,也就是移动设备的可视区域-视觉视口(物理视口),这个尺寸随着设备的不同也有不同。这样在视觉视口中创建了一个布局视口,类似overscroll:scroll;这样,可以通过滚动拖拽、缩放扩大进行较好的访问体验。

ideal viewport

像上面的体验在早些年可能比较多,但是近几年几乎很少了,还是归咎于用户体验,所以,我们还需要一个视口-理想视口(同样是虚拟视口),不过这个理想视口的大小是等于布局视口的,这样用户就能得到更好的浏览体验。


一个特性

viewport有六种可以设置的常用属性:

  1. width:定义layout viewport的宽度,如果不设置,大部分情况下默认是980

  2. height:非常用

  3. initial-scale:可以以某个比例将页面缩放\放大,你也可以用它来设置ideal viewport:

    1
    <meta name='viewport' content='initial-scale=1' />
  4. maximum-scale:限制最大放大比例

  5. minimum-scale:限制最小缩小比例

  6. user-scalable:是否允许用户放大\缩小页面,默认为yes


rem适配方案

先说原理,通过meta修正1px对应的物理像素数量,在根据统一的设计稿来生成html上的动态font-size,根据dpr构造字体等误差较大的样式的mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第一版:
function initRem() {
const meta = document.querySelector('meta[name="viewport"]');;
const html = document.documentElement;
const cliW = html.clientWidth;
const dpr = window.devicePixelRatio || 1;
meta.setAttribute('name', 'viewport');
meta.setAttribute(
'content',
`width=${cliW * dpr}, initial-scale=${1 /
dpr} ,maximum-scale=${1 / dpr}, minimum-scale=${1 /
dpr},user-scalable=no`
);
html.setAttribute('data-dpr', dpr);
// 这样计算的好处是,你可以直接用ui的px/100得到的就是rem大小,方便快捷,无需mixin
html.style.fontSize = 10 / 75 * cliW * dpr + 'px';
}
initRem();
window.onresize = window.onorientationchange = initRem();

对于引入的第三方ui组件,需要使用px2rem转换工具去做整体转换,比如postcss-pxtorem

参考链接

移动端高清,多屏适配
viewport详解
完全理解px dpr dpi dip

RGB <=> HSL

Posted on 2021-05-09 In 原理

首先我们需要了解两种颜色模式:

  • RGB
  • HSL

RGB

顾名思义,red,green,blue的首字母缩写。RGB是添加剂颜色系统,意味着哪个色值高,最终颜色会更趋向与哪个。如果色值相等,那么趋向于灰色,为0则是黑色,255则是白色。
一种替换方案是你可以用十六进制表示,也就是说将各个色值从十进制转换成十六进制。比如:

rgb(50, 100, 0) => #326400

不过,RGB很难阅读,或者直观的知道最终的颜色,所以又有了HSL,一种更直观的颜色表示形式。

HSL

同样,HSL也是首字母缩写:hue,saturation,light。

  • hue:色相 - 色彩的基本属性,单位是角度,所以,我们可以用一个圆环表示出所有的色相
    12
  • saturation:饱和度 - 色彩的纯度,值越高,色彩越纯越浓,越低,色彩越灰越淡。
    12
  • light:亮度 - 色彩的明暗程度,值越高,月白,直到变成白色,反之变成黑色。该值优先级最高,可以直接影响前两者。。
    12

颜色模式之间的转换

RGB和HSL都可以将颜色分解成多个属性,要想颜色语法转换,我们需要计算他们的属性。
除了hue,其他值都可以用百分比表示,下面的函数中,这些百分比将有小数表示。
不过我们不会深入数学公式,只会简单的了解一下,然后转换成js代码。

RGB 转 HSl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;

if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}

return [h, s, l];
}

HSL 转 RGB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function hslToRgb(h, s, l) {
var r, g, b;

if (s == 0) {
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}

var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}

return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

这样我们就可在RGB和HSl之间进行任意转换,有什么奇怪的需求也就没问题啦
比如这个工具

参考链接

  • 学会调色,从理解HSL面板开始
  • JS HEX十六进制与RGB, HSL颜色的相互转换

移动端长列表滚动优化

Posted on 2021-05-09 In 原理

背景

一个滚动列表,无论是pc端还是移动端,一旦列表内容过长,上千甚至上万,对浏览器的性能肯定是会有影响的。这个影响的根本大部分是在渲染,因为重排相对于重绘更消耗性能。甚至页面初始化后,会显示很长时间的loading,这种体验是很差的。

那有没有办法优化这个问题呢?这个就是这次的主题,无限滚动列表。
实现的方式有很多,有好有坏,由于刚开始没有参考资料,自己写了一版。
大致原理是,计算当前视口的高度,获取滚动的每一项的高度,用视口高度/每一项的高度,就得到了当前可以容纳的最大项。默认进入页面的时候,第一项肯定是0,然后加上最大项,这就是我们真正需要渲染的数量。然后我们需要给当前所有列表的容器加上一个动态高度,这样才能撑起滚动的元素。
容器里面的元素给绝对定位,然后通过translateY将决定定位的元素恢复到正确的位置上,当我们滚动的时候,使用 (当前item的索引 + startindex - buffer) * itemHeight 来获取正确translateY
这样,就算你有一亿条数据,也仅仅渲染这几条,当然不会卡顿。

但是正如上图所示,如果这时候你向下滚动,由于只渲染了这几条,所以当你滚动的时候下面是什么都没有,是一片白的。那么如何解决这个问题呢?
我们需要根据滚动距离动态更新第一项的索引,初始化的时候是0,如果每一项高度是20,那么滚动到21的时候,第一项的索引就编程了1,整体都往后移动了一个,这样列表就增量更新了当前视口的列表,但是渲染的数量还是之前那么多。

这样就完成了最基本的无限滚动列表,不过,如果滚动稍微快一点的话,还是有空白,因为增量更新的还没来得及渲染完毕,所以这个时候我们需要前后都加上buffer列表项,以适应这种情况。

当滚动开始的时候就会提前渲染增量部分。
不过,当你滚动更快的时候,还是会有空白的部分,所以需要实现自定义scroll行为,控制用户滚动速度,即可。

snabbdom

Posted on 2021-05-09 In 原理浅析

什么是虚拟 dom

总所周知,操作 dom 有性能成本,而损耗性能的关键就是操作过程中造成的重绘、重排,所以如果我们能减少重绘、重排,就能提升 web 性能,进而改善用户体验,虚拟 dom 也就这么产生了。

虚拟 dom 其实就是一个用来描述 Dom 节点的 json 对象,比如 snabbdom 中的声明是这样的:

1
2
3
4
5
6
7
8
interface VNode {
sel: string | undefined; // 选择器 tag + id + classnames,也是sameDom判断条件之一
data: VNodeData | undefined; // 看下面
children: Array<VNode | string> | undefined; // 子元素们(与text冲突)
elm: Node | undefined; // Vnode对应的真实Dom
text: string | undefined; // 子文本节点(与children冲突)
key: Key | undefined; // sameDom判断条件之一
}

其中 VNodeData 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
hero?: Hero;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: Array<any>; // for thunks
[key: string]: any; // for any other 3rd party module
}

上面的结构就是一个个虚拟 dom,东西少,很清晰,也正是因为这种结构的产生,进而衍生了服务端渲染,因为虚拟 dom 不依赖浏览器的 Dom 相关内容,所以可以在几乎任何环境下共存,搞不好以后还能出个 css 同构呢。

剩下的就是如果操作虚拟 dom,进而刷新浏览器的 ui,snabbdom 中,核心方法暂且说成两个,一个是 patch, 一个是 h。

方法解析

h

一个用来生成 Vnode 的方法,snabble 中定义了使用案例

1
2
3
4
function h(sel: string): VNode;
function h(sel: string, data: VNodeData): VNode;
function h(sel: string, children: VNodeChildren): VNode;
function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;

方法内的内容很比较简单,通过对函数参数个数的判断,来取到对应的所需参数,然后调用 Vnode 方法,返回创建的 Vnode 实例。

1
2
3
4
5
6
7
8
9
10
function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}

patch

功能主要是三个,比对两个 Vnode 的差异,然后赋值 Vnode.elm 和更新到 html 上,并调用注入的全局钩子

  • 比较 Vnode 的差异
    使用的是 updateChildren,该方法值取新的和旧的 Vnode 的 child 的 startIdx 和 endIdx,然后进行同级别的比较,之所以这么做是因为对于浏览器的场景是适用的,具体细节不啰嗦,也有很多分享,建议自己去看代码,最后我会把带有注释的代码贴上来,做下记录和分享。
  • 赋值并更新 html
    这块更简单了,就是调用 createElm 方法,然后赋值给 Vnode,之后调用 htmldomapi 里面封装好的各种 DOM 操作方法,进行 dom 的增删改查,updateChildren 中也有一些增删改查的操作。
  • 调用全局钩子
    这里的钩子是 snabbdom.bound.js 中挂载的,将 用于操作Vnode.data上属性的钩子方法 挂载到 snabbdom.js 中
1
2
3
4
5
6
7
8
9
10
11
// snabbdom.bound.js
var patch = init([ // Init patch function with choosen modules
attributesModule,
classModule,
propsModule,
styleModule,
eventListenersModule
]) as (oldVNode: any, vnode: any) => any;

// attributesModule.js
export const attributesModule = {create: updateAttrs, update: updateAttrs} as Module;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// snabbdom.js

// 全局钩子们
const hooks: (keyof Module)[] = [
"create",
"update",
"remove",
"destroy",
"pre",
"post"
];
// 声明全局容器
let cbs = {} as ModuleHooks;
// 循环hooks
for (i = 0; i < hooks.length; ++i) {
// 设置cbs的keys,对应value是数组
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
// 取modules中的每一项中的当前钩子,然后全部push到cbs中的对应key中
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}

typescript类型学习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function fn (modules: Array<Partial<Module>>){}
// 首先 <> 是泛型,所以 Array<>代表是一个数组的类型
// 其次Partial代表 => type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
// 此处只能卧槽并配以一个demo来解释一下,其中Module是对象,如下
// {
// 可选的Module中的key?: module当前key对应的value | undefined
// },
// ...
// 上面就是Partial代表的意思,大白话解释一下就是,遍历xxx,key为其中的每一个值,因为有问号,所以转换可选,即不一定必须与xxx中的key一一对应,对应value是可选的

// 整个连起来就是: Array<Partial<Module>> =>

// [
// {
// 可选的Module中的key: module当前key对应的value | undefined
// },
// ...
// ]

snabbdom.js 注释版(不是很长,但是注释比较随性,所以看起来可能不是很舒服)

1
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
/* global module, document, Node */
// 钩子函数的type和interface
import { Module } from "./modules/module";
// 定义了一些type和interface
import { Hooks } from "./hooks";
import vnode, { VNode, VNodeData, Key } from "./vnode";
import * as is from "./is";
import htmlDomApi, { DOMAPI } from "./htmldomapi";

function isUndef(s: any): boolean {
return s === undefined;
}
function isDef(s: any): boolean {
return s !== undefined;
}

type VNodeQueue = Array<VNode>;

const emptyNode = vnode("", {}, [], undefined, undefined);

// 通过 key 和 选择器 判断说相同
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

function isVnode(vnode: any): vnode is VNode {
return vnode.sel !== undefined;
}

type KeyToIndexMap = { [key: string]: number };

type ArraysOf<T> = {
[K in keyof T]: (T[K])[];
};

// Module {
// pre: PreHook;
// create: CreateHook;
// update: UpdateHook;
// destroy: DestroyHook;
// remove: RemoveHook;
// post: PostHook;
// }

// type[] => [a: type, b: type, ...]
type ModuleHooks = ArraysOf<Module>;
// ModuleHooks => {
// pre: [prefn1, prefn2],
// ...
// }

// 遍历 child Vnodes 返回一个 键为 key value 的当前 索引的 对象
function createKeyToOldIdx(
children: Array<VNode>,
beginIdx: number,
endIdx: number
): KeyToIndexMap {
let i: number,
map: KeyToIndexMap = {},
key: Key | undefined,
ch;
for (i = beginIdx; i <= endIdx; ++i) {
ch = children[i];
if (ch != null) {
key = ch.key;
if (key !== undefined) map[key] = i;
}
}
return map;
}

const hooks: (keyof Module)[] = [
"create",
"update",
"remove",
"destroy",
"pre",
"post"
];

export { h } from "./h";
export { thunk } from "./thunk";

// 首先 Array<Partial<Module>> 中的 <> 是泛型,其次 Partial<xxx> 是关键字,相当于 [key in keyof xxx]?: xxx[key] | undefined,
// 大白话解释一下就是,遍历xxx,key为其中的每一个值,因为有问号,所以转换可选,即不一定必须与xxx中的key一一对应,对应value是可选的
// 所以Array<Partial<Module>>的意思就是
// [
// {
// 可选的Module中的key: module当前key对应的value | undefined
// },
// ...
// ]
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
// as 或者 <> 表明编码者明确知道该变量的类型,并指定
let i: number,
j: number,
cbs = {} as ModuleHooks;

// 获取操作dom的所有api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

for (i = 0; i < hooks.length; ++i) {
// cb[hooks中的每一个] = []
cbs[hooks[i]] = [];
// modules:
// [{
// create: fn
// }]
// 遍历modules,将数组的每一项(key为hooks其一)中的每一项统一添加到cbs中
// 结果为 cbs
// {
// create: [fn1, fn2]
// }
for (j = 0; j < modules.length; ++j) {
// 判断传入的参数modules中有没有当前hooks,有的话则push
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}

// 返回vnode描述
function emptyNodeAt(elm: Element) {
const id = elm.id ? "#" + elm.id : "";
const c = elm.className ? "." + elm.className.split(" ").join(".") : "";
return vnode(
api.tagName(elm).toLowerCase() + id + c,
{},
[],
undefined,
elm
);
}

// 通过childElm的父元素 移除childElm
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}

// 根据 vnode 创建真实的 dom
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
// data 是 描述 DOM Node 节点的对象
let i: any,
data = vnode.data;
if (data !== undefined) {
if (isDef((i = data.hook)) && isDef((i = i.init))) {
// 调用hooks的init方法
i(vnode);
data = vnode.data;
}
}
// VNode的选择器,nodeName+id+class的组合
let children = vnode.children,
sel = vnode.sel;
// 创建html注释
if (sel === "!") {
// <!-- <div></div> -->
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text as string);
} else if (sel !== undefined) {
/* 如果有选择器 */ // Parse selector
// 获取id的索引值
const hashIdx = sel.indexOf("#");
// 从id的索引位置开始,或许class的索引值
const dotIdx = sel.indexOf(".", hashIdx);
// 如果有id或class那么返回对应的索引,否则为长度
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
// 获取dom的tagname
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
// 创建真实的el或者elns,赋值给当前vnode的elm属性上,又赋值给elm,因为是对象,所以是引用类型,可以在之后直接使用
const elm = (vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, tag)
: api.createElement(tag));

// 设置 id 和 class
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));

// 调用create hook
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);

// 处理 dom 的子dom元素们
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text));
}
i = (vnode.data as VNodeData).hook; // Reuse variable
if (isDef(i)) {
if (i.create) i.create(emptyNode, vnode);
if (i.insert) insertedVnodeQueue.push(vnode);
}
} /* 否则创建文本节点 */ else {
vnode.elm = api.createTextNode(vnode.text as string);
}
// 返回创建后的 真实dom
return vnode.elm;
}

// 插入Vnodes
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}

function invokeDestroyHook(vnode: VNode) {
let i: any,
j: number,
data = vnode.data;
if (data !== undefined) {
// 调用Vnode的销毁hooks
if (isDef((i = data.hook)) && isDef((i = i.destroy))) i(vnode);
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
// 递归子元素
if (vnode.children !== undefined) {
for (j = 0; j < vnode.children.length; ++j) {
i = vnode.children[j];
if (i != null && typeof i !== "string") {
invokeDestroyHook(i);
}
}
}
}
}

function removeVnodes(
parentElm: Node,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number
): void {
// 循环remore传入的Vnodes
for (; startIdx <= endIdx; ++startIdx) {
let i: any,
listeners: number,
rm: () => void,
ch = vnodes[startIdx];
if (ch != null) {
// 如果是有选择器
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners);
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
if (
isDef((i = ch.data)) &&
isDef((i = i.hook)) &&
isDef((i = i.remove))
) {
i(ch, rm);
} else {
rm();
}
} else {
// Text node
api.removeChild(parentElm, ch.elm as Node);
}
}
}
}

// updateChildren接受的第一个参数是oldVnode的elm,更新的就是这个elm,更新的结果elm还是旧的,不过里面的child发生了变化
function updateChildren(
parentElm: Node,
oldCh: Array<VNode>,
newCh: Array<VNode>,
insertedVnodeQueue: VNodeQueue
) {
// old child是待更新的vnode
// 两个初始值为0的startindex
let oldStartIdx = 0,
newStartIdx = 0;

// 如果长度为0,那么length - 1 为 -1,然后下面的while条件就不会通过了
// 获取old child的长度
let oldEndIdx = oldCh.length - 1;
// 获取old child的第一个vnode
let oldStartVnode = oldCh[0];
// 获取old child的最后一个vnode
let oldEndVnode = oldCh[oldEndIdx];
// 获取new child的最后长度 => 也就是最后一个vnode的索引
let newEndIdx = newCh.length - 1;
// 获取new child的第一个vnode
let newStartVnode = newCh[0];
// 获取new child的最后一个vnode
let newEndVnode = newCh[newEndIdx];

let oldKeyToIdx: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;

/* ======while的开始====== */
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
/* ======一个if的开始====== */
// 获取old child的第一个Vnode
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
/* 获取old child 最后一个Vnode */ oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
/* 获取new child 第一个Vnode */ newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
/* 获取new child 最后一个Vnode */ newEndVnode = newCh[--newEndIdx];
// 上面几步是获取合法的 old child 和 new child的第一个和最后一个Vnode
// 下面进行old child 和 new chile 的更新,并更新 oldStartVnode 和 newStartVnode
// !!!!!! 下面两个比较是同步推进,也就是说 正序 和 倒叙比较 new child 和 old child
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
/* 同上,不过对比的是最后一组Vnode */ patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue
);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
/* 比较的是 old child 的第一个 和 new child的最后一个 */ // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm as Node,
api.nextSibling(oldEndVnode.elm as Node)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
/* 同上 */ // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldEndVnode.elm as Node,
oldStartVnode.elm as Node
);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
// 获取childVnodes中的 每一个key对应的位置
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 获取new的Vnode的位置(索引)
idxInOld = oldKeyToIdx[newStartVnode.key as string];

if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
newStartVnode = newCh[++newStartIdx];
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(
parentElm,
elmToMove.elm as Node,
oldStartVnode.elm as Node
);
}
newStartVnode = newCh[++newStartIdx];
}
}
/* ======一个if的结束====== */
}
/* ======while的结束====== */

// 这两个 || 代表 至少长度为1,因为如果为0的话 endIdx 为 -1, -1 不会小于等于0,也就是说
// old child 或者 new child 至少一个长度部位0,如果满足的话进入if
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
// 如果old child为空数组,也就是没有child(不代表没有Text)
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}

/* ==========patchVnode开始========== */
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
let i: any, hook: any;

// 如果有,那么获取vnode.data中的 prepatch(patch前的钩子函数)
if (
isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))
) {
i(oldVnode, vnode);
}

const elm = (vnode.elm = oldVnode.elm as Node);
// 旧的子dom们
let oldCh = oldVnode.children;
// 新的子dom们
let ch = vnode.children;

// 引用类型的(指针)判断相等
if (oldVnode === vnode) return;

if (vnode.data !== undefined) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
}

// 如果 不是text 类型
if (isUndef(vnode.text)) {
// 如果新旧vnode都有child
if (isDef(oldCh) && isDef(ch)) {
// 并且不相等
if (oldCh !== ch)
// 进行更新
updateChildren(
elm,
oldCh as Array<VNode>,
ch as Array<VNode>,
insertedVnodeQueue
);
} else if (isDef(ch)) {
/* new child 有子节点 old child 没有 */ if (isDef(oldVnode.text))
api.setTextContent(elm, "");
// 直接新增
addVnodes(
elm,
null,
ch as Array<VNode>,
0,
(ch as Array<VNode>).length - 1,
insertedVnodeQueue
);
} else if (isDef(oldCh)) {
/* old child有子节点,new child 没有 */ // 直接删除
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
} else if (isDef(oldVnode.text)) {
/* 新旧都没有子节点 */ // 通过textContent设置文本内容,textContent本身会防止XSS攻击
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/textContent
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
/* 如果text节点不一样 */ // 如果old child 有 chile 那么remove
if (isDef(oldCh)) {
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
}
// 设置为new Vnode 的text
api.setTextContent(elm, vnode.text as string);
}
// 调用postpath钩子
if (isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode);
}
}
/* ==========patchVnode结束========== */

return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;

const insertedVnodeQueue: VNodeQueue = [];

for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}

if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);

createElm(vnode, insertedVnodeQueue);

if (parent !== null) {
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}

for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(
insertedVnodeQueue[i]
);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}

webpack4分包方案

Posted on 2021-05-09 In 经验总结

webpack4放弃了 commonsChunkPlugin,使用更方便灵活智能的 splitChunks 来做分包的操作。

下面有几个例子,并且我们假设所有的chunks大小至少为30kb(采用splitChunks默认配置)

vendors

入口

chunk-a: react react-dom 其他组件
chunk-b: react react-dom 其他组件
chunk-c: angular 其他组件
chunk-d: angular 其他组件

产出

vendors-chunk-a-chunk-b: react react-dom
vendors-chunk-c-chunk-d: angular
chunk-a 至 chunk-d: 对应的其他组件

重复的vendors

入口

chunk-a: react react-dom 其他组件
chunk-b: react react-dom lodash 其他组件
chunk-c: react react-dom lodash 其他组件

产出

vendors-chunk-a-chunk-b-chunk-c: react react-dom
vendors-chunk-b-chunk-c: lodash
chunk-a 至 chunk-c: 对应的其他组件

模块

入口

chunk-a: vue 其他组件 shared组件
chunk-b: vue 其他组件 shared组件
chunk-c: vue 其他组件 shared组件

假设这里的shared体积超过30kb,这时候webpack会创建vendors和commons两个块

产出

vendors-chunk-a-chunk-b-chunk-c: vue
commons-chunk-a-chunk-b-chunk-c: shared组件
chunk-a 至 chunk-c: 对应的其他组件

如果shared提交小于30kb,webpack不会特意提出来,webpack认为如果仅仅为了减少下载体积的话,这样做是不值得的。

多个共享模块

入口

chunk-a: react react-dom 其他组件 react组件
chunk-b: react react-dom angular 其他组件
chunk-c: react react-dom angular 其他组件 react组件 angular组件
chunk-d: angular 其他组件 angular组件

产出

vendors-chunk-a-chunk-b-chunk-c: react react-dom
vendors-chunk-b-chunk-c-chunk-d: angular
commons-chunk-a-chunk-c: react组件
commons-chunk-c-chunk-d: angular组件
chunk-a 至 chunk-d: 对应的其他组件

关于webpack默认配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
  • chunks: 表示从哪些chunks里抽取代码,有三个值:
    1. initial:初始块,分开打包异步\非异步模块
    2. async:按需加载块, 类似initial,但是不会把同步引入的模块提取到vendors中
    3. all:全部块,无视异步\非异步,如果有异步,统一为异步,也就是提取成一个块,而不是放到入口文件打包内容中

通过import()控制模块的一些属性

initial情况下,如果两个入口一个是同步引入,一个是异步引入,那么会分开打包,同步的直接将引入包打到入口文件的打包文件里,异步的会分出单独的块,按需引入
all情况下,如果一个异步一个同步,会统一分出一个单独的块,然后引入

  • minSize代表最小块大小,如果超出那么则分包,该值为压缩前的。也就是先分包,再压缩
  • minchunks表示最小引用次数,默认为1
  • maxAsyncRequests: 按需加载时候最大的并行请求数,默认为5
  • maxInitialRequests: 一个入口最大的并行请求数,默认为3
  • automaticNameDelimiter表示打包后人口文件名之间的连接符
  • name表示拆分出来块的名字
  • cacheGroups:缓存组,除了上面所有属性外,还包括
    • test:匹配条件,只有满足才会进行相应分包,支持函数 正则 字符串
    • priority:执行优先级,默认为0
    • reuseExistingChunk:如果当前代码块包含的模块已经存在,那么不在生成重复的块

几种配置示例(依赖优先级priority)

个人感觉其实只要玩好cacheGroups,就能完成各种各样的分包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将所有node_modules中应用2次以上的抽成common块
optimization: {
splitChunks: {
cacheGroups: {
common: {
test: /[\\/]node_modules[\\/]/,
name: 'common',
chunks: 'initial',
priority: 2,
minChunks: 2
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
// 把所有超过2次的达成common,不局限于node_modules
optimization: {
cacheGroups: {
common: {
name: 'common',
chunks: 'initial',
priority: 2,
minChunks: 2,
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 额外提取react相关基础模块,然后抽取引入超过两次的模块到common
optiomization: {
cacheGroups: {
reactBase: {
name: 'reactBase',
test: (module) => {
return /react|redux|prop-types/.test(module.context);
},
chunks: 'initial',
priority: 10,
},
common: {
name: 'common',
chunks: 'initial',
priority: 2,
minChunks: 2,
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果提取出来的包依然很大,你又想利用好缓存,你可以这样做
// 这样你的每一个node_modules包都是一个chunks,对缓存很友好,会节约很多用户流量,虽然流量已经不之前
optimization: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// 避免服务端不支持@
return `npm.${packageName.replace('@', '')}`;
},
},
}
}

相关文章

Code Splitting, chunk graph and the splitChunks optimization
webpack4 splitchunks实践探索
chunks解释
vendors过大的解决方案

1234

FoxDaxian

遇到好玩的,稀奇的,新鲜的,记录下来,分享出去。
40 posts
11 categories
4 tags
© 2021 FoxDaxian
Powered by Hexo v3.9.0
|
Theme – NexT.Muse v7.2.0