LVM模式安装manjaro

安装流程

首先对物理磁盘分区,对于 UEFI 引导方式,需要划分 efi 分区 并把磁盘挂载到 /boot/efi 路径

注意: 所有的分区和格式化操作都需要在命令行中执行, manjaro 的 GUI 只用来挂载分区

如果使用 window 环境下的虚拟机安装, 在 启用或关闭 windows 功能 配置中,关闭 windows 沙箱, Hyper-v 两个配置, 这功能可能导致虚拟死机。

  • 启动前将系统的引导方式修改为 UEFI,进入系统时选择专有驱动的选项启动方式

  • 使用 fdisk 查看磁盘信息

  • 格式化磁盘为 GPT 格式,划分出 500M 作为 efi 分区,剩余空间给 LVM 使用

  • 分区成功后将 efi 分区格式化为 FAT32 格式

  • 将剩余的空间转为 LVM, 创建出 /dev/vgdisk/lvhome 分区,并将 lvhome 分区格式化为 ext4

分配成功后使用以下命令查看分区信息:

1
2
3
4
5
6
7
8
9
sudo pvs
sudo vgs
sudo lvs

# 查看详细信息

sudo pvdisplay
sudo vgdisplay
sudo lvdisplay

删除分区

1
2
3
sudo pvremove /dev/something
sudo vgremove something
sudo lvremove /dev/something
  • 通过 GUI 界面将分区挂载到指定路径,挂载磁盘时一定不要重新格式化磁盘

错误处理

  • 开机时提示一下错误,可能是遗留的 bug,解决方案如下

1
2
3
4
# /etc/default/grub
GRUB_SAVDEFAULT=false

update-grub

手动挂载分区

系统安装成功后,可能需要挂载更多自定义分区, 首先创建一个 workspace lv分区

查看分区信息

1
2
3
4
sudo blkid

#指定分区
sudo blkid /dev/sda1

添加信息到 /etc/fstab, 实现自动挂载

扩容 lvm

  • 扩容虚拟磁盘, vmware 磁盘设置中增加磁盘容量。

  • 使用 gdisk 为剩余磁盘创建分区

    或者给已有的磁盘扩展分区, 2 表示扩展分区, 100% 表示扩展到剩余的全部空间

    1
    2
    3
    4
    5
    6
    7

    # GPT 分区表为了安全,会在磁盘的 开头 和 末尾 各保存一份分区表备份。
    # 当你通过 PVE 调整了虚拟磁盘的大小(从 512G 增加到 1T)后,原先位于磁盘 512G 处的“末尾备份”现在处于磁盘的“中间”位置了。
    # Warning: Not all of the space available...:系统检测到磁盘变大了,但 GPT 的备份头(Backup Header)还在旧的位置。
    # Fix/Ignore?:如果你选 Fix,parted 会把备份头移动到新的 1T 磁盘末尾,这样你才能使用新增的那 500G 空间。

    sudo parted /dev/sda resizepart 2 100%

    创建成功后磁盘可能无法使用,使用以下命令刷新分区表

    1
    sudo partprobe
  • 使用 pvcreate 将新的分区创建为新的pv

    或者为已有的分区扩展 pv

    1
    sudo pvresize /dev/sda2
  • 扩容 vg 将新的 pv 添加至 指定卷组

    1
    2
    3
    vgextend vgdisk /dev/sda3

    vgs #查看容量
  • 扩容 lv

    -l + :指定逻辑卷的LE个数,如 -l +200 一般一个为 4M
    -L + :表示增加多少空间,如 -L +15G ,单位有bBsSkKmMgGtTpPeE
    -l +100%FREE :表示增加vg的全部可用空间

    1
    lvextend -L +15G /dev/vgname/lvname
  • 扩展文件系统

    1
    resize2fs /dev/something

⑪控制访问-代理模式

代理模式

为另一个对象提供一个替身或占位符以控制对这个对象的访问。

代理模式非常灵活,可能不经意写的一行代码也是代理模式,例如:

  • 分发请求到到远程
  • 为初始化开销大的对象提供代理
  • 保护某些对方法不能访问

JS 中提供了 Proxy 这个代理对象,

图片加载

想象一下一个图片的加载过程是不是可以通过代理来实现

  • 发起加载图片并绘制在页面上的请求
  • 这个请求将发送给代理对象
  • 代理对象通过网络获取图片
  • 绘制图片并提供图片控制的接口

防火墙代理

控制网络资源的访问,保护主题免于侵害

只能引用代理

当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数

缓存代理

为开销大的运算结果提供暂时存储,它也允许多个客户共享结果,以减少计算或网络延迟。

同步代理

在多线程的情况下,提供安全访问。

复杂隐藏代理

用来隐藏一个类的复杂度,并控制访问,有时候也称为 外观代理
复杂隐藏代理和外观模式不一样,因为代理控制访问,而外观模式提供另一组接口。

写入时复制代理

用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。

CSS 尺寸/位置/布局/元素

复选框与文本对齐

1
2
<p><input type="checkbox" /><span>文本</span></p>
<p><input type="radio" /> <span>文本</span></p>

当字号大于 input 尺寸时, input 会与文字中间对其

但是字号过小的时候,input 与文字底部并不能对其

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 方案1 */
input {
vertical-align: -3px;
}

/* 方案2 */
input {
vertical-align: top;
margin-top: 4px;
}

/* 方案3 */
input {
vertical-align: middle;
margin-top: -2px;
margin-bottom: 1px;
}

多行文字垂直居中

1
2
3
4
5
<div class="box">
<div class="child"
>这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。这里显示多行文字。</span
>
</div>
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
/* 方案1 */

.box {
width: 500px;
height: 500px;
display: flex;
align-items: center;
}

/* 方案2 */

.box {
width: 500px;
height: 500px;
display: flex;
}

.child {
margin: auto 0;
}

/* 方案3 */

.box {
width: 500px;
height: 500px;
line-height: 500px;
}

.child {
vertical-align: middle;
line-height: normal;
display: inline-block;
}

/* 方案4 */

.box {
width: 500px;
height: 500px;
display: table;
}

.child {
display: table-cell;
vertical-align: middle;
}

多行文本超出隐藏

1
2
3
4
5
.box {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}

三列布局

  • 绝对定位 1
1
2
3
4
5
<div class="box">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
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
/* 绝对定位1 */

.left {
position: absolute;
width: 200px;
left: 0;
top: 0;
height: 100%;
background: goldenrod;
}

.right {
position: absolute;
width: 200px;
right: 0;
height: 100%;
background: thistle;
}

.center {
position: absolute;
right: 200px;
left: 200px;
height: 100%;
background: palegreen;
}

.box {
position: relative;
height: 100%;
}

/* 绝对定位2 */

.left {
position: absolute;
width: 200px;
left: 0;
top: 0;
height: 100%;
background: goldenrod;
}

.right {
position: absolute;
width: 200px;
right: 0;
top: 0;
height: 100%;
background: thistle;
}

.center {
margin: 0 200px;
height: 100%;
background: palegreen;
}

.box {
position: relative;
height: 100%;
}
  • 浮动 + 负 margin
1
2
3
4
5
6
7
<div class="box">
<div class="wrapper">
<div class="center"></div>
</div>
<div class="left"></div>
<div class="right"></div>
</div>
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
.left {
float: left;
width: 200px;
margin-left: -100%;
height: 100%;
background: goldenrod;
}

.center {
margin: 0 200px;
height: 100%;
background: palegreen;
}

.wrapper {
float: left;
height: 100%;
width: 100%;
}

.right {
float: left;
width: 200px;
height: 100%;
margin-left: -200px;
background: thistle;
}

.box {
position: relative;
height: 100%;
}
1
2
3
4
5
6
7
8
<!-- 推荐,元素与视觉保持一致 -->
<div class="box">
<div class="left"></div>
<div class="wrapper">
<div class="center"></div>
</div>
<div class="right"></div>
</div>
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
.left {
float: left;
width: 200px;
margin-left: -100%;
height: 100%;
background: goldenrod;
}

.center {
margin: 0 200px;
height: 100%;
background: palegreen;
}

.wrapper {
float: left;
height: 100%;
width: 100%;
}

.right {
float: left;
width: 200px;
height: 100%;
margin-left: -200px;
background: thistle;
}

.box {
position: relative;
height: 100%;
}
  • flex
1
2
3
4
5
<div class="box">
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</div>
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
.left {
width: 200px;
height: 100%;
background: goldenrod;
}

.center {
flex: 1;
height: 100%;
background: palegreen;
}

.wrapper {
float: left;
height: 100%;
width: 100%;
}

.right {
width: 200px;
height: 100%;
background: thistle;
}

.box {
display: flex;
height: 100%;
}

尺寸百分比

  • 无定位元素: 相对于外层元素的内容(容纳)区域
  • 定位元素: 相对于最近的定位元素的 padding 区域
属性 百分比相对与
width 参考元素宽度
height 参考元素高度,需要指定参考元素高度
padding 参考元素宽度
border 参考元素宽度
margin 参考元素宽度

最大/最小 尺寸

可用于防止内部元素,超出容器尺寸

1
2
3
4
5
6
.box {
width: 300px;
}
img {
max-width: 100%;
}

表单

  • form 可以原生支持回车键提交表单, 会自动查找 form 中第一个有 type='submit' 的按钮, 并触发这个按钮的点击事件

  • 放在 label 中的 input 元素,即使没有使用 for 关联在一起,点击 label 中的任意元素,也会选中 input

    1
    2
    3
    4
    5
    <label>
    <input type="radio" />
    <p>name</p>
    ></label
    >

精灵图

spritesmith 用于合并图片,并生成生成样式

属性值计算过程

每一个元素的每一个属性都必须有值,属性值变为最终计算样式的过程就是属性值计算过程。

  • 确定声明值: 样式表中没有冲突的声明,作为最终样式

  • 层叠冲突: 对有冲突的声明使用层叠规则,确定 css 属性值

    • 比较重要性,作者样式表会覆盖浏览器默认样式
    • 比较特殊性,比较权重
    • 比较源次序,权重相同时,后写的样式覆盖先写的样式
  • 使用继承,对仍然没有值的属性,若可以继承,则继承父元素的值

  • 使用默认值,对仍然没有值的属性,使用默认值

⑩状态模式

避免陷入在 if/else 之中

考虑以下两种场景,哪种更容易让你深陷在 if/else 的逻辑之中。

订单配送,订单可能处于下面的几种状态

  • 下单状态
  • 打包状态
  • 配送状态
  • 签收状态

自动售卖机

  • 无货状态
  • 有货状态
  • 已投币状态
  • 未投币状态
  • 出货状态

显然订单配送的状态处理起来更容易一些

  • 下单状态需要备货
  • 打包状态需要检查时候安全,是否要放入小礼物
  • 配送状态要同步配送信息
  • 签收状态要回访客户

虽然这些状态环环相扣,但是一旦状态完成转移就不需要在考虑原有状态中的行为是否还需要关注。

而自动售售货机可能让你陷入 if/else 的深渊,因为不同的状态可能伴随相同的操作,无论处于哪种状态,用户都可能按下取货按钮,但只有投币并且有货,按下取货才有意义。

状态模式

状态模式:允许对象在内部状态改变的时候改变他的行为,对象看起来好像修改了它的类。

状态模式和策略模式中的组合对象很像,但是状态模式更专注与状态的迁移,和不同状态中的行为。

  • 状态模式允许一个对象基于内部状态而拥有不同的行为。
  • 和程序状态机(PSM)不同,状态模式用类代表状态
  • Context 会将行为委托给当前对象
  • 通过将每个状态封装进一个类,我们把以后需要做的任何变化都局部化了
  • 状态模式允许 Context 随着状态改变而改变行为
  • 状态转移可以由 State 类或 Context 类控制
  • 使用状态模式通常会导致设计中类的数目大量增加
  • 状态类可以被多个 Context 实例共享
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
// 投币
// 点击出货

abstract class State {
machine: VendingMachine;
constructor(machine: VendingMachine) {
this.machine = machine;
}
addCoin(coin?: Number) {
console.log("必须实现 addCoin");
}
getProduct() {
console.log("必须实现 getProduct");
}
}

// 无商品

class NoProduct extends State {
addCoin() {
console.log("机器里无商品,你不能购买");
}
getProduct() {
console.log("机器里无商品,你不能购买");
}
}

class HasProduct extends State {
addCoin(coin: number) {
this.machine.coin += coin;
}
getProduct() {
console.log("你已经获取了商品");
this.machine.count -= 1;
this.machine.coin -= 1;
this.stateTransform();
}
stateTransform() {
const { count, coin } = this.machine;
if (coin === 0) {
this.machine.setState(this.machine.stateNoCoin);
} else if (count === 0) {
this.machine.setState(this.machine.stateNoProduct);
}
}
}

class NoCoin extends State {}

class VendingMachine {
stateNoProduct: NoProduct;
stateHasProduct: HasProduct;
stateNoCoin: NoCoin;
count: number;
coin: number = 0;
state: NoProduct | HasProduct;
constructor(count: number) {
this.count = count || 2;
this.stateNoProduct = new NoProduct(this);
this.stateHasProduct = new HasProduct(this);
this.stateNoCoin = new NoCoin(this);
this.state = this.stateHasProduct;
}
setState(state: NoProduct | HasProduct) {
this.state = state;
}
addCoin(coin = 0) {
this.state.addCoin(coin);
}
getProduct() {
this.state.getProduct();
}
}

canvas 画板

橡皮擦

依赖于 ctx.globalCompositeOperation 配置,有以下可选项

source-over(默认值):新图形绘制在现有图形上方。
source-in:新图形仅在与现有图形重叠的区域内绘制。
source-out:新图形仅在与现有图形不重叠的区域内绘制。
source-atop:新图形绘制在现有图形上方,但只在它们重叠的区域内可见。
destination-over:新图形绘制在现有图形下方。
destination-in:现有图形仅保留与新图形重叠的部分。
destination-out:现有图形中与新图形不重叠的部分保留。
destination-atop:现有图形绘制在新图形上方,但只在它们重叠的区域内可见。
lighter:重叠区域的颜色通过加法混合。
copy:只有新图形可见,现有内容被清除。
xor:重叠区域变透明。

1
2
3
4
5
6
ctx.globalCompositeOperation = "destination-out";
// 线宽影响橡皮擦大小
ctx.lineWidth = 10;
ctx.strokeStyle = "red";
ctx.lineTo(x, y);
ctx.stroke();

使用 rfa 逐帧绘制

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
const queue = [];
const draw = () => {
requestAnimationFrame(() => {
let len = queue.length;
for (var i = 0; i < len; i++) {
const a = queue[i];
ctx.lineTo(a[0], a[1]);
}
if (len) {
ctx.stroke();
}
queue = [];
if (mark) draw();
});
};

canvas?.addEventListener("pointerdown", () => {
mark = true;
// 取整数减少浮点运算
queue.push([
Math.floor(e.clientX * window.devicePixelRatio),
Math.floor(e.clientY * window.devicePixelRatio),
]);
ctx.beginPath();
draw();
});

平滑曲线

使用贝塞尔曲线拟合

  • 取 B C 中点 B1, A 为起点,B 为控制点,B1 为终点
  • 取 C D 中点 C1, B1 为起点,C 为控制点,C1 为终点

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
let rfa = 0;
const draw = (index = 0) => {
if (!index && rfa) return;
rfa = requestAnimationFrame(() => {
let len = queue.length;
if (len && !index) {
const a = queue[index];
ctx.lineTo(a[0], a[1]);
index++;
}
while (len >= 3 && index < len - 1) {
const cur = queue[index];
const next = queue[index + 1];
const cx = (cur[0] + next[0]) >> 1;
const cy = (cur[1] + next[1]) >> 1;
ctx.quadraticCurveTo(cur[0], cur[1], cx, cy);
index += 1;
}

if (len) {
ctx.stroke();
}
if (mark) {
draw(index);
} else {
rfa = 0;
}
});
};

离屏 canvas 模拟粉笔效果

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
let queue: any[] = [];
const offScreen = new OffscreenCanvas(dimension.width, dimension.height);
const offCtx = offScreen.getContext("2d") as OffscreenCanvasRenderingContext2D;
offCtx.scale(width / dimension.width, height / dimension.height);
offCtx.strokeStyle = "red";
offCtx.lineWidth = 6;
offCtx.lineCap = "round";

const draw = () => {
requestAnimationFrame(() => {
let len = queue.length;
offCtx.clearRect(0, 0, dimension.width, dimension.height);
// 在清除画布之后必须使用 beginPath
offCtx.beginPath();
for (var i = 0; i < len; i++) {
const a = queue[i];
offCtx.lineTo(a[0], a[1]);
}
if (len) {
offCtx.stroke();
}

for (let i = 1; i < len; i++) {
const pre = queue[i - 1];
const cur = queue[i];
const length = Math.round(
Math.sqrt(Math.pow(pre[0] - cur[0], 2) + Math.pow(pre[1] - cur[1], 2))
);
const xUnit = (cur[0] - pre[0]) / length;
const yUnit = (cur[1] - pre[1]) / length;
for (let i = 0; i < length; i++) {
const xCurrent = pre[0] + i * xUnit;
const yCurrent = pre[1] + i * yUnit;
const xRandom = xCurrent + (Math.random() - 0.5) * 6 * 1.2;
const yRandom = yCurrent + (Math.random() - 0.5) * 6 * 1.2;
offCtx.clearRect(
xRandom,
yRandom,
Math.random() * 2 + 2,
Math.random() + 1
);
}
}

queue = [];

ctx.globalCompositeOperation = "source-over";
ctx.drawImage(offScreen, 0, 0, dimension.width, dimension.height);
if (mark) draw();
});
};

⑨集合管理-迭代器和组合模式

统一的遍历方法

像是为对象提供 Iterator 接口一样,有时需要遍历一个复杂对象的内部属性,所以需要一个统一的接口,这也是迭代器模式需求的由来。

迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

现在有两个对象保存着一些数据,但是数据使用不同的数据结构保存,数组,对象,链表,或是一些特殊的封装结构。

1
2
3
4
5
6
7
8
9
10
11
class Test1 {
data = ["a1", "b1", "c1"];
}

class Test2 {
data = {
a2: "a2",
b2: "b2",
c2: "c2",
};
}

如果想要便利这两个对象中的所有数据,最容易想到的办法就是分别使用数据和对象的遍历方法,通过两次循环依次返回。

把这个遍历的实现定义为类 MapObject,但这并不是一个合理的办法:

  • 遍历的前提是必须要知道对象的实现细节,违背了针对接口编程,而不是针对实现,也可以说是违背了封装。
  • 如果需要更换其中的一个类遍历,那么必须修改 MapObject,违背了对扩展开放,对修改关闭。

现在已经清楚了变化之处在于遍历,想办法把遍历封装起来。

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
interface IteratorClass<T> {
new (): T;
}
interface IIterator {
iterator(): IterableIterator<string>;
}

// 遵循面向接口编程的原则,两个类都实现了相同的接口

class Test1 implements IIterator {
data = ["a1", "b1", "c1"];
iterator() {
return this.data[Symbol.iterator]();
}
}

class Test2 implements IIterator {
data = {
a2: "a2",
b2: "b2",
c2: "c2",
};
iterator() {
const iterator = (): IterableIterator<string> => {
let data = this.data;
let keys = Object.keys(data);
let index = 0;
return {
next() {
return index < keys.length
? {
value: data[keys[index++] as keyof typeof data],
done: false,
}
: {
value: undefined,
done: true,
};
},
} as IterableIterator<string>;
};

return iterator();
}
}

class MapObject {
test1: Test1;
test2: Test2;
constructor(test1: IteratorClass<Test1>, test2: IteratorClass<Test2>) {
this.test1 = new test1();
this.test2 = new test2();
}
printItem() {
let stack = [this.test1, this.test2];

stack.forEach((instance) => {
let iterator = instance.iterator();
let done: any, value: any;
do {
({ done, value } = iterator.next());
if (value) {
console.log(value);
}
} while (!done);
});
}
}

单一职责

一个类应该只有一个引起变化的原因。, 当一个类有多个变化的可能时,会增加维护的成本,或导致其他的功能出现错误。

内聚这个术语也可以看作是衡量单一指责的一个表述。

组合模式

组合模式相对于之前提到的代码组合更加具体,可以理解为代码组合包括组合模式。

现在有这样的几个对象,每个对象用不同的数据结构保存自己的数据,而且数据还可能分级,下一级是另一个对象,形成一个树的结构。需要实现一个迭代器,能在树的不同节点中以及下一层节点中移动。

实现这个功能可以使用组合模式,组合模式允许你将对象组合成树形结构来表现 ‘整体/部分’ 层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

组合的目的是忽略整体和个体的差别,应用在整体上的操作同样可以应用在个体上。

遵循面向接口的变成方式,首先实现抽象类,所有的对象以及叶子节点的对象都需要实现抽象类,并且从冲向类中继承 print 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class AbstractDataCollection {
abstract createIterator(): any;
print(): void {
let done: any, value: any;
const iterator = this.createIterator();
do {
({ done, value } = iterator.next());
if (!done) {
value.print();
}
} while (!done);
}
}

不同的对象集合可以组合但是由于实现方式不同,需要各自实现抽象相类中的 createIterator 方法

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
class DataCollection1 extends AbstractDataCollection {
data: any[] = [];
add(dataItem: any) {
this.data.push(dataItem);
return this;
}
iterator() {
return this.data[Symbol.iterator]();
}
createIterator() {
return new CompositeIterator(this.iterator());
}
}
class DataCollection2 extends AbstractDataCollection {
data: { [key: string]: any } = {};
add(dataItem: any, key: string) {
this.data[key] = dataItem;
return this;
}
iterator() {
let data = this.data;
let keys = Object.keys(data);
let index = 0;
return {
next() {
return index < keys.length
? {
value: data[keys[index++] as keyof typeof data],
done: false,
}
: {
value: undefined,
done: true,
};
},
};
}
createIterator() {
return new CompositeIterator(this.iterator());
}
}

type LinkData = {
next: LinkData | null;
value?: any;
};

class DataCollection3 extends AbstractDataCollection {
data: LinkData = { next: null };
last: LinkData = this.data;
add(dataItem: any) {
this.last.next = {
value: dataItem,
next: null,
};
this.last = this.last.next;
return this;
}
iterator() {
let data: LinkData | null = this.data;
return {
next() {
data = data!.next;

if (data === null) {
return {
done: true,
value: undefined,
};
} else {
return {
done: false,
value: data.value,
};
}
},
};
}
createIterator() {
return new CompositeIterator(this.iterator());
}
}

class DataItem extends AbstractDataCollection {
data: any;
constructor(data: any) {
super();
this.data = data;
}
createIterator() {
return new NullIterator();
}
print(): void {
console.log(this.data);
}
}

叶子节点的 createIterator 方法返回一个空的迭代器,真实环境中经常会使用这个方式添加占位,让所有对象的行为保持一致。

对象集合的 createIterator 方法,实现了一个 Iterator 类,把没有迭代完成的迭代器重新放回到队列中

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
class CompositeIterator<T> implements Iterator<T> {
stack: Iterator<T>[] = [];
constructor(iterator: Iterator<T>) {
this.stack.push(iterator);
}
next(...args: [] | [undefined]): IteratorResult<T, any> {
if (this.stack.length) {
const iterator = this.stack.pop();

let component = iterator!.next();
if (!component.done) {
this.stack.push(iterator!);
}
return component;
} else {
return {
value: undefined,
done: true,
};
}
}
}

class NullIterator<T> implements Iterator<T> {
next(...args: [] | [undefined]): IteratorResult<T, any> {
return {
done: true,
value: undefined,
};
}
}

最后实现遍历的类

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
const t = new DataCollection1();
t.add(new DataItem(1));
t.add(new DataItem(2));

const p = new DataCollection2();

p.add(new DataItem("a"), "a");
p.add(new DataItem("b"), "b");

const q = new DataCollection3();
q.add(new DataItem("p"));

const w = new DataCollection2();
w.add(new DataItem("w1"), "w1");
w.add(new DataItem("w2"), "w2");

q.add(w);

q.add(new DataItem("q"));

class MapObject extends AbstractDataCollection {
stack: any[] = [];
add(instance: AbstractDataCollection) {
this.stack.push(instance);
return this;
}
iterator() {
return this.stack[Symbol.iterator]();
}
createIterator() {
return new CompositeIterator(this.iterator());
}
}
const o = new MapObject();

o.add(t);
o.add(p);
o.add(q);
o.print();

真实世界中的组合模式没有这么刻板,例如 React 组件的组合,通过单向数据流或状态管理工具,只要调用外层组件的方法即可,无需关心子组件的实现。

有时不同的节点之间需要双向连接用于回退,或反查父节点。

如果某个节点作为计算功能,并且频繁调用,可以考虑使用缓存。

鼠标,触摸,指针事件

触摸事件

Safari 不支持 Touch 事件

| touches | 正在触摸屏幕所有手指的一个列表 |
| targetTouches | 正在触摸当前 DOM 元素上的手指的一个列表 |
| changedTouches | 相对上一次触摸事件改变的 Touch 对象, 从无到有,从有到无变化 |

模拟一个触摸事件

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
// 获取按钮元素
var button = document.getElementById("myButton");

// 创建一个触摸点
var touch = new Touch({
identifier: Date.now(),
target: button,
clientX: 50, // X坐标
clientY: 50, // Y坐标
radiusX: 2.5,
radiusY: 2.5,
rotationAngle: 10,
force: 0.5,
});

// 创建一个触摸事件
var touchEvent = new TouchEvent("touchstart", {
cancelable: true,
bubbles: true,
touches: [touch],
targetTouches: [],
changedTouches: [touch],
shiftKey: true,
});

button.dispatchEvent(touchEvent);

指针事件历史

chrome 55 版本之前,同时期浏览器浏览器都只有 MouseEvent, 然而,近年来的新兴设备支持更多不同方式的指针定点输入,如各类触控笔和触摸屏幕等。这就有必要扩展现存的定点设备事件模型,以有效追踪各类指针事件。

PointerEvent 接口继承了所有 MouseEvent 中的属性,以保障原有为鼠标事件所开发的内容能更加有效的迁移到指针事件。

不同事件对应表

MouseEvent TouchEvent PointerEvent
mousedown touchstart pointerdown
mousemove touchmove pointermove
mouseup touchend pointerup
touchcancel pointercancel
mouseenter pointerenter
mouseleave pointerleave
mouseover pointerover
mouseout pointerout
gotpointercapture
lostpointercapture

setPointerCapture

用于将特定元素指定为未来指针事件的捕获目标。指针的后续事件将以捕获元素为目标,直到捕获被释放

通常用于实现,在元素外拖动的时候让元素保持响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<div id="slider">SLIDE ME</div>
<script>
function beginSliding(e) {
slider.onpointermove = slide;
slider.setPointerCapture(e.pointerId);
}

function stopSliding(e) {
slider.onpointermove = null;
slider.releasePointerCapture(e.pointerId);
}

function slide(e) {
slider.style.transform = `translate(${e.clientX - 70}px)`;
}

const slider = document.getElementById("slider");

slider.onpointerdown = beginSliding;
slider.onpointerup = stopSliding;
</script>
</body>

event.pointerType

用于区分各类设备,以兼容不同的事件类型.

  • mouse 鼠标触发的事件

  • pen 笔或手写笔设备

  • touch 手指等触摸事件

多指触控

1
2
3
4
// 通过id区分
document.onpointerdown = (e) => {
console.log(e.pointerId);
};

React Router 解析

整个项目大致分为三个包:

  • router
    实现了各种类型的 history, 完成了路由配置的底层实现,例如导航,路由 loader,lazy

  • react-router
    实现了各类 hooks, 根组件的 Provider, 可以用组件形式配置的 Route 组件,最终还是会被解析为 routes 配置。

  • react-router-com
    完善了 React 组件,可以直接用组件去声明 BrowserRouter 还是 HashRouter 以及 Link 等业务组件。

history.pushState 参数解释
state(状态对象)一个 JavaScript 对象,用于保存与当前历史记录条目关联的状态数据。当用户通过浏览器的前进/后退按钮导航到该记录时,可以通过 popstate 事件 (event.state) 获取到这个对象。可以存储当前页面的状态(例如:滚动位置、表单数据、组件状态等),以便在导航回该页面时恢复状态。
title(标题)理论上用于设置浏览器历史记录中该条目的标题,但目前所有主流浏览器均忽略此参数。
url(新的 URL)指定浏览器地址栏显示的新 URL。页面不会重新加载,但必须满足同源策略(Same-origin Policy)。
使用 pushState 不会触发 popstate 事件,popstate 事件仅在用户点击浏览器的后退,前进按钮或通过 JavaScript 调用 history.back()、history.forward() 或 history.go() 方法时触发。

go 方法的行为,不会删除 history 的记录栈,它只是移动指针指向之前或是之后的历史地址。如果地址栏是 hash 改变默认不会刷新页面,如果是 path 改变默认会刷新页面。

Browser History

提供对以下接口对 history 对象进行控制。可以发现并没有对原生对象封装或扩展,而是创建了一个新对象。因此 history 对象只在 react router 内部使用。

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
function createBrowserLocation() {
return let history: History = {
get action() {
return action;
},
get location() {
return getLocation(window, globalHistory);
},
listen(fn: Listener) {

window.addEventListener(PopStateEventType, handlePop);
listener = fn;

return () => {
window.removeEventListener(PopStateEventType, handlePop);
listener = null;
};
},
createHref(to) {
return createHref(window, to);
},
createURL,
encodeLocation(to) {
let url = createURL(to);
return {
pathname: url.pathname,
search: url.search,
hash: url.hash,
};
},
push,
replace,
go(n) {
return globalHistory.go(n);
},
}

push 方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function push(to: To, state?: any) {
action = Action.Push;
let location = createLocation(history.location, to, state);
if (validateLocation) validateLocation(location, to);

index = getIndex() + 1;
let historyState = getHistoryState(location, index);
let url = history.createHref(location);

/// ios 有 100条 pushState 的限制
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
if (error instanceof DOMException && error.name === "DataCloneError") {
throw error;
}
window.location.assign(url);
}
}

Hash History

Hash Router 多用于静态托管环境无法配置服务器将所有路径重定向到入口文件。

hash router 的设计与 browser router 共用了对外接口的实现,因为对于浏览器来说无论是地址变化还是 hash 变化都是 pushState,当浏览器返回或前进是都会触发 popstate 事件,因此区别只是在于处理的参数不同。

react router v6 版本并没有使用 hashchange 实现事件监听。hashchange IE8+ 开始支持,popstate IE10+ 开始支持。

Memory History

使用数组作为历史记录栈,且监听函数不能多次绑定

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
export function createMemoryHistory(
options: MemoryHistoryOptions = {}
): MemoryHistory {
let { initialEntries = ["/"], initialIndex, v5Compat = false } = options;
let entries: Location[]; // Declare so we can access from createMemoryLocation
entries = initialEntries.map((entry, index) =>
createMemoryLocation(
entry,
typeof entry === "string" ? null : entry.state,
index === 0 ? "default" : undefined
)
);
let index = clampIndex(
initialIndex == null ? entries.length - 1 : initialIndex
);
let action = Action.Pop;
let listener: Listener | null = null;

function clampIndex(n: number): number {
return Math.min(Math.max(n, 0), entries.length - 1);
}
function getCurrentLocation(): Location {
return entries[index];
}
function createMemoryLocation(
to: To,
state: any = null,
key?: string
): Location {
let location = createLocation(
entries ? getCurrentLocation().pathname : "/",
to,
state,
key
);
warning(
location.pathname.charAt(0) === "/",
`relative pathnames are not supported in memory history: ${JSON.stringify(
to
)}`
);
return location;
}

function createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
}

let history: MemoryHistory = {
get index() {
return index;
},
get action() {
return action;
},
get location() {
return getCurrentLocation();
},
createHref,
createURL(to) {
return new URL(createHref(to), "http://localhost");
},
encodeLocation(to: To) {
let path = typeof to === "string" ? parsePath(to) : to;
return {
pathname: path.pathname || "",
search: path.search || "",
hash: path.hash || "",
};
},
push(to, state) {
action = Action.Push;
let nextLocation = createMemoryLocation(to, state);
index += 1;
entries.splice(index, entries.length, nextLocation);
if (v5Compat && listener) {
listener({ action, location: nextLocation, delta: 1 });
}
},
replace(to, state) {
action = Action.Replace;
let nextLocation = createMemoryLocation(to, state);
entries[index] = nextLocation;
if (v5Compat && listener) {
listener({ action, location: nextLocation, delta: 0 });
}
},
go(delta) {
action = Action.Pop;
let nextIndex = clampIndex(index + delta);
let nextLocation = entries[nextIndex];
index = nextIndex;
if (listener) {
listener({ action, location: nextLocation, delta });
}
},
listen(fn: Listener) {
listener = fn;
return () => {
listener = null;
};
},
};

return history;
}

navigate 是 router 包中的核心方法,执行导航流程,中间处理各种配置,数据加载策略,loader,lazy 也会在导航过程中处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const route = createRoute([
{
path: "/",
},
{
id: "json",
path: "/test",
loader: true,
children: [
{
id: "text",
index: true,
loader: true,
},
],
},
]);

await route.navigate("/test");
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
async function navigate(
to: number | To | null,
opts?: RouterNavigateOptions
): Promise<void> {
// 传入数字特殊处理,相当于 history.go
if (typeof to === "number") {
init.history.go(to);
return;
}

// 根据参数创建将要导航的地址
// {pathname: '/test', search: '', hash: '', state: null, key: 'h5jaikrx'}
nextLocation = {
...nextLocation,
...init.history.encodeLocation(nextLocation),
};

// 记录滚动条的位置
saveScrollPosition(state.location, state.matches);

// 找出匹配了哪些路由
let matched = matchRoutes(routesToUse, location, basename);

// 处理 lazy 属性
let loadRouteDefinitionsPromises = matches.map((m) =>
m.route.lazy
? loadLazyRouteModule(m.route, mapRouteProperties, manifest)
: undefined
);

// 处理路由中需要请求的数据
let results = await dataStrategyImpl({
matches: dsMatches,
request,
params: matches[0].params,
fetcherKey,
context: requestContext,
});

// 如果 loader 返回 redirect
let redirect = findRedirect(loaderResults);
if (redirect) {
await startRedirectNavigation(request, redirect.result, true, {
replace,
});
return { shortCircuited: true };
}

// 更新内部状态,执行 subscribers 添加的监听方法
updateState();
// 获取到数据,pushState 提交导航
if (pendingAction === HistoryAction.Push) {
init.history.push(location, location.state);
}
}

组件更新绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
children: [
{
path: "dashboard",
element: <Dashboard />,
},
{
path: "about",
element: <About />,
},
],
},
]);

<RouterProvider router={router} fallbackElement={<BigSpinner />} />;

RouterProvider 会注册监听函数到 router 的 subscribes, 当 navigate 执行结束,会执行 subscribes 中注册的回调函数

当通过 RouterProvider 定义路由时,React Router 会自动优化组件的重用和渲染逻辑 推荐使用上面的写法,而不是组件的写法。

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
export function RouterProvider({
fallbackElement,
router,
future,
}: RouterProviderProps): React.ReactElement {
let [state, setStateImpl] = React.useState(router.state);

let setState = React.useCallback<RouterSubscriber>(
(newState: RouterState) => {
setStateImpl(newState);
},
[setStateImpl]
);

// 导航结束会触发更新
React.useLayoutEffect(() => router.subscribe(setState), [router, setState]);

let navigator = React.useMemo((): Navigator => {
return {
//...
};
}, [router]);

let basename = router.basename || "/";

let dataRouterContext = React.useMemo(
() => ({
router,
navigator,
static: false,
basename,
}),
[router, navigator, basename]
);

return (
<>
<DataRouterContext.Provider value={dataRouterContext}>
<DataRouterStateContext.Provider value={state}>
<Router
basename={basename}
location={state.location}
navigationType={state.historyAction}
navigator={navigator}
>
{state.initialized || router.future.v7_partialHydration ? (
<DataRoutes
routes={router.routes}
future={router.future}
state={state}
/>
) : (
fallbackElement
)}
</Router>
</DataRouterStateContext.Provider>
</DataRouterContext.Provider>
{null}
</>
);
}

DataRoutes 会递归处理 routers 数组,生成嵌套关系的组件树。

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
if (dataRouterState && future && future.v7_partialHydration) {
for (let i = 0; i < renderedMatches.length; i++) {
let match = renderedMatches[i];
if (match.route.HydrateFallback || match.route.hydrateFallbackElement) {
fallbackIndex = i;
}

if (match.route.id) {
let { loaderData, errors } = dataRouterState;
let needsToRunLoader =
match.route.loader &&
loaderData[match.route.id] === undefined &&
(!errors || errors[match.route.id] === undefined);
if (match.route.lazy || needsToRunLoader) {
renderFallback = true;
if (fallbackIndex >= 0) {
renderedMatches = renderedMatches.slice(0, fallbackIndex + 1);
} else {
renderedMatches = [renderedMatches[0]];
}
break;
}
}
}
}

return renderedMatches.reduceRight((outlet, match, index) => {
let error: any;
let shouldRenderHydrateFallback = false;
let errorElement: React.ReactNode | null = null;
let hydrateFallbackElement: React.ReactNode | null = null;
let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
let getChildren = () => {
let children: React.ReactNode;
if (error) {
children = errorElement;
} else if (shouldRenderHydrateFallback) {
children = hydrateFallbackElement;
} else if (match.route.Component) {
children = <match.route.Component />;
} else if (match.route.element) {
children = match.route.element;
} else {
children = outlet;
}
return (
<RenderedRoute
match={match}
routeContext={{
outlet,
matches,
isDataRoute: dataRouterState != null,
}}
children={children}
/>
);
};
}

组件路由是如何工作的

可以使用组件的方式来组织路由,Routes 会收集它下面的所有 Route 并渲染匹配的路由。

1
2
3
4
5
6
7
8
9
function App() {
return (
<BrowserRouter basename="/app">
<Routes>
<Route path="/" /> {/* 👈 Renders at /app/ */}
</Routes>
</BrowserRouter>
);
}

Routers 会遍历自己所有的子元素,解析出组件的树结构,同样会交给上面处理 renderedMatches 的方法

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
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
function createRoutesFromChildren(children) {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element, index) => {
if (!React.isValidElement(element)) {
return;
}

let treePath = [...parentPath, index];

let route: RouteObject = {};

if (element.props.children) {
route.children = createRoutesFromChildren(
element.props.children,
treePath
);
}

routes.push(route);
});

return routes;
}
return useRoutes(createRoutesFromChildren(children));
}

全局拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const GlobalInterceptor = (props) => {
const location = useLocation();

if (!isAuthenticated() && location.pathname !== "/login")
return <Navigate to="/login" replace={true} />;
return props.children;
};

const App = () => {
return (
<Router>
<GlobalInterceptor>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</GlobalInterceptor>
</Router>
);
};

解析 React Transition Group ①

Transition Group 组件

Transition Group 组件用于管理一个 Transition 组件列表,即使 Transition 不声明状态 Transition Group 也会自动为其维护一个内部状态。

  • 自动为子组件列表添加状态并记录
  • 子组件添加或删除时不会被直接渲染,而是被 Transition Group 拦截,当执行完动画逻辑后,在内部状态中删除,并重新渲染。

首次渲染时可以记录子组件并吸收为内部状态,需要注意以下细节:

  • 子组件可能不合法,或没有 key
  • Transition Group 的属性优先级需要高于子组件相同属性的优先级
  • 需要实现清除逻辑给组件被删除时使用,从内部状态中清除被删除的组件。
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
const TransitionGroup = (props) => {
// 是否首次渲染
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);

const [, rerender] = useState([]);

const latestChildrenRef = useRef(props.children);
latestChildrenRef.current = props.children;

// handleOnExit 是一个组件内的方法
// 需要注意的是它通过 latestChildrenRef 保证 children 永远是最新的
// 因为组件可能被重新渲染,而 handleOnExit 方法可能已经被绑定在了组件上
// 因此,在它真正执行的上下文中无法获取到最近的 children 属性

const handleOnExit = (child, node) => {
const currentMap = getChildrenMapping(latestChildrenRef.current);

// onExit 执行的时候,这个元素可能已经没有了,在外部通过map重新渲染了新列表,所以已经计算的currentChildrenMap 不可靠
if (currentMap.has(child.key)) return;

if (child.props.onExited) {
child.props.onExited(node);
}

preChildrenMap.current.delete(child.key);
rerender([]);
};

const mappedChildren = firstRender.current
? getInitialChildMapping(props, handleOnExit)
: getNextChildrenMap(props, preChildrenMap.current, handleOnExit); // 更新时的逻辑
};

const getInitialChildMapping = (props, handleExit) => {
return getChildrenMapping(props.children, (child) => {
return React.cloneElement(child, {
// 在子组件退出的方法中处理删除内部状态的逻辑
onExited: handleExit(child),

// 初始化状态
in: true,

// Transition Group 的属性优先级需要高于子组件
appear: getProp(child, "appear", props),
enter: getProp(child, "enter", props),
exit: getProp(child, "exit", props),
});
});
};

const getChildrenMapping = (children, fn) => {
const childrenMap = new Map();
const mapper = (child) => {
return fn && React.isValidElement(child) ? fn(child) : child;
};

if (children) {
// map 函数可以自动添加key
React.Children.map(children, (child) => child).forEach((child) => {
childrenMap.set(child.key, mapper(child));
});
}
return childrenMap;
};

更新阶段, 新列表并不一定只会删除其中一个元素,而是可以添加或删除多个元素,而删除操作并不是立即执行的,而是当动画结束后,从内部状态中删除。

这需要一种策略合并两个列表。让旧组件尽量保持在原来的位置上,再将附近的新元素插入到就元素的前面。

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
const mergeMappingChildren = (preChildrenMap, currentChildrenMap) => {
const pre = preChildrenMap || new Map();
const next = currentChildrenMap || new Map();

let pendingQueue = [];
let prePendingMap = new Map();

// 如果新列表中有当前这个旧元素
// 记录这个旧元素之前的元素,放进列表
pre.forEach((preChild) => {
if (next.has(preChild.key)) {
if (pendingQueue.length) {
prePendingMap.set(preChild.key, pendingQueue);
pendingQueue = [];
}
} else {
pendingQueue.push(preChild.key);
}
});

const mergedChildrenMap = new Map();
next.forEach((nextChild) => {
// 新元素排在旧元素的前面
// 旧元素尽可能保持在原有的位置。
if (prePendingMap.has(nextChild.key)) {
const pendingQueue = prePendingMap.get(nextChild.key);
pendingQueue.forEach((key) => {
mergedChildrenMap.set(key, pre.get(key));
});
}
mergedChildrenMap.set(nextChild.key, next.get(nextChild.key));
});

if (pendingQueue.length) {
pendingQueue.forEach((preChild) => {
mergedChildrenMap.set(preChild.key, next.get(preChild.key));
});
}

return mergedChildrenMap;
};

const getNextChildrenMap = (props, preChildrenMap, handleExit) => {
const nextChildrenMap = getChildrenMapping(props.children);
const mergedChildrenMap = mergeMappingChildren(
preChildrenMap,
nextChildrenMap
);

const childrenMap = new Map();
mergedChildrenMap.forEach((child) => {
const preHas = preChildrenMap.has(child.key);
const nextHas = nextChildrenMap.has(child.key);
const isExiting = preHas && !preChildrenMap.get(child.key).props.in;

// 本次更新中被删除的元素
if (preHas && !nextHas && !isExiting) {
childrenMap.set(
child.key,
React.cloneElement(child, {
in: false,
})
);
// 新增的元素,包括正在退出中的元素
} else if ((!preHas || isExiting) && nextHas) {
childrenMap.set(
child.key,
React.cloneElement(child, {
onExited: () => handleExit(child),
in: true,
enter: getProp(child, "enter", props),
exit: getProp(child, "exit", props),
})
);
// 没有改变的元素
} else if (preHas && nextHas && isValidElement(prevChild)) {
childrenMap.set(
child.key,
React.cloneElement(child, {
onExited: () => handleExit(child),
in: preChildrenMap.get(child.key).props.in,
enter: getProp(child, "enter", props),
exit: getProp(child, "exit", props),
})
);
}
});
return childrenMap;
};

⑧抽取算法-模板方法模式

抽取公共算法

现在有一个场景,需要让你实现煮方便面和挂面,他们的过程如下。

挂面:

  • 加水
  • 水开后加入挂面
  • 一分钟后加入葱花
  • 装盘

方便面:

  • 加水
  • 水开后加入方便面
  • 一分钟后加入调料包
  • 装盘

可能第一反应会想到继承,父类中抽取加水和装盘的步骤,而中间两步因为实现不同所以不能抽取,而让子类去实现。

虽然中间的两步他们的逻辑相似但是针对的对象不同,一个需要加葱花,一个需要加调料包。而对与程序而言可以理解成算法相同而参数不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract class MakeNoodles {
public addWater() {}
// 加入某种类型的面
public addNoodle(noodleType: string) {
console.log("加入" + noodleType);
}
// 等一分钟加入调料
public waitMinuteAddCondiment(condiment: string) {
console.log("一分钟后加入" + condiment);
}
// 装盘
public sabot() {}
}

// 挂面
class MakeFineDriedNoodles extends MakeNoodles {
addNoodle(type: string) {
super.addNoodle(type);
}
waitMinuteAddCondiment(condiment: string) {
super.waitMinuteAddCondiment(condiment);
}
}

当然也可以使用组合的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type MakeNoodlesType = {
new (): any;
};
// 挂面
class MakeFineDriedNoodles {
makeNoodles: MakeNoodles;
constructor(MakeNoodlesConstructor: MakeNoodlesType) {
this.makeNoodles = new MakeNoodlesConstructor();
}
addNoodle(type: string) {
this.makeNoodles.addNoodle(type);
}
waitMinuteAddCondiment(condiment: string) {
this.makeNoodles.waitMinuteAddCondiment(condiment);
}
}

这就是模板方法模式的雏形,最大限度的抽取公共算法,而称为模板方法也是因为此模式经常作为方法调用,而仅限于用于类的继承。

模板方法模式

模板方法模式:在一个方法中定义一个算法骨架,而将一些步骤延续到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤

另外在类的模板方法中经常会定义 Hooks(钩子方法),为子类实现流程控制提供可能。

继承的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
abstract class MakeNoodles {
// 等一分钟加入调料
public waitMinuteAddCondiment(condiment: string) {
if (this.likeCondiment()) {
console.log("一分钟后加入" + condiment);
}
}
public likeCondiment() {
return true;
}
}

// 挂面
class MakeFineDriedNoodles extends MakeNoodles {
likeCondiment() {
return false;
}
waitMinuteAddCondiment(condiment: string) {
super.waitMinuteAddCondiment(condiment);
}
}

组合的方式:

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
class MakeNoodles {
// 等一分钟加入调料
public waitMinuteAddCondiment(condiment: string) {
if (this.likeCondiment()) {
console.log("一分钟后加入" + condiment);
}
}
public likeCondiment() {
return true;
}
}
type MakeNoodlesType = {
new (): any;
};
// 挂面
class MakeFineDriedNoodles {
makeNoodles: MakeNoodles;
constructor(MakeNoodlesConstructor: MakeNoodlesType) {
this.makeNoodles = new MakeNoodlesConstructor();
}
likeCondiment() {
return false;
}
waitMinuteAddCondiment(condiment: string) {
this.makeNoodles.likeCondiment = this.likeCondiment;
this.makeNoodles.waitMinuteAddCondiment(condiment);
}
}

如果对于某些算法是可选的,可以考虑使用 Hooks, 而 Hooks 不一定只是子类控制模板的算法流程,也能使是子类直接使用模板算法里面的 Hooks 控制子类的逻辑。

而这种子类和父类互相调用的场景经常存在,这也涉及到一个原则 避免底层和高层组件之间有明显的环状依赖。

方法中的模板模式

很多场景下模板模式体现的并不明显,如 lodash 中的 add 方法.

1
const add = createMathOperation((augend, addend) => augend + addend, 0);

add 方法中,你可以将传入的回调函数看作是模板方法,但并没有直接返回相加的结果,而是将相加的算法传入 createMathOperation, 而在这个方法中处理数据类型转换的问题。这个方法补充了模板方法的空白。

你也可一把他看作是装饰方法,但不是装饰模式,装饰模式通常会被定义为装饰者和被装饰者实现相同的接口。

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信