⑩状态模式

避免陷入在 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, 而在这个方法中处理数据类型转换的问题。这个方法补充了模板方法的空白。

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

解析 React Transition Group ①

Transition 组件

优先设计以下用户接口,只考虑最核心的功能。

  • 高阶组件的形式使用
  • in 属性,控制元素是否显示

关键在于如何处理状态变化,因为要实现状态切换,需要避免在同一渲染帧中执行。 实现一个 hooks 专门用管理状态改变。

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
const useTransitionState = (props) => {
const stateRef = useRef();
const propsRef = useRef();

const [state, setState] = useState(() => {
if (props.in) {
if (props.appear) return "exited";
return "entered";
} else {
return "exited";
}
});

stateRef.current = state;
propsRef.current = props;

const next = useCallback(() => {
const state = stateRef.current;
const props = propsRef.current;

let nextState = null;
if (props.in) {
if (
(props.appear && state === "exited") ||
(state !== "entering" && state !== "entered")
) {
nextState = "entering";
}

if (!props.enter) {
nextState = "entered";
}

if (state === "entering") {
nextState = "entered";
}
} else {
if (state === "entering" || state === "entered") {
nextState = "exiting";
}
if (!props.exit) {
nextState = "exited";
}

if (state === "exiting") {
nextState = "exited";
}
}

if (nextState !== null) setState(nextState);

return nextState;
}, []);

return [state, next];
};

调用 next 方法即可完成状态切换,在组件渲染后调用 next 实现状态切换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
useEffect(() => {
let nextState = next();

if (nextState === null) return;

if (nextState === "entering") {
return transitionEnd(
props.timeout,
safeCallback(() => {
next();
})
);
}

if (nextState === "exiting") {
return transitionEnd(
props.timeout,
safeCallback(() => {
next();
})
);
}
}, [next, props.in, props.timeout]);

在状态切换的过程中,如果状态改变,如何清除副作用?

当通过用户交互修改了 in 属性,会再次进入到 useEffect 函数中,在进入会执行清理函数, 清理函数需要可以清除执行到一半的中间状态。

如果使用计时器可以执行 clear 方法,也可以实现一个清除函数,它不会清除定时器的执行,但是会让回调函数失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const safeCallback = (callback) => {
let active = true;

const nextCallback = () => {
if (active) {
callback();
}
};

nextCallback.cancel = () => {
active = false;
};
return nextCallback;
};

更加细节的处理:

  • 考虑组件的类型,函数还是组件

    1
    2
    3
    4
    5
    if (typeof children === "function") {
    renderChildren = children(state);
    } else {
    renderChildren = React.Children.only(children);
    }
  • 控制进入和退出状态切换是否产生动画 exit enter,需要跳过中间状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (
    (props.appear && state === "exited") ||
    (state !== "entering" && state !== "entered")
    ) {
    nextState = "entering";
    }

    if (!props.enter) {
    nextState = "entered";
    }

    if (state === "entering") {
    nextState = "entered";
    }
  • 实现 appear 首次挂载的时候是否展示动画
    如果需要首次渲染时执行动画,那么即使 in 为 true,内部状态也要初始化为退出状态。然后通过 next 方法进行状态切换

    1
    2
    3
    4
    5
    6
    7
    8
    const [state, setState] = useState(() => {
    if (props.in) {
    if (props.appear) return "exited";
    return "entered";
    } else {
    return "exited";
    }
    });
  • timeout 可以为退出,进入单独配置

  • 剩余属性透传

⑦适配器模式-外观模式

适配器模式

适配器就像是转换国内充电器和香港充电器的插座一样,能让原本可以使用的插座在另一个地方也能正常使用。

而转换的过过程中,并没有修改原有插座的功能,只是将他的功能进行转换,这也是适配器的核心逻辑,有别于外观模式,和之前的装饰者模式。

适配器模式:将一个类的接口,转换为消费方期望的另一个接口。适配器让原本不兼容的类可以正常使用。, 而装饰着模式通常为类提供额外的功能接口,已达到增强类的目的。

真实的场景中适配器模式可以适配对象或类。

通常对类的扩展操作会考虑继承,但是 JS 中的扩展方式很灵活,也可以通过函数的形式对对象或类进行操作。

通过一个适配器,为对象实现 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
32
33
34
35
36
37
38
39
40
41
const obj = {
a: 1,
b: 2,
};

const objIteratorAdapter = (
obj: Record<string, any>
): Record<string, any> & { [Symbol.iterator]: () => any } => {
const iterator = (): { next: () => any } => {
let keys = Object.keys(obj);
let index = 0;
return {
next() {
return index < keys.length
? {
value: obj[keys[index++]],
done: false,
}
: {
value: undefined,
done: true,
};
},
};
};
const iteratorObj = Object.create(obj, {
[Symbol.iterator]: {
value: iterator,
writable: false,
enumerable: false,
configurable: false,
},
});

return iteratorObj;
};

const newObj = objIteratorAdapter(obj);
for (let i of newObj) {
console.log(i);
}

外观模式

外观模式提供了统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统的接口更容易使用。

外观模式并不会封装新的功能,而是封装了一段子系统的接口。

虽然外观模式定义简单,但是引出了一个设计理念, 最少知识原则,只和你的密友谈话。

在对象的方法调用中,应该遵循几个原则:

  • 调用该对象本身的方法
  • 调用当作参数出入对象中的方法
  • 调用通过此方法创建或实例化对象中的方法
  • 调用对象实例中引用的对象中的方法

⑥封装调用-命令模式

解耦调用者和请求者

现在有 2 个开关需要控制 2 台设备的开机和停止。设备的开机和停止是一个繁琐的过程,需要设置一些参数,并经历几步操作。

如果我们把对机器的操作都写在开关的实现中,一旦开关流程改变或是更换了设备,都需要重新修改开关的代码。

所以我们考虑先把设备的开机和停止过程封装在一个对象方法里面,开关只要直接调用这个对象的方法就能实现设备开始和停止,不需要关心实现的细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface ICommand {
execute(): void;
}

class Device1 implements ICommand {
setOption() {}
step1() {}
step2() {}
on() {}
execute() {
this.setOption();
this.step1();
this.step2();
this.on();
console.log("设备已经开启");
}
}

开关类接受命令类,并直接调用命令类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ControlA {
command: ICommand;
constructor(command: ICommand) {
this.command = command;
}
setCommand(command: ICommand) {
this.command = command;
}
on() {
this.command.execute();
}
}

const controlA = new ControlA(new Device1());
controlA.on();

对于某些按钮,可能没有实现 execute 方法,而我们又不想通过 ifelse 判断破坏代码风格,可以使用占位符类。

1
2
3
4
5
6
class NoCommand implements ICommand {
execute() {}
}

const controlB = new ControlA(new NoCommand());
controlB.on();

命令模式

将请求封装成对象,以便使用不同的请求,队列,或日志来参数化其他的对象,命令模式也支持可撤销的操作。

宏命令

把所有的命令对象通过一个类编排起来,统一执行,就实现了宏命令

1
2
3
4
5
6
7
8
9
10
11
class MacroCommand implements ICommand {
commandStack: ICommand[] = [];
setCommand(command: ICommand) {
this.commandStack.push(command);
}
execute(): void {
for (let i = 0; i < this.commandStack.length; i++) {
this.commandStack[i].execute();
}
}
}
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信