阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

组件通信: EventBus 的原理解析与应用

在开发复杂的单页面应用时,我们经常会遇到一个问题:如何高效地在组件或模块之间进行通信?这里,EventBus(事件总线)就派上了用场。简单来说,EventBus 是一种设计模式,它允许不同组件或模块之间通过事件来通信,而无需直接引用彼此。

EventBus 是传统的组件通信解决方案,下面我们将讲解 EventBus 跨组件通信的原理、实现方式以及该如何使用。

原理解析

EventBus 的核心在于提供一个中央机制,允许不同的组件或模块相互通信,而不必直接引用对方。它是一种典型的发布-订阅(pub-sub)模式,这是一种广泛使用的设计模式,用于解耦发送者和接收者。

在这个模式中,EventBus 充当了一个中介的角色:它允许组件订阅那些它们感兴趣的事件,并在这些事件发生时接收通知。同样,当某个事件发生时,比如用户的一个动作或者数据的变化,EventBus 负责将这一消息广播给所有订阅了该事件的组件。

它基于三个核心操作:注册事件(on(event, callback))、触发事件(emit(event, ...args))、以及移除事件(off(event, callback))。因此,EventBus 的基本代码可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
on(event, callback) {
// 注册事件监听器
}

emit(event, ...args) {
// 触发事件
}

off(event, callback) {
// 移除事件监听器
}
}

显然,我们需要有一个私有变量来储存用户的函数,此时为类添加 events 属性。events 属性是一个对象映射,其中每个属性表示一个事件名称,对应的值是一个回调函数的数组,这个数组存储了所有订阅了该事件的回调函数。

1
2
3
4
class EventBus {
private events: Record<string, Function[]> = {};
// ...
}

当用户执行订阅事件 on 时,回调函数会被添加到相应事件名称的数组中。这样,同一个事件可以被不同组件或模块订阅,而每个订阅者的回调函数都会被正确地保存在事件队列中。最后,当触发事件 emit 时,事件队列中的每个回调函数都会被执行,实现了事件的触发和通知功能。若已经没有订阅需求,则可以通过 off 移除已经订阅的事件。

代码实现

接下来我们按照前文所述完善我们的代码实现:

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
class EventBus {
// 事件存储对象,用于保存不同事件的回调函数
private events: Record<string, Function[]> = {};

/**
* 注册事件监听器
* @param eventName - 事件名称
* @param callback - 回调函数,当事件触发时执行
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public on(eventName: string, callback: Function): this {
// 检查回调函数是否为函数类型
if (typeof callback !== "function") {
throw new Error("EventBus 'on' method expects a callback function.");
}

// 如果事件不存在,创建一个空数组用于存储回调函数
if (!this.events[eventName]) {
this.events[eventName] = [];
}

// 将回调函数添加到事件的回调函数列表中
this.events[eventName].push(callback);

// 支持链式调用
return this;
}

/**
* 触发事件
* @param eventName - 要触发的事件名称
* @param args - 传递给回调函数的参数
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public emit(eventName: string, ...args: any[]): this {
// 获取事件对应的回调函数列表
const callbacks = this.events[eventName];
if (callbacks) {
// 遍历执行每个回调函数,并传递参数
callbacks.forEach((callback) => callback(...args));
}

// 支持链式调用
return this;
}

/**
* 移除事件监听器
* @param event - 要移除的事件名称或事件名称数组
* @param callback - 要移除的回调函数(可选)
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public off(event?: string | string[], callback?: Function): this {
// 清空所有事件监听器
if (!event || (Array.isArray(event) && !event.length)) {
this.events = {};
return this;
}

// 处理事件数组
if (Array.isArray(event)) {
event.forEach((e) => this.off(e, callback));
return this;
}

// 如果没有提供回调函数,则删除该事件的所有监听器
if (!callback) {
delete this.events[event];
return this;
}

// 移除特定的回调函数
const callbacks = this.events[event];
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}

// 支持链式调用
return this;
}
}

当涉及到一次性的事件监听需求时,我们可以进一步扩展 EventBus,以支持一次性事件监听。允许用户在某个事件触发后,自动移除事件监听器,以确保回调函数只执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
// other code ...
public once(eventName: string, callback: Function): this {
const onceWrapper = (...args: any[]) => {
this.off(eventName, onceWrapper);
callback(...args);
};

this.on(eventName, onceWrapper);

return this;
}
}

使用方式

我们将类的封装到 event-bus.ts 中,通过模块的来管理:

1
2
3
export class EventBus {
// ...
}

我们现在已经封装好了一个类,若我们像使用则需要实例化。此处再文件内直接实例化一个类:

1
2
// 创建 EventBus 实例并导出
export const eventBus = new EventBus();

这样使用时可以提供两种方式:

  1. 引入已经实例化的 eventBus

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { eventBus } from './event-bus';

    // 订阅事件
    eventBus.on('eventName', callback);

    // 触发事件
    eventBus.emit('eventName', data);

    // 移除事件
    eventBus.off('eventName', callback);
  2. 需要多个独立的事件总线实例时,或者希望在不同模块或组件之间使用不同的事件总线时,可以选择额外实例化 eventBus。这样做的目的可能是为了隔离命名的冲突、组件与模块逻辑隔离等原因。

    1
    2
    3
    4
    5
    6
    // events.ts
    import { EventBus } from './event-bus';

    // 创建独立的事件总线实例
    export const eventBusA = new EventBus();
    export const eventBusB = new EventBus();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import {eventBusA, eventBusB} from './events'

    // 在不同模块或组件中使用不同的事件总线
    eventBusA.on('eventA', callbackA);
    eventBusB.on('eventB', callbackB);

    // 触发不同事件总线上的事件
    eventBusA.emit('eventA', dataA);
    eventBusB.emit('eventB', dataB);

以下是 CodeSandbox 的演示代码:

总结

在本文中,我们深入探讨了 EventBus 的原理,了解了它是如何工作的。我们学习了它的核心操作。除了本文所提及的实现方式,有时候在生产项目中,为了确保代码的可靠性,我们可以考虑使用成熟的第三方库,例如 mitttiny-emitter

这些库已经经过广泛的测试和使用,可以提供稳定和可靠的 EventBus 功能。

🔲 ⭐

数据结构实践

本篇将根据自考实践要求对「数据结构」一科进行简要的复习,代码实现使用 C++ 语言实现。

实践

已知 Q 是一个非空队列,S 是一个空栈。编写算法,仅用队列和栈的 ADT 函数和少量工作变量,将队列 Q 的所有元素逆置。

栈的基本 ADT 函数有:

  1. 置空栈。函数原型为: void MakeEmpty(SqStack s);
  2. 元素e入栈。函数原型为: void Push(SqStack s,ElemType e);
  3. 出栈,返回栈顶元素。函数原型为: ElemType pop(SqStack s);
  4. 判断栈是否为空。函数原型为: int isEmpty(SqStack s);

队列的基本ADT函数有:

  1. 元素e入队。函数原型为:void enQueue(Queue q,ElemType e);
  2. 出队,返回队头元素。函数原型为:ElemType deQueue(Queue q);(3)(3)判断队是否为空。函数原型为:int isEmpty(Queue q);

题目要求:

  1. 编程实现队列和栈的ADT函数
  2. 仅用队列和栈的ADT函数和少量工作变量,编写将队列Q的所有元素逆置的函数
  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
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
// 栈的基本 ADT 函数有:

// 1. 置空栈。函数原型为: `void MakeEmpty(SqStack s);`
// 2. 元素e入栈。函数原型为: `void Push(SqStack s,ElemType e);`
// 3. 出栈,返回栈顶元素。函数原型为: `ElemType pop(SqStack s);`
// 4. 判断栈是否为空。函数原型为: `int isEmpty(SqStack s);`
#include <iostream>

using namespace std;

#define StackSize 10
typedef int ElemType;

// 栈结构
class SqStack {
private:
ElemType data[StackSize];
int top;
public:
SqStack(): top(-1) {}

// 1. 置空栈
void makeEmpty() {
this->top = -1;
}

// 2. 元素e入栈
void push(ElemType e) {
if (this->isFull()) {
std::cout << "栈满" << std::endl;
return;
}

this->data[++this->top] = e;
}

// 3. 出栈,返回栈顶元素
ElemType pop() {
if (this->isEmpty()) {
std::cout << "栈空" << std::endl;
return -1;
}

return this->data[this->top--];
}

// 4. 判断栈是否为空
bool isEmpty() {
return this->top == -1;
}

// 5. 栈满
int isFull() {
return this->top == StackSize;
}
};

// 队列的基本ADT函数有:

// (1)元素e入队。函数原型为:void enQueue(Queue q,ElemType e);
// (2)出队,返回队头元素。函数原型为:ElemType deQueue(Queue q);(
// (3)判断队是否为空。函数原型为:int isEmpty(Queue q);
#define QueueSize 10

// 队列结构
class Queue {
private:
ElemType data[QueueSize];
int front, real;
public:
Queue(): front(0), real(0) {}

// 队列是否已满
int isQueueFull() {
return (this->real + 1) % QueueSize == this->front;
}

// 元素e入队
void enQueue(ElemType e) {
if (isQueueFull()) {
std::cout << "队列满" << std::endl;
return;
}

this->data[this->real] = e;
// 循环意义下的 +1
this->real = (this->real + 1) % QueueSize;
}

// 出队列
ElemType deQueue() {
if (this->isEmpty()) {
std::cout << "队列空" << std::endl;
return -1;
}

ElemType e = this->data[this->front];
this->front = (this->front + 1) % QueueSize;

return e;
}

// 判断队列是否为空
int isEmpty() {
return this->front == this->real;
}
};

// 队列元素倒序, 这里注意要用 & 取引用才有副作用
void reverseQueue(Queue &q) {
SqStack s;
int val;

while (!q.isEmpty()) {
val = q.deQueue();
s.push(val);
}

while (!s.isEmpty()) {
val = s.pop();
q.enQueue(val);
}
}

// (1) 编程实现队列和栈的ADT函数
// (2) 仅用队列和栈的ADT函数和少量工作变量,编写将队列Q的所有元素逆置的函数。
// (3) 测试该函数
int main() {
cout << "准备测试 stack 数据结构" << endl;
SqStack s;

cout << "[stack] 1. test SqStack.push" << endl;
int testData1[] = {109, 108, 107};
for (int i = 0; i < 3; i++) {
s.push(testData1[i]);
}

cout << "[stack] 2. test SqStack.isEmpty: " << (s.isEmpty() ? "" : "非") << "空栈" << endl;
cout << "[stack] 3. test SqStack.pop: " << s.pop() << endl;
cout << "[stack] 4. test SqStack.makeEmpty" << endl;
s.makeEmpty();

cout << "[stack] 5. check stack now is empty: " << (s.isEmpty() ? "" : "非") << "空栈" << endl;
cout << "============================================" << endl;

cout << "准备测试 queue 数据结构" << endl;
Queue q;

cout << "[queue] 1. test SqStack.push" << endl;
for (int i = 0; i < 3; i++) {
q.enQueue(testData1[i]);
}

cout << "[queue] 2. test Queue.isEmpty: " << (q.isEmpty() ? "空队列" : "非空队列") << endl;
while (!q.isEmpty()) {
cout << "[queue] 3. test Queue.pop: " << q.deQueue() << endl;
}

cout << "[queue] 4. check queue now is empty: " << (q.isEmpty() ? "空队列" : "非空队列") << endl;
cout << endl << endl;
cout << "============================================" << endl;

cout << "仅用队列和栈的ADT函数和少量工作变量,编写将队列Q的所有元素逆置的函数。" << endl;
const int reverseTestData[] = {11,12,13,14,15};
for (int i = 0; i < 5; i++) {

q.enQueue(reverseTestData[i]);
}
reverseQueue(q);

while(!q.isEmpty()) {
cout << "reverseQueue deQueue: " << q.deQueue() << endl;
}

return 0;
}

排序

选择排序

基本思想: 每一趟在待排序的记录中选出关键字最小的记录,依次存放在已排好序的记录序列的最后,直到全部排序完为止。

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

#include <iostream>
using namespace std;

void SelectSort(int arr[], int n) {
int k;
for (int i = 0; i < n; i++) {
k = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[k]) {
k = j;
}
}

if (k != i) {
swap(arr[i], arr[k]);
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
}

int main() {
int arr[] = {4, 3, 2, 9, 8, 6, 7, 1, 5, 10};
int n = sizeof(arr) / sizeof(arr[0]);
cout << sizeof(arr[0]) << "\n";

SelectSort(arr, n);

for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
}

插入排序

基本思想: 每次将一个待排序的记录按其关键字的大小插入到前面已经排序好的文件中的适当位置,直到全部记录插入完位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void InsertSort(int arr[], int n) {
int i, j, tmp;
// 对顺序表做直接插入排序
for(i = 1; i < n; i++) {
// 当前值比上一个值小,则交换位置
if (arr[i] < arr[i - 1]) {

tmp = arr[i];
// 对有序区逐项向后 diff,寻找合适的插入位置
for(j = i - 1; j >= 0 && tmp < arr[j]; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = tmp;
}
}
}

冒泡排序

冒泡排序的基本思想是:通过相邻元素之间的比较和交换,使娇小的元素逐渐从底部移向顶部,就像水底下气泡一样逐渐向上冒泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void BubbleSort(int *arr, int n) {
int i, j, flag, temp;
for (i = 0; i < n; i++) {
flag = 0;

// 从右向左对比
for (j = n - 1; j >= i; j--) {
if (arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
// temp = arr[j];
// arr[j] = arr[j - 1];
// arr[j - 1] = temp;
flag = 1;
}
}

if (flag == 0) return;
}
}
🔲 ⭐

计算机网络原理笔记

计算机网络原理学习笔记。

目录

下面目录定位有些使用不了。若需要跳转到具体段落应使用侧边栏文章定位


计算类速览

速率与带宽

  1. 传输时延:链路发送到结束所用的时间

    1
    dt = L(分组长度) / R(链路带宽)
  2. 传播时延: 从发送端到接收端传输所需的时间

    1
    dp = D(链路长度) / V(信号传播速度)
  3. 时延带宽(乘)积:传播时延和链路带宽的乘积

    1
    G = dp(传播时延) * R(链路带宽)

TCP 报文段确认号

题目一般是主机 A 会发送两个 TCP 报文段给主机 B,其实有字节序号分别为 n1 和 n2。

  1. 算携带了多少字节: 字节数=n2-n1
  2. 接收到第一个报文段返回的确认号是 n2
  3. 如果主机 B 接收到第二个报文段后的确认号是 n3, 问第二个携带多少字节: 字节数=n3-n2
  4. 如果第一段丢失了,第二段到达了,主机 B 返回的确认号是: n1, 即要求主机 B 重传之前没有接受到的数据

汉明距离

两个等长码字之间的,对应位不同的位数,成为两个码字的汉明距离。汉明距离是两个码字进行按位异或后 1 的个数。

1
2
3
4
5
6
01100101
10011101
-------- 进行异或(^)
11111000
--------
5个1,汉明距离 = 5

循环冗余码

多项式 G(x)=x4 + x3 + 1,对位串 101100111101 进行 CRC 编码,结果为:

G(x)=x4 + x3 + 1 对应的比特位为 11001,则在待编位串后面添加 0000.

位串除 11001


计算机网络概述

常见应用的端口号

应用端口号
TCP/FTP21
SMTP25
HTTP80
POP3 服务器110

TCP/IP、OSI 参考模型

OSI模型单位
7. 应用层报文
4. 传输层数据报/报文段
3. 网络层分组/包
2. 数据链路层
1. 物理层比特流

简述OSI参数模型物理层的主要功能及该层协议规定的四个特性。

物理层的主要功能是实现比特流的透明传输,为数据链路层提供数据传输服务。

物理层协议规定的特性包括:

  1. 机械特性
  2. 电气特性
  3. 功能特性
  4. 规程特性

应用层

网络协议的三要素包括

  • 语法: 定义实体之间交换信息的格式与结构
  • 语义: 定义实体之间交换的信息中需要发送哪些控制信息,这些信息的具体含义,以及针对不同含义的控制信息,接收信息端应如何响应。
  • 时序: 定义实体之间交换信息的顺序以及如何匹配或适应彼此的速度

简述典型的HTTP请求方法及其作用

  1. GET: 读取由 URL 所标识的信息
  2. POST: 给服务器添加信息
  3. HEAD: 请求读取由 URL 所标识的信息首部,无需在相应报文中包含对象
  4. OPTION: 请求一些选项的信息
  5. PUT: 在指明的 URL 下存储一个文档

简述 POP3 协议交互过程

POP3 是邮件读取协议,可用于接收邮件。

  1. 授权阶段: 用户代理需要向邮件服务器发送用户名和口令,服务器鉴别用户身份,授权访问邮箱。
  2. 事务处理阶段: 用户代理向邮件服务器发送 POP3 命令,实现邮件读取,为邮件做删除编辑、取消邮件删除标记以及获取邮件的统计信息等操作。
  3. 更新阶段: 客户发出来 quit 命令,结束 POP3 回话,服务器删除哪些被标记为删除的邮件。

传输层

传输层核心任务:为应用进程之间提供端到端的逻辑通信服务。

TCP/IP

核心层: 传输层
网络互联层核心协议: IP 协议

简述传输层所实现的功能

实现的功能:

  1. 传输层寻址
  2. 对应用层报文进行分段和重组
  3. 对报文进行差错检测
  4. 实现进程间端到端的可靠数据传输控制
  5. 面向应用层实现复用与分解
  6. 流量控制
  7. 拥塞控制

简述传输层实现可靠数据传输的主要措施

不可靠传输信道在数据传输中可能发生:

  1. 比特差错
  2. 乱序
  3. 数据丢失
  1. 差错控制: 利用差错编码实现数据报传输过程中的比特差检测(甚至是纠正)。
  2. 确认: 「接收方」向「发送方」反馈接受状态
  3. 重传: 「发送方」重新发送「接收方」没有正确接收到的数据
  4. 序号: 确保数据按序提交
  5. 计时器: 解决数据丢失问题

简述保证网络传输可靠性的确认与重传机制的概念

  • 确认是指数据分组接受节点再收到每个分组后,要求想发送节点会送正确接受分组的确认信息。
  • 在规定时间内,如果发送节点没有接收到「接收方」返回的确认信息,就认为该数据分组发送失败,发送节点会重传该数据分组。

简述差错控制的概念以及差错控制的基本方法

差错控制就是通过差错编码技术实现对信息传输的检测,并通过某种机制进行差错纠正和处理。

差错检测的基本方法有:

  1. 检错重发
  2. 检错丢弃
  3. 前向纠错
  4. 反馈校验

简述TCP所提供的面向连接服务

在生成报文开始传送之前,TCP 客户和服务器相互交换传输层的控制信息,完成握手。在客户进程与服务器进程的套接字之间建立一条逻辑的 TCP 连接。

简述为 UDP 套接字分配端口号的两种方法

  1. 传输层自动分配: 创建一个 UDP 套接字时,传输层自动为该套接字分配一个端口号,该端口号当前未被该主机任何其他 UDP 套接字使用。
  2. 手动绑定: 在创建 UDP 套接字后,通过调用 bind 函数来绑定一个特定的端口号。

简述 UDP 提供的服务的主要特征

  1. 应用进程更容易控制发送什么数据以及什么时候发送。
  2. 无需建立连接
  3. 无连接状态
  4. 首部开销小,仅有8字节的开销

网络层

网络层提供的功能有:

  1. 连接建立
  2. 路由
  3. 转发

简述虚电路的概念及其构成要素

虚电路是源主机到目的主机的一条路径上建立的一条网络层逻辑连接,成为虚电路。

comment: 因为是逻辑连接,不是真实的电路连接,故称为虚电路

一条虚电路由 3 个要素组成:

  1. 从源主机到目的主机之间的一条路径
  2. 该路径上每条链路各有一个虚电路标记(VCID)
  3. 该路径上每台分组交互机的转发表记录虚电路标识的接续关系

虚电路交换和数据交换的主要差别

  • 虚电路网络通常由网络完成顺序控制、差错控制和流量控制等功能,向端系统提供无差错数据传送服务,而端系统则可以很简单。
  • 数据报网络的顺序控制、差错控制和流量控制等功能需要由端系统完成,网络实现的功能很简单,比如基本的路由与转发功能。

电路交换的特点和优缺点

电路交换的特点是有连接的,在通信时需要先建立电路连接,在通讯过程中独占一个信道,在通讯结束后需要拆除电路连接。

优点: 实时性高,时延和时延抖动都较小
缺点: 对于突发性数据传输,信道利用率低,且传输速率单一。

简述永久虚电路与交换虚电路的区别

永久虚电路是一种提前建立、长期使用的虚电路,虚电路的建立时间开销基本上可以忽略。
交换虚电路是根据通信需要而临时建立的虚电路,通信结束后立即拆除,虚电路的建立和拆除时间有时相对影响较大。

简述路由器输入端口接受与处理数据的过程

输入端口接受信号,还原数据链路层帧,提取 IP 数据报,根据 IP 数据报的目的 IP 地址检索路由表,决策将数据报交换到哪个输出端口


数据链路层与局域网

数据链路层提供的服务有:

  1. 组帧
  2. 链路接入
  3. 可靠交付
  4. 差错控制

帧的组成

HDLC: 帧组成:

  1. 管理帧
  2. 信息帧
  3. 无序号帧

IEEE 802.11 帧:

  1. 管理帧
  2. 控制帧
  3. 数据帧

PPP (point to point protocol) 数据帧结构:

  1. 标志(01111110)
  2. 地址(11111111)
  3. 控制(00000011)
  4. 协议
  5. 信息
  6. 校验和
  7. 标志(01111110)

==== 多路访问控制协议 ====

非坚持 csma 的基本原理

  • 若通信站有数据发送,先监听信道,若发现信道空闲,则立即发送数据(与 1-坚持 CSMA 第一步一致)
  • 若发现信道忙,则等待一个随机时间,然后再重新监听信道,尝试发送数据。
  • 若发送数据时产生冲突,则等待一个随机时间,然后重新开始监听信道,尝试发送数据。

这是个做事不太着急的协议。将上面文绉绉的描述用通俗的话来理解是:它在寝室中想要去洗澡

  1. 它会先看看有没有人在用浴室,没人在用就直接去洗澡
  2. 去洗澡时发现有人也想用了,它会礼让给其他人。自个再晚一段时间再看看还有没有人用,没人用就自个用了
  3. 如果已经有人在用浴室了,那又晚点再看看

1-坚持 csma 的基本原理

  • 若通信站有数据发送,先监听信道,若发现信道空闲,则立即发送数据(与 非坚持 CSMA 第一步一致)
  • 若发现信道忙,则继续监听信道,直至发现信道空闲,然后立即发送数据。

通俗话理解: 顾名思义,坚持不懈。如果浴室有人用了,我就守在门口。有人出来我就立马进去。

==== 局域网 ====

简述地址解析协议 ARP 的作用和基本思想

ARP 用于根据本网内目的主机默认网关的 IP 地址获取其 MAC 地址。

基本思想是: 在每一台主机中设置专用内存区域作为 ARP 高速缓存区域,储存该主机所在局域网中其他主机和路由器(默认网关)的 IP 地址与 MAC 地址之间的映射,并且要经常更新这个映射表。

ARP 在局域网中通过广播 ARP 查询报文的方式,来询问某目的站的 IP 地址对应的 MAC 地址,即知道本网内某主机的 IP 地址就能知道它的 MAC 地址。

简述虚拟局域网(VLAN)的概念以及划分方法

虚拟局域网是一种基于交换机的逻辑分隔广播域的局域网应用形式。划分方法主要有 3 种:

  1. 基于交换机端口划分
  2. 基于 MAC 地址划分
  3. 基于上层协议或地址划分

物理层

简述 CMI 码的编码规则,并画出二进制比特序列 1011010011 的 CMI 码信号波形

CMI 码的编码规则是将信息码的 0 编码为双极不归零码的 01,信息码的 1 交替编码为双极不归零码的 11 和 00。

米勒码的编码规则

P229

  1. 信息码的 1 编码为「双极非归零码」的 01 或 10(占半格)
  2. 信息码连 1 时,后面的 1 要换编码
  3. 信息码的 0 编码为 00 或 11,中间码元不跳变(占一格)
  4. 单个 0 时不跳变
  5. 多个 0 时,间隔跳变
  6. (备注): 有两极

无线与移动网络

简述 4 个 IEEE 802.11 标准具有的共同特征

  1. 都使用相同介质访问协议 CSMA/CA。
  2. 链路层帧使用相同的帧格式
  3. 都具有降低传输速率以传输更远距离的能力
  4. 都支持“基础设施模式”和“自组织模式”两种模式

简答题

每个 AS 可以通过 BGP(边界网关协议) 实现哪些功能

AS: Autonomous system, 自治系统

  1. 从相邻 AS 获取某子网的可达性信息。
  2. 向本 AS 内部的所有路由器传播跨 AS 的某子网可达性信息。
  3. 基于某子网可达性信息和 AS 策略,觉得到达该子网的最佳路由

简述数字签名应满足的要求

  1. 接收方能够确认或证实发送方的签名,但不能伪造
  2. 发送发发送签名给接受方后,就不能否认他所签发的信息
  3. 接收方对已收到的签名信息不能再否认,既有收报认证
  4. 第三者可以确认收发双方之间的消息传送,但不能伪造这一过

基础计算

十进制转二进制

十进制转二进制主要的方法是除2取余,逆序排列法

可以写一个简单的 js 函数打印每次计算的结果。例如将整数 251 转为二进制的过程是:

点击展开详细代码
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
function convertToBinary(n, buffer = []) {
const a = Math.floor(n / 2)
const b = n % 2;

buffer.push(b);
console.log(`${n} / 2 = ${a}...${b}`)

if (a === 0) {
const result = buffer.reverse().join('');
console.log(`将每次取到的余数逆转排序后,最终转换后的二进制是: ${result}`)

return result;
}

return convertToBinary(a, buffer);
}

convertToBinary(521);

/**
* 521 / 2 = 260...1
* 260 / 2 = 130...0
* 130 / 2 = 65...0
* 65 / 2 = 32...1
* 32 / 2 = 16...0
* 16 / 2 = 8...0
* 8 / 2 = 4...0
* 4 / 2 = 2...0
* 2 / 2 = 1...0
* 1 / 2 = 0...1
* 将每次取到的余数逆转排序后,最终转换后的二进制是: 1000001001
*/

// js 的 toString 方法还可以将数值转为指定进制
var fn = (n, base = 2) => n.toString(base);

fn(521); // "1000001001"

2^n 速查表

2 的 N 次方速查表
次方
2^12
2^24
2^38
2^416
2^532
2^664
2^7128
2^8256
2^9512
2^101024
2^112048
2^124096
2^138192
2^1416384
2^1532768
2^1665536
2^17131072
2^18262144
2^19524288
2^201048576

子网掩码速览

类别子网掩码十进制子网掩码二进制
A255.0.0.011111111 00000000 00000000 00000000
B255.255.0.011111111 11111111 00000000 00000000
C255.255.255.011111111 11111111 11111111 00000000

通过 IP 地址与子网掩码推算出其他信息

1
2
1. 子网地址: 主机 IP 地址 & 子网掩码
2. 广播地址: 子网地址 | 子网掩码反码
🔲 ⭐

闭包与链式设计的使用示例

最近遇到了个按需请求数据的需求,非常适合用于讲解闭包与链式设计的例子,故来分享一下思路。

大致需求如下: 目前有个 list, list 中每项 item 都是可展开的折叠项。当展开某个折叠项时,需要根据 item 的 code 另外去取 name 的映射。考虑到列表的数据量非常大,且一次性查询过多 code 时,接口的查询效率会明显降低,故采用按需请求映射的方案。

屏蔽与本例无关的属性,瘦身后的 list 数据结构大致如下:

1
2
3
4
5
6
interface DataType {
code: string;
paymentTransaction: string[];
}

type ListType = DataType[];

我们知道大型企业中的数据会比较复杂,比较常见的一种情况是数据中有一个 id 或 code 是用于跟另一个数据项相关联的。学习过数据库的同学很容易就联想到了外键这个概念。

现在我们就要取出这些 code 发送给服务端去查询。考虑到 code 可能会有重复,因此可以将 codes 存入 Set 中,利用 Set 的特性去重。除此之外,为了使 name 映射可以被复用,每次从接口返回的 name 映射将会被缓存起来。若下次再触发事件时有对应的 key,便不再查询。

我们可以将这段逻辑抽离出来作为一个依赖收集的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const mapping = new Map();

function collectionCodes(initCodes?: string[] | Set<string>) {
const codes = new Set<string>(initCodes)

return {
append(code: string) {
if (!mapping.has(code)) {
codes.add(code);
}

return this;
},
empty() {
return !codes.size;
},
value() {
return codes;
},
}
}

collectionCodes 函数是用于收集 codes。它内部利用了闭包的特性将 codes 缓存了起来,并且在添加新的 code 之前会判断 code 在 local 的映射中是否已经存在。append 返回的 this 是经典的链式调用设计,允许多次链式添加。当本次依赖收集结束后,调用 value 方法获取最终的 codes。

可以写一些简单的 mock 数据进行尝试:

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
function handleNameMapping(data: DataType) {
const codes = collectionCodes()
.append(data.code)
.append('code-append-1')
.append('code-append-1')
.append('code-append-2');

data.paymentTransaction.forEach(code => codes.append(code));

if (codes.empty()) {
console.log('can get values from existing mapping.')
return;
}

// 如果请求的数据需要转为数组,可以 Array.from 进行转换
const list = Array.from(codes.value());
console.log('fetch data before, codes --> ', list);

// mock 获取数据后拿到 name mapping 后,存入 mapping 中的行为.
// 注意,Set 类型也可以用 forEach 方法,不一定得转为数组才可以操作
list.forEach(code => mapping.set(code, `random-name-${Math.random()}`))
}

const mockItemData = {
code: 'code-main',
paymentTransaction: [
'code-payment-4',
'code-payment-1',
'code-payment-2',
'code-payment-1',
'code-payment-3',
]
}

handleNameMapping(mockItemData);
// fetch data before, codes --> (7) ["code-main", "code-append-1", "code-append-2", "code-payment-4", "code-payment-1", "code-payment-2", "code-payment-3"]

handleNameMapping(mockItemData);
// can get values from existing mapping.

handleNameMapping 在发起请求前会做 code 收集,若本次收集中没有需要 fetch 的 code,那就避免发送无用的 HTTP 请求,从而达到了优化的目的。

最终示例的 TS 代码如下。若想直接在控制台尝试效果的话,可以通过 ts 官网中的 Playground 编译为可直接运行的 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
interface DataType {
code: string;
paymentTransaction: string[];
}

const mapping = new Map();

function collectionCodes(initCodes?: string[] | Set<string>) {
const codes = new Set<string>(initCodes);

return {
append(code: string) {
if (!mapping.has(code)) {
codes.add(code);
}

return this;
},
empty() {
return !codes.size;
},
value() {
return codes;
},
};
}

function handleNameMapping(data: DataType) {
const codes = collectionCodes()
.append(data.code)
.append('code-append-1')
.append('code-append-1')
.append('code-append-2');

data.paymentTransaction.forEach((code) => codes.append(code));

if (codes.empty()) {
console.log('can get values from existing mapping.');
return;
}

// 如果请求的数据需要转为数组,可以 Array.from 进行转换
const list = Array.from(codes.value());
console.log('fetch data before, codes --> ', list);

// mock 获取数据后拿到 name mapping 后,存入 mapping 中的行为.
// 注意,Set 类型也可以用 forEach 方法,不一定得转为数组才可以操作
list.forEach(code => mapping.set(code, `random-name-${Math.random()}`))
}

const mockItemData = {
code: 'code-main',
paymentTransaction: [
'code-payment-4',
'code-payment-1',
'code-payment-2',
'code-payment-1',
'code-payment-3',
],
};

handleNameMapping(mockItemData);
// fetch data before, codes --> (7) ["code-main", "code-append-1", "code-append-2", "code-payment-4", "code-payment-1", "code-payment-2", "code-payment-3"]

handleNameMapping(mockItemData);
// can get values from existing mapping.

本例的分析就到此结束了,虽然在本例中链式调用没有充分展示出自己的优势,但也可以作为一个设计思路用于参考。

🔲 ⭐

webpack + Travis CI 自动部署项目应用

我们知道 Github Pages 是 Github 免费提供给用户展示页面的一项服务。当我们完成项目开发后,想将页面部署到 Github Pages 时,该要怎么操作呢?

可以在 GitHub 的储存库设置中设置用于展示页面的分支,该分支只保留构建后的静态资源,也就是源码与编译后的静态资源分离。按照传统的做法是:手动运行编译命令,编译后再复制到指定分支中。这样操作很繁琐,但使用 Travis CI 持续集成服务之后就可以不用操心这些事了。

概念

既然我们要使用 Travis CI,首先得搞清楚人家具体是干嘛的吧?

Travis CI 是一个 **持续集成(Continuous integration, CI)**。它与 git 相耦合,每当有 commit 提交时,它将自动触发构建与测试。若运行结果符合预期,才将新代码集成到 主流(mainline) 中,这样使应用更加健壮。

值得注意的是,Travis CI 提倡每次 commit 都是独立较小的改动,而不是突然提交一大堆代码。因为这有助于后续构建失败时可以回退到正常的版本。

运行构建时,Travis CI 将 GitHub 存储库克隆到全新的虚拟环境中,并执行一系列任务来构建和测试代码。如果这些任务中的一项或多项失败,则将构建视为已损坏。如果所有任务均未失败,则认为构建已通过,Travis CI 会将代码部署到 Web 服务器或应用程序主机中(在本文中是指 Github Pages 服务)。

准备

在使用之前,需要准备一个 Github 的账号对 Travis CI 进行授权。

  1. 接着通过 Github 的账号登录 Travis CI,点击 SIGN IN WITH GITHUB
  2. 点击后会被重定向到 Github 进行授权。
  3. 授权后,若是第一次登录的话会被重定向至引导页:
  4. 点击引导页第一步的按钮,使用 GitHub Apps 激活储存库。可以选择给全部储存库都激活,也可以激活指定储存库。本文以 <username>.github.io 为例:

    注意: 这个 username 是你自己的 Github 用户名。笔者的 usernameanran758 那储存库的名字就为 **anran758.github.io**。

  5. 激活后会被重定向到设置页,点击待部署的储存库右侧的 setting 按钮,跳转至 Travis CI 储存库设置页。我们需要在此页设置部署 Github Pages 时所需的环境变量:

环境变量的值需要从 Github 拿拥有部署权限的 token:

  1. 打开 Github,点击头像,再点击 Settings 进入设置页:
  2. 进入设置页面后在左侧边栏点击开发者设置:
  3. 跳转后在左侧边栏点击 Personal access tokens, 然后在头部点击 Generate new token:
  4. 填写 token 备注、权限,最后点击生成 token:
  5. 生成 token 后点击复制按钮,复制到粘贴板: 注意要妥善保管好 token,重新刷新页面后这个 token 将不会再展示出来。如果忘记了 token 的话,也只能在 token 编辑页中重新生成。这会导致所有用到该 token 的应用都要更新值。 比方说有三个应用使用了该 token,重新生成后只在一个应用更新的值,那其他两个应用不更新就无法使用了。
  6. 复制 token 后切回 Travis CI 储存库的设置页,添加环境变量:

这样我们的准备工作就完成的差不多了。

配置

在项目目录中新建文件 .travis.yml,内容如下:

/.travis.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
language: node_js
node_js:
- lts/*

install:
- yarn install # npm ci
script:
- yarn test # npm run test
- yarn build # npm run build

deploy:
provider: pages
local_dir: dist
target_branch: master
on:
branch: develop
token: $GITHUB_TOKEN
skip_cleanup: true
keep_history: true
committer_from_gh: true

由于 webpack 项目依赖 Node.js,因此语言(language) 设置为 node_js,同时还指定使用最新的 LTS Node.js 版本(lts/*)。

install 是安装部署所需的依赖项,script 则是用于运行测试或构建脚本。他们都是 Travis 的工作生命周期(Job Lifecycle)必触发的钩子(阶段)。

install 钩子若有脚本/命令运行失败的话,整个构建会停止。而 script 钩子表现则不同,当有脚本/命令运行失败后虽然构建会失败,但还会继续执行后面的脚本。如 yarn test 运行失败后会继续跑 yarn build 命令。

以下是 Travis CI 主要的阶段流程图:

graph TDA[before_install] --> B[install]A & B -.-> Z((停止构建))B --> C[before_script]C --> D[scrip]D --> E(after_success)D --> F(after_failure)E & F --> G[before_deploy]G --> H[deploy]H --> I[after_deploy]I --> J[after_script]

部署

通过 deploy 可以指定部署方式,下面将逐个介绍部署所用的选项:

provider 是部署类型。现在我们想将页面部署到 Github Pages,那就需要将 provider 设为 pages

local_dir 指定要推送到 Github Pages 的目录,默认为当前目录。webpack 默认的输出目录是 /dist,因此需要将值设为 dist。除此之外,Travis CI 默认情况下会删除构建期间创建的所有文件,因此需要设置 skip_cleanup: true 保留构建出来的 dist 目录.

on.branch 有 commit 提交的话,Travis CI 将从 on.branch 分支运行编译脚本,编译后会把 local_dir 目录强制推送到 target_branch 中。(target_branch 默认值为 gh-pages)

现在我们要部署的储存库是 <username>.github.io。这种类型的储存库有些特殊——它只能在 master 分支展示构建后的代码,而不能修改为其他分支。在 GitHub 储存库的 Settings 中的 Source 选项可以看到详细信息:

然而其他储存库则没有这种限制:

因此要部署到 <username>.github.io 储存库的话,target_branch 只能设为 master,触发编译的 on.branch 分支则可以自己定义。

其他储存库可以按照标准流程来开发:

  • develop 作为开发分支
  • master 作为主分支
  • gh-pages 作为页面展示分支

等功能开发并测试完毕后,将 develop 的代码合并到 master 分支并推送至远程。Traivis CI 检测到 matsercommit 提交后会自动运行脚本构建,构建完毕后将输出目录推送至 gh-pages 分支。

当然 Github Pages 也不是随便来一个人就可以部署的,你想要部署到储存库中首先得有该储存库的操作权限吧?token 就是证明你身份的东西。在上文中我们预先设置好了一个名为 GITHUB_TOKEN 的环境变量,此处我们可以通过 $GITHUB_TOKEN 直接取出该环境变量的值即可。

其他还有一些细节问题可以调整:比如推送构建后的代码到 target_branch 时使用的是强制推送(git push --force),如果你觉得这种强制覆盖历史记录的方式有点暴力的话,可以设置 keep_history: true 来保留提交记录。

自动部署后 commit 提交者默认是 Travis CI 的信息。也可以设置 committer_from_gh 允许 Travs CI 使用令牌所有者的个人信息来提交 commit

配置完毕后现在只需将 .travis.yml 提交到远程,Travis CI 就开始工作了:

甚至还可以在 Github commit 信息中看到编译的情况:

如果构建出问题的话,Travis CI 还会发邮件提示你:

部署成功后就可以直接通过浏览器访问啦~ 储存库部署的是 <username>.github.io 的话,访问链接为 https://<username>.github.io/。其他储存库可以访问 https://<username>.github.io/<repoName>

比如笔者的主页与博客是两个项目分离的,部署后的链接地址为 https://anran758.github.iohttps://anran758.github.io/blog

Travis CI CLI

还可以通过 Travis CI CLI 来进行操作:

按照文档的 Installation 部分安装 Travis CI CLI

安装完毕后通过命令行进入储存库目录,输入 travis -v 来检查是否安装成功。

Travis CI 有两个不同域名版本的 API,一个是 .com 新版本,.org 是旧版本的。先确定自己使用的是哪个平台,再设定它:

1
2
3
4
5
6
7
# 默认是 .org
travis endpoint
# API endpoint: https://api.travis-ci.org/

# 笔者使用的是 .com 的平台,因此需要修改默认的模式。设置 `--com` 和 `--pro` 的效果是相等的。
travis endpoint --com --set-default
# API endpoint: https://api.travis-ci.com/ (stored as default)

确定版本后输入 travis logintravis login --pro 进行登录。Mac os 系统可能会遇到 Travis Ci CLI 依赖的 ruby 版本和系统自带 ruby 有冲突:

1
2
3
4
5
6
7
8
9
10
11
travis login --com
# We need your GitHub login to identify you.
# This information will not be sent to Travis CI, only to api.github.com.
# The password will not be displayed.

# Try running with --github-token or --auto if you don't want to enter your password anyway.

# Username: anran758
# Password for anran758: ***********
# Unknown error
# for a full error report, run travis report --pro

若不想处理这些麻烦的依赖问题,可以在 Travis CI 的个人设置页 复制 access_token~/.travis/config.yml 的配置中:

1
2
3
4
# code ~/.travis/config.yml # 通过 vscode 进行修改

# 通过 vim 进行修改
vim ~/.travis/config.yml

修改 endpoints 下的 access_token 并保存后,在命令输入 travis accounts --pro 检查是否成功:

1
2
3
4
travis accounts --pro
# travis accounts --pro
# anran758 (Anran758): not subscribed, 18 repositories
# To set up a subscription, please visit travis-ci.com.

这样就登录完毕啦~ 接着在输入 travis logs 就可以查看日志:

1
2
3
4
5
# 查看最新构建的日志
travis logs

# 查看指定构建日志
travis logs 2

还可以清空指定构建的日志:

1
2
# travis logs 2 --d # -d 简短选项
travis logs 2 --delete

参考资料:

🔲 ☆

Flexbox 布局实际用例

上篇文章介绍了 flexbox 的属性与示例,本文再通过几个 flex 布局的案例来体会 flex 布局的特性带来的便利和问题~

格式化上下文

当我们给父容器设置 flex 属性后,flex 容器会在容器内创建一个新的 **flex 格式化上下文(formatting context)**。在这上下文中 floatclear 将失去作用,vertical-align 对于 flex 元素也不再会起作用。

在实际开发中,当我们使用行内元素(inlineinline-block) 时,有时候可能会看到元素之间会有一个奇怪的间隙,并且设置的字体越大间隙就越大。原来这个间隙是我们在编写源代码时标签换行导致,不换行就不会出现这种情况。

多数情况下,我们在编写代码时会习惯用编辑器对代码进行格式化,格式化后会使这些标签换行从而导致间隙。这在要求像素级还原的项目中就有点尴尬了。

以前常见的做法是在父元素设置 font-size: 0 消除间隙,再设置子元素的字体大小。这样做确实有点麻烦,因此在 flex 上下文中,这些间隙默认就会被清除。

圣杯布局

通常我们使用 flex 布局更多的是用于整体的布局设计,如:

在互联网早期,由于用户网路的限制,经常会出现 html 的内容显示出来但页面样式还没加载出来的情况,这会导致用户没能最先看到想看的东西。因此 Matthew Levine 在 2006 年提出了圣杯布局的概念,在 HTML 源代码中将用户想看的内容挪到次要内容的前面。

上例 demo 就是使用 flex 布局实现的圣杯布局,虽然在 HTML 源码里 Main 处于其他两块内容之上,但通过 order 属性可以调整元素间的顺序。

除此之外,还可以通过媒体查询(@media)做响应式页面,当屏幕宽度小于 640px 后仅需修改几项 flex 属性就可以改变布局排列的方式,十分灵活。

如果你使用过 react/vue 主流 UI 库的话,你就会发现他们使用布局容器也是 flex 布局实现的,比如 Element UIAnt Design 等。

栅格布局

栅格布局也可以通过 flex 来实现:在以下的 demo 中,HTML 源码内的各元素都是平级,通过调整 flex 属性实现了跨行或跨列的效果。

justify-content 尾列不整齐

让CSS flex布局最后一行列表左对齐的N种方法 –By 张鑫旭

多数情况下使用 justify-content 是要求子元素们散开,但尾列元素不够的时候,散开就显得很奇怪了,为此我们可以做如下处理:

动画

在 MDN Animatable CSS properties 上列出了可以使用 AnimationsTransitions 进行动画处理的属性,其中就有 flex 属性。因此还可以结合动画进行布局设计:

结束

通过以上几个案例是不是对 flex 布局的灵活有了更深的感受呢?以上 demo 大多借鉴已有的思路,如果你有什么好的想法,也可以自己动手尝试一番或分享出来~

参考资料:

Pixiv 背景图例:

  1. ちょけ | アリスミクと白うさぎ
  2. Azit | Miku
  3. ぽむ | もっと高くまで!
  4. 雨陌 | 8.31
  5. akino | つもりつもるキモチ。
🔲 ☆

Flexbox 布局入门

互联网早期实现布局是需要通过多种不同属性组合才能实现我们想要的布局。

比如常见的垂直居中,刚接触 css 的朋友看到 vertical-align: middle; 这个属性可能就会认为它就是用于垂直居中的,但实际上并没有那么简单。如果想要通过该属性来实现垂直居中,还需要其他小伙伴配合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.container {
width: 200px;
height: 200px;
border-radius: 6px;
text-align: center;
color: #fff;
background: #e44b27;
white-space: nowrap;
}

/* 该伪类是实现垂直居中关键 */
.container:after {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle;
}
.content {
display: inline-block;
white-space: normal;
vertical-align: middle;
text-align: left;
}
1
2
3
<div class="container">
<div class="content">我想居中!</div>
</div>

这样看来,为了实现垂直居中布局,我们还得打一套组合拳才能出来才行,是不是看起来有点麻烦的样子?

W3C 在 2009 年提出的 Fiexbox(flex) 布局草案,就是针对用户界面设计优化的 CSS 盒模型。如果使用 flex 布局来实现上面的垂直居中布局的话,可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
.container {
width: 200px;
height: 200px;
border-radius: 6px;
color: #fff;
background: #e44b27;

/* 使用 flex 布局 */
display: flex;
justify-content: center;
align-items: center;
}
1
2
3
<div class="container">
<div>我想居中!</div>
</div>

修改后的代码就显得更精简了,也不需要其他小伙伴来搭把手。布局的事情就让 flex 家族自己来解决即可。


概念

应用 flex 布局的容器我们通常称为 **弹性盒子/容器(flex container)**。弹性容器可以由 display: flexdisplay: inline-flex 生成。弹性盒子的子项常称为 **弹性元素/项目(flex items)**,它以 flex 布局模型进行布局。

1
2
3
.container {
display: flex | inline-flex;
}

如果想要学习 flex 布局的工作方式,最先需要学习的是它自身的术语。下面直接引用 flex 草案中术语的介绍图:

别被原版英文术语给吓倒了,咱们翻译一下其实就很好理解了:

在术语示意图中可以看到两根轴,分别是**主轴(main axis)垂直交叉轴(cross axis)。同时标注了主轴起点(main start)终点(main end)交叉轴的起点(cross start)终点(cross end)**。

默认情况下 flex 布局是按主轴的方向进行布局的。flex 元素所占据的 主轴空间(main size) 就是 flex 元素的宽度(width)、所占据的 交叉轴空间(cross size) 就是 flex 元素的高度(height)。


flex 容器属性

flex 容器里可以通过以下几种属性来控制容器的行为:

  • flex-direction
  • flex-wrap
  • flex-flow
  • justify-content
  • align-content
  • align-items

为了更好的观察各属性的行为,笔者在 codepen 上给不同属性都写了 demo 做参考。

目前有个新规范(CSS Box Alignment Module Level 3)正处于工作草案的状态中,对一些属性添加新值,如 [first|last]? baselineself-startself-endstartendleftrightunsafe | safe

这些新值多数浏览器都没实现,为了便于演示,此处仅讲解初始版本的值。Firefox 浏览器对新值实现的比较超前,也建议通过使用 Firefox 浏览器来查看 demo。

flex-direction

flex-direction 指示内部元素如何在 flex 容器中布局。可以简单的理解为 flex 容器的布局方向。其默认值为 row,可选语法如下:

1
2
/* 常用属性 */
flex-direction: row | row-reverse | column | column-reverse;
  • row: 主轴起点和主轴终点与内容方向相同。简而言之就是内容从左到右进行布局。
  • row-reverse: 与 row 行为相同,但主轴起点和主轴终点对调了位置。
  • column: 主轴由水平方向转为垂直方向,布局从上往下排。
  • column-reverse: 主轴由水平方向转为垂直方向,布局从上往下排。

值得注意的是,全局属性 dir 的作用是指示元素的文本的方向性,该属性会受到 rowrow-reverse 的影响。

flex-wrap

flex-wrap 指定 flex 元素单行显示还是多行显示 。如果可以换行,你甚至还可以通过该属性控制行的堆叠方向。它的取值如下所示:

1
flex-wrap: nowrap(默认值) | wrap | wrap-reverse;

可以通过本例 demo 右上角的按钮来修改元素的数量,观察三个值之间的变化:

  • nowrap: flex 容器宁愿压榨元素的空间也不肯换行。甚至压缩到一定地步后还会溢出容器。
  • wrap: 若子项超过容器所容纳的宽度,则允许断行展示。
  • wrap-reverse: 和 wrap 的行为一样,只是交叉轴起点与终点互换

flex-flow

flex-flow 属性是 flex-directionflex-wrap 的简写。这个没啥好说的,也就不额外写 demo 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 语法 */
flex-flow: <flex-direction> || <flex-wrap>;

/* 单独设置 flex-direction 的属性 */
flex-flow: row;
flex-flow: column;

/* 单独设置 flex-wrap 的属性 */
flex-flow: nowrap;
flex-flow: wrap;

/* 同时设置两种属性,建议按照语法顺序进行书写 */
flex-flow: row nowrap;
flex-flow: column wrap;

justify-content

justify-content 属性定义了容器主轴中各 flex 元素之间的对齐方式。这是 flex 布局中常用的属性之一。

1
2
3
justify-content: normal |
space-between | space-around | space-evenly |
center | flex-start | flex-end

在初始版本中,justify-content 的默认值为 flex-start。但在最新版本中的 chrome 浏览器被修改为了 normal

为了对比属性之间的差异,本例 demo 将元素的两侧 margin 清空:

  • normal: 排列效果等同 flex-start
  • flex-start: 默认情况是左对齐,从行首开始排列。每行第一个 flex 元素与行首对齐,同时所有后续的 flex 元素与前一个对齐。
  • flex-end: 默认情况下是右对齐,从行尾开始排列。每行最后一个 flex 元素与行尾对齐,其他元素将与后一个对齐。
  • center: 该值使元素居中对齐。
  • space-between: 首尾两端对齐,内部元素之间的间距相等。
  • space-around: 在每行上均匀分配弹性元素。相邻元素间距离相同,首尾两个元素的距离是相邻元素之间距离的一半
  • space-evenly: 主轴内各元素两侧均匀分配剩余空间。(注意此处与 space-around 的差异)

align-items

align-items 属性除了可以在 flex 布局中有效,还可以在 grid(网格) 布局中应用。在 flex 布局中它的作用是决定交叉轴的对齐方式。这也是 flex

1
2
3
4
5
/* 主流浏览器已经实现的值 */
align-items: normal | flex-start | flex-end | center | baseline | stretch

/* 新草案添加的值 */
align-items: | start | end | [ first | last ]baseline | left | right
  • normal: 在 flex 布局中 normal 的表现效果如同 stretch 一样。
  • stretch: 弹性元素被在交叉轴轴方向被拉伸到与容器相同的高度或宽度。若容器没有设置高度,则取当前行中最高元素的高度,如本例中元素 4 是第一行中最高的元素,那第一行中的高度都被拉伸到与最高元素相同的高度。第二行中最高的元素是元素 2,因此第二行高度都取至元素 2。
  • flex-start: 元素向交叉轴起点对齐。
  • flex-end: 元素向交叉轴终点对齐。
  • center: 元素在交叉轴居中。
  • baseline: 所有元素向基线对齐。侧轴起点到元素基线距离最大的元素将会于侧轴起点对齐以确定基线。在例子中放大元素 6 的 font-size, 与 center 进行对比就能看到差异了。

align-content

justify-content 是作用于主轴上,而 align-content 则是用于定义交叉轴的对齐方式。值得注意的是,若 flex 容器内只有一根轴线,该属性将不起作用

1
2
3
4
5
/* 主流浏览器已经实现的值 */
align-content: normal | space-between | space-around | space-evenly | stretch | center | flex-start | flex-end

/* 主流浏览器多数未实现的值 */
align-content: [first|last]? baseline, start, end, left, right

父容器设置了 flex 布局后,若子元素没有设定 height 属性的话,默认会将容器内的子元素进行拉伸。

为了便于观察两者的差异,笔者在 demo 中新增一列进行对比。左列的 flex 元素使用 height 属性,右列使用 min-height 属性。同时将 flex 容器高度设置为 400px:

  • normal: 像未设置值,元素处于默认位置。
  • stretch: 拉伸所有行来填满剩余空间。剩余空间平均的分配给每一行(若某元素设置了高度,那么该值对这个元素将不会起作用)。
  • flex-start: 交叉轴起点对齐。
  • flex-end: 交叉轴终点对齐。
  • center: 交叉轴居中对齐。
  • space-between: 交叉轴两端对齐,行之间间距相等
  • space-around: 交叉轴均匀对齐,行两端间距相等
  • space-evenly: 交叉轴内各元素两侧均匀分配剩余空间。

Flex Item

Flex Container(弹性容器)的一级子元素就是 Flex item(弹性元素)。以下主要应用于 Flex item 的属性。

  • flex-basis
  • flex-grow
  • flex-shrink
  • flex
  • align-self
  • order

flex-grow

flex-grow 属性用于定义元素所占有的比例,它接受一个正整数,默认值为 0

1
2
3
4
flex-grow: <number>

/* 例子: 仅接受正数的值 */
flex-grow: 1;

flex-shrink

flex-grow 相反,flex-shrink 属性处理元素收缩的问题,默认为 1,意味着元素默认会随着容器缩小而等比例缩小。当值为 0 时则不缩放。

1
2
3
4
5
6
7
flex-shrink: <number>

/* 例子: 默认缩放 */
flex-shrink: 1;

/* 例子: 使元素不缩放 */
flex-shrink: 0;

在以下 demo 中,各 flex 项目的宽高相等。当父容器有足够的空间时,元素不需要紧衣缩食,因此 flex-shrink 也没有机会表现出它的作用。

将 flex 容器尺寸调小后可以发现,flex-shrink 的值越大,元素被压榨的空间越多。

flex-basis

flex-basis 指定了 flex 元素在主轴空间(main size)所占的初始大小。

1
flex-basis:  <'width'>

当一个元素同时被设置了 flex-basis (值为 auto 除外)和 width 属性时,flex-basis 具有更高的优先级。

W3C 鼓励使用 flex 简写属性(下一小节进行秒速)来控制灵活性,而不是直接使用 flex-basis 属性。因为简写属性 flex 可以正确地重置任何未指定的属性以适应常见的用途。

flex

flex 属性是 flex-growflex-shrinkflex-basis 的简写,规定了弹性元素如何伸缩以适应 flex 容器中的可用空间,默认值为 0 1 auto

1
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]

flex 属性可以指定 1 个,2 个或 3 个值。

单值语法: 值必须为以下其中之一:

  • 一个无单位数(<number>): 它会被当作 <flex-grow> 的值。
  • 一个有效的宽度(width)值: 它会被当作 <flex-basis>的值。
  • 关键字 noneautoinitial

双值语法: 第一个值必须为一个无单位数,并且它会被当作 <flex-grow> 的值。第二个值必须为以下之一:

  • 一个无单位数:它会被当作 <flex-shrink> 的值。
  • 一个有效的宽度值: 它会被当作 <flex-basis> 的值。

三值语法:

  • 第一个值必须为一个无单位数,并且它会被当作 <flex-grow> 的值。
  • 第二个值必须为一个无单位数,并且它会被当作 <flex-shrink> 的值。
  • 第三个值必须为一个有效的宽度值, 并且它会被当作 <flex-basis> 的值。

这个属性没啥好演示的,其实就是之前介绍的三个属性的组合:

align-self

align-self 属性在 flex 布局中作用于单个 flex 元素上,它将控制指定元素在交叉轴上的位置。

1
2
3
4
align-self: auto | normal | stretch | center | flex-start | flex-end;

/* 多数浏览器未实现的功能 */
align-self: start | end | self-start | self-end | [first | last]? baseline;
  • auto: 设置为父元素的 align-items 值,如果该元素没有父元素的话,就设置为 stretch
  • normal: 在 flex 布局中,相当于 stretch 的效果。
  • stretch: flex 元素将会基于容器的宽和高,按照自身 margin box 的 cross-size 拉伸。
  • center: 使项目在交叉轴中居中。
  • flex-start: flex 元素会对齐到 cross-axis 的首端。
  • flex-end: flex 元素会对齐到 cross-axis 的尾端。

order

order 属性用于设置指定 flex 元素在容器中的顺序。容器中的 flex 元素按升序值排序,若值相同则按其源代码出现的顺序进行排序,默认值为 0。它接受一个整数值(integer),如 -203 等。

1
order: <integer>

我们可以操作下面的 demo 来控制元素的顺序,比如将第三项元素通过 order 在移动到第一位。


兼容性

要将学到的新东西应用到实际项目中就不得不考虑其兼容性了。通过 caniuse 我们可以看到:flex 布局经过多年的发展,主流浏览器都已经对 flex 布局基本模块都实现完毕了。

PC 端需要考虑的是要不要兼容 IE,移动端最低兼容为 ios 3.2+Android 2.1+。如果你需要开发微信小程序,那么小程序官方就推荐使用 flex 布局。

早期 flex 布局是通过 display: box; 来申明,这是使用了旧的规范,后来该值被 flex 给替换掉了。还有一些很低版本的浏览器或许还需要添加浏览器前缀才能使用 flex 布局。因此你在某处看到如下代码也不用感到奇怪,这是开发者在给布局做兼容呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.flex-center {
display: -webkit-box;
display: -webkit-flex;
display: -moz-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-moz-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-moz-box-align: center;
-ms-flex-align: center;
align-items: center;
}

但如果要我们在开发时手动写这种兼容好像不是很靠谱,兼容又冗余。所幸现在的前端开发都会使用脚手架,这些脚手架一般都会内置 postcssautoprefix 之类的插件来帮助我们完成这些事。

还有一些朋友可能会说,我们老项目还是得要兼容 IE 8+ 呀,是不是意味着跟 flex 布局无缘了?其实不是的,github 上有一个叫 flexibilitypolyfill 可以让 IE8 + 也实现 flex 布局效果.


结束

本篇介绍了 flex 布局该如何使用、各属性的作用与效果,下一篇再详细讲讲 flex 布局在实际工作中的妙用~

参考资料:

  1. CSS Flexible Box Layout Module Level 1
  2. CSS Box Alignment Module Level 3
  3. Flex Item
❌