React Lane 模型

二进制操作常用于权限控制,在表示多种状态叠加(一对多)的场景更加方便。

1
2
3
4
// 负数用补码来表示

5 = 0b00000101
-5 = 反码 + 1 = 0b11111010 + 1 = 0b11111011

对 fiberNode 的操作也是用位运算来标记的。

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
// 初始化一些 flags
const NoFlags = 0b0000000000000000; // 0,表示没有任何标志
const PerformedWork = 0b0000000000000001; // 1,表示执行过某项工作
const Placement = 0b0000000000000010; // 2,表示需要放置某项内容
const Update = 0b0000000000000100; // 4,表示需要更新

// 一开始将 flag 变量初始化为 NoFlags,表示没有任何操作
let flag = NoFlags;

// 这里就是在组合多个状态
flag = flag | PerformedWork | Update; // 按位或运算,将 PerformedWork 和 Update 结合进来

// 要判断是否有某个 flag,直接通过 & 来进行判断即可
// 判断是否有 PerformedWork 类型的更新
if (flag & PerformedWork) {
// 执行对应操作
console.log("执行 PerformedWork");
}

// 判断是否有 Update 类型的更新
if (flag & Update) {
// 执行对应操作
console.log("执行 Update");
}

// 判断是否有 Placement 类型的更新
if (flag & Placement) {
// 执行对应操作
console.log("执行 Placement");
}

上下文

react 中有许多上下文变量,使用位运算来标记,可以方便的表示进入或移出上下文

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
// 未处理于 React 上下文
export const NoContext = /* 0b000 */;

// 处理于 batchedUpdates 上下文
const BatchedContext = /* 0b001 */;

// 处理于 render 阶段
export const RenderContext = /* 0b010 */;

// 处理于 commit 阶段
export const CommitContext = /* 0b100 */;

// 是否处于 RenderContext 上下文中,结果为 true
if ((executionContext & RenderContext) !== NoContext) {
// 在这里执行与 RenderContext 相关的逻辑
}

// 是否处于 CommitContext 上下文中,结果为 false
if ((executionContext & CommitContext) !== NoContext) {
// 在这里执行与 CommitContext 相关的逻辑
}

// 如果要移除某个上下文
executionContext &= ~RenderContext; // 从当前上下文中移除 RenderContext

// 是否处于 RenderContext 上下文中,结果为 false
if ((executionContext & RenderContext) !== NoContext) {
// 在这里执行与 RenderContext 相关的逻辑
}


Lane 模型

schedule 是一个单独的包定义了 5 种优先级。

Lane 是 react 内部的更细粒度的优先级管理,react 所有的更新都只能通过事件或异步任务触发, 所以 React 定义了自己的优先级:

  • discreteEventPriority: 离散事件 input focus,blur,touchStart 等
  • continuousEventPriority: 连续事件 drag mousemove, scroll 等;
  • DefaultEventPriority: 默认优先级 通过计时器产生的任务
  • idleEventPriority: 对应空闲情况的优先级

每个优先级对应的值就是 Lane, 因此需要与 schedule 优先级相互转换。

1
2
3
4
5
6
7
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;

//...
  • react 优先级转为 scheduler, 先将 lanes 转为 EventPriority

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export function lanesToEventPriority(lanes: Lanes): EventPriority {
    const lane = getHighestPriorityLane(lanes);

    if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
    return DiscreteEventPriority;
    }

    if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
    return ContinuousEventPriority;
    }

    if (includesNonIdleWork(lane)) {
    return DefaultEventPriority;
    }
    return IdleEventPriority;
    }

    再将 EventPriority 转为 schedule 优先级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
    case DiscreteEventPriority:
    schedulerPriorityLevel = ImmediateSchedulerPriority;
    break;

    case ContinuousEventPriority:
    schedulerPriorityLevel = UserBlockingSchedulerPriority;
    break;

    case DefaultEventPriority:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;

    case IdleEventPriority:
    schedulerPriorityLevel = IdleSchedulerPriority;
    break;

    default:
    schedulerPriorityLevel = NormalSchedulerPriority;
    break;
    }
  • scheduler 优先级转为 react 优先级

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const schedulerPriority = getCurrentSchedulerPriorityLevel(); // 获取当前调度器优先级

    switch (schedulerPriority) {
    case ImmediateSchedulerPriority:
    return DiscreteEventPriority;
    case UserBlockingSchedulerPriority:
    return ContinuousEventPriority;
    case NormalSchedulerPriority:
    case LowSchedulerPriority:
    return DefaultEventPriority;
    case IdleSchedulerPriority:
    return IdleEventPriority;
    default:
    return DefaultEventPriority;
    }
expiration Time 模型

如果同一时间出发了多个更新,应该先去更新哪一个。

早期 react 使用 expiration Time 模型,这一点和 scheduler 的设计是一致的。不同的优先级对应不同的 deadline, 每次 schedule 执行的时候,选出一个最高的优先级执行。

但是这种模式无法表示批的概念,当优先级大于 priorityBunch 就会划分到同一批,但是无法将提交的不同更新种的某种类型的任务算作同一批。因此基于上面的原因引入了 lane 模型。

lane 是如何灵活表达批的概念?

1
2
3
4
5
6
7
8
9
10
11
12
// 要使用的批次
let batch = 0;

// laneA 和 laneB 是不相邻的优先级
const laneA = 0b00000000000000001000000; // 代表某个优先级
const laneB = 0b00000000000000000000001; // 代表另一个优先级

// 将 laneA 纳入批中
batch |= laneA; // 将 laneA 的优先级合并到 batch 中

// 将 laneB 纳入批中
batch |= laneB; // 将 laneB 的优先级合并到 batch 中

updateLane

用于调度更新的优先级 通过 requestUpdateLane 创建

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
export function requestUpdateLane(fiber: Fiber): Lane {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
// 初次加载时为SyncLane
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
? (SyncLane: Lane)
: (SyncBatchedLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// This is a render phase update. These are not officially supported. The
// 这是一个渲染阶段的更新,这些都没有得到官方的支持
// old behavior is to give this the same "thread" (expiration time) as
// 原来的方案是赋予它与当前渲染相同的过期时间
// whatever is currently rendering. So if you call `setState` on a component
// 如果你在一个组件上调用setState,他们会在相同的渲染中稍后生效,
// that happens later in the same render, it will flush. Ideally, we want to
// 会发生闪屏
// remove the special case and treat them as if they came from an
// 理想情况下,我们希望删除特例,并将它们视为来插入的事件
// interleaved event. Regardless, this pattern is not officially supported.
// 无论如何,这种模式并没有得到官方的支持。
// This behavior is only a fallback. The flag only exists until we can roll
// 这种行为值是一个回退机制,标识只存存在于我们可以离开setState警告之前
// out the setState warning, since existing code might accidentally rely on
// 因为现有代码可能意外地依赖于当前行为。
// the current behavior.
return pickArbitraryLane(workInProgressRootRenderLanes);
}

// The algorithm for assigning an update to a lane should be stable for all
// updates at the same priority within the same event. To do this, the inputs
// 对于同一事件中具有相同优先级的所有更新,为车道分配更新的算法应该是稳定的(幂等的)。
// to the algorithm must be the same. For example, we use the `renderLanes`
// 为此,算法的输入必须相同。
// to avoid choosing a lane that is already in the middle of rendering.
// 我们使用“renderLanes”来避免选择已经在渲染过程中的车道。
// However, the "included" lanes could be mutated in between updates in the
// 然而 included 车道可能在两次相同事件中的更新被改变
// same event, like if you perform an update inside `flushSync`. Or any other
// 就像在“flushSync”中执行更新一样
// code path that might call `prepareFreshStack`.
// 或者任何其他可能调用“prepareFreshStack”的代码。
// The trick we use is to cache the first of each of these inputs within an
// 我们使用的技巧是在事件中缓存这些输入中的第一个
// event. Then reset the cached values once we can be sure the event is over.
// 然后在确定事件结束后重置缓存的值。
// Our heuristic for that is whenever we enter a concurrent work loop.
// 启发式方法是,每当我们进入一个并发工作循环时
// We'll do the same for `currentEventPendingLanes` below.
if (currentEventWipLanes === NoLanes) {
currentEventWipLanes = workInProgressRootIncludedLanes;
}

const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
if (currentEventPendingLanes !== NoLanes) {
currentEventPendingLanes =
mostRecentlyUpdatedRoot !== null
? mostRecentlyUpdatedRoot.pendingLanes
: NoLanes;
}
return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);
}

// TODO: Remove this dependency on the Scheduler priority.
// To do that, we're replacing it with an update lane priority.
const schedulerPriority = getCurrentPriorityLevel();

// The old behavior was using the priority level of the Scheduler.
// This couples React to the Scheduler internals, so we're replacing it
// with the currentUpdateLanePriority above. As an example of how this
// could be problematic, if we're not inside `Scheduler.runWithPriority`,
// then we'll get the priority of the current running Scheduler task,
// which is probably not what we want.
let lane;
if (
// TODO: Temporary. We're removing the concept of discrete updates.
(executionContext & DiscreteEventContext) !== NoContext &&
schedulerPriority === UserBlockingSchedulerPriority
) {
lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
} else {
const schedulerLanePriority =
schedulerPriorityToLanePriority(schedulerPriority);

if (decoupleUpdatePriorityFromScheduler) {
// In the new strategy, we will track the current update lane priority
// inside React and use that priority to select a lane for this update.
// For now, we're just logging when they're different so we can assess.
const currentUpdateLanePriority = getCurrentUpdateLanePriority();

if (
schedulerLanePriority !== currentUpdateLanePriority &&
currentUpdateLanePriority !== NoLanePriority
) {
if (__DEV__) {
console.error(
"Expected current scheduler lane priority %s to match current update lane priority %s",
schedulerLanePriority,
currentUpdateLanePriority
);
}
}
}

lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
}

return lane;
}

eventTime

eventTime

用于的调度更新的时间戳 通过 requestEventTime 创建

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
// 用二进制来表示所处的上下文状态
export const NoContext = /* */ 0b0000000;
const BatchedContext = /* */ 0b0000001;
const EventContext = /* */ 0b0000010;
const DiscreteEventContext = /* */ 0b0000100;
const LegacyUnbatchedContext = /* */ 0b0001000;
const RenderContext = /* */ 0b0010000;
const CommitContext = /* */ 0b0100000;
export const RetryAfterError = /* */ 0b1000000;

var currentEventTime = NoTimestamp = -1;;
// executionContext = NoContext
export function requestEventTime() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
return now();
}
// 不在react中,可能在浏览器的事件中
if (currentEventTime !== NoTimestamp) {
// 对所有更新使用相同的开始时间,直到再次进入react中
return currentEventTime;
}
// 这是React运行后后的第一次更新。计算新的开始时间。
currentEventTime = now();
return currentEventTime;
}

获取时间戳的时候,如果Date对象初始化的时间过长,在使用的时候还需要把初始化的时间减去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let getCurrentTime;
const hasPerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';

if (hasPerformanceNow) {
const localPerformance = performance;
getCurrentTime = () => localPerformance.now();
} else {
const localDate = Date;
const initialTime = localDate.now();
getCurrentTime = () => localDate.now() - initialTime;
}

var Scheduler_now = Scheduler.unstable_now = getCurrentTime;
var initialTimeMs$1 = Scheduler_now$1();
// 如果初始化的事件戳非常小,直接使用 initialTimeMs,对于现代浏览器直接使用performance.now
// 在老的浏览器中回退使用Date.now, 返回Unix事件戳,这时需要减去模块初始化的时间来模拟 performance.now
// 并把时间控制在32位以内

var now = initialTimeMs$1 < 10000 ? Scheduler_now$1 : function () {
return Scheduler_now$1() - initialTimeMs$1;
};

createUpdate

Update

用于记录组件状态的改变,保存到UpdateQueue中,多个Update可以同时存在

/packages/react-reconciler/src/ReactUpdateQueue.new.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function createUpdate(eventTime: number, lane: Lane): Update<*> {
const update: Update<*> = {
eventTime,
lane,
// export const UpdateState = 0; 更新
// export const ReplaceState = 1; 替换更新
// export const ForceUpdate = 2; 强制更新
// export const CaptureUpdate = 3; 捕获哦错误后更新
tag: UpdateState,
payload: null, // 更新内容,比如`setState`接收的第一个参数
callback: null, // 对应的回调,`setState`,`render`都有
next: null, // 指向下一个更新
};
return update;
}

enqueueUpdate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// fiber 节点中的updateQueue,默认是null
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// 只在未挂载时执行
return;
}

const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
// pending update链表,最新的更新在链表的顶端
// pending = 3->2->1->3....
const pending = sharedQueue.pending;
if (pending === null) {
// 只有一个update时候,循环引用
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;

FiberNode

V16

每一个ReactElement对应一个Fiber对象,记录各个节点的状态,由于Fiber对象并不是绑定在组件的实例上,这也给hooks实现时,拿到每个节点的状态提供了方便

Fiber 记录了整个应用的树型结构

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
// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber = {|
// 标记不同的组件类型
tag: WorkTag,

// ReactElement里面的key
key: null | string,

// ReactElement.type,也就是我们调用`createElement`的第一个参数
elementType: any,

// The resolved function/class/ associated with this fiber.
// 异步组件resolved之后返回的内容,一般是`function`或者`class`
type: any,

// The local state associated with this fiber.
// 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
stateNode: any,

// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,

// 单链表树结构
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构
// 兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,

// ref属性
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

// 新的变动带来的新的props
pendingProps: any,
// 上一次渲染完成之后的props
memoizedProps: any,

// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,

// 上一次渲染的时候的state
memoizedState: any,

// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,

// 用来描述当前Fiber和他子树的`Bitfield`
// 共存的模式表示这个子树是否默认是异步渲染的
// Fiber被创建的时候他会继承父Fiber
// 其他的标识也可以在创建的时候被设置
// 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
mode: TypeOfMode,

// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,

// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,

// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,

// 代表任务在未来的哪个时间点应该被完成
// 不包括他的子树产生的任务
expirationTime: ExpirationTime,

// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,

// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 镜像Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,

// 下面是调试相关的,收集每个Fiber和子树渲染时间的

actualDuration?: number,

// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,

// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,

// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,

// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,

V17

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
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.

// Fiber 是组件的中的工作内容,需要完成或已经完成,每个组件可以个有多个。

export type Fiber = {|
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.

// 第一个字段概念上是实例的一部分,它过去被拆分成单独的类型并且存在于其他Fiber节点的字段中。
// 直到Flow修复了交叉错误,我们把它合并成单一类型

// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.

// 实例在组件的所有的版本中共享,可以很容易的拆分成单个对象避免复制太多内容到镜像树上,
// 现在我们把它放在单独的对象上,为了初始化渲染期间最小化创建对象的数量

// Tag identifying the type of fiber.
// fiber类型, 标签标识

tag: WorkTag,

// Unique identifier of this child.
// 这个子元素的唯一标识
key: null | string,

// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
// element.type的值,用于在调和子节点期间保留标识。
elementType: any,

// The resolved function/class/ associated with this fiber.
// 函数组件或类组件通过type链接,type为函数组件或类组件的引用
type: any,

// The local state associated with this fiber.
// 保存实例化之后的节点对象
stateNode: any,

// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.

// Remaining fields belong to Fiber

// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,

// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,

// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,

// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.

// A queue of state updates and callbacks.
updateQueue: mixed,

// The state used to create the output
memoizedState: any,

// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,

// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
mode: TypeOfMode,

// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,

// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null,

// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: Fiber | null,
lastEffect: Fiber | null,

lanes: Lanes,
childLanes: Lanes,

// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,

// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,

// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,

// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,

// Sum of base times for all descendants of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,

// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only

_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,

// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
|};

FiberRoot

FiberRoot

  • 整个应用的起点
  • 包含应用挂载的目标节点
  • 记录整个应用更新过程的各种信息
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
type BaseFiberRootProperties = {|
// root节点,render方法接收的第二个参数
containerInfo: any,
// 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到
pendingChildren: any,
// 当前应用对应的Fiber对象,是Root Fiber
current: Fiber,

// 一下的优先级是用来区分
// 1) 没有提交(committed)的任务
// 2) 没有提交的挂起任务
// 3) 没有提交的可能被挂起的任务
// 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
// The earliest and latest priority levels that are suspended from committing.
// 最老和新的在提交的时候被挂起的任务
earliestSuspendedTime: ExpirationTime,
latestSuspendedTime: ExpirationTime,
// The earliest and latest priority levels that are not known to be suspended.
// 最老和最新的不确定是否会挂起的优先级(所有任务进来一开始都是这个状态)
earliestPendingTime: ExpirationTime,
latestPendingTime: ExpirationTime,
// The latest priority level that was pinged by a resolved promise and can
// be retried.
// 最新的通过一个promise被reslove并且可以重新尝试的优先级
latestPingedTime: ExpirationTime,

// 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
// 在`renderRoot`出现无法处理的错误时会被设置为`true`
didError: boolean,

// 正在等待提交的任务的`expirationTime`
pendingCommitExpirationTime: ExpirationTime,
// 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null
// 在commit阶段只会处理这个值对应的任务
finishedWork: Fiber | null,
// 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的timeout
timeoutHandle: TimeoutHandle | NoTimeout,
// 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用
context: Object | null,
pendingContext: Object | null,
// 用来确定第一次渲染的时候是否需要融合
+hydrate: boolean,
// 当前root上剩余的过期时间
// TODO: 提到renderer里面区处理
nextExpirationTimeToWorkOn: ExpirationTime,
// 当前更新对应的过期时间
expirationTime: ExpirationTime,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
// 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟
// 同时包括完成之后的回调
// 貌似用在测试的时候?
firstBatch: Batch | null,
// root之间关联的链表结构
nextScheduledRoot: FiberRoot | null,
|};

mode

创建跟节点RootFiber时,在current属性上挂载了通过createHostRootFiber创建的fiber对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode;
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
} else {
// tag 为0
mode = NoMode;
}

// 开发时mode为8
if (enableProfilerTimer && isDevToolsPresent) {
// Always collect profile timings when DevTools are present.
// This enables DevTools to start capturing timing at any point–
// Without some nodes in the tree having empty base times.
mode |= ProfileMode;
}

return createFiber(HostRoot, null, null, mode);
}

ReactDOM.render

ReactDOM.render

packages/react-dom/src/client/ReactDOMLegacy.js

  • 创建ReactRoot
  • 创将FiberRoot和RootFiber
  • 创将更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
) {
// ...
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
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
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>, // 父组件
children: ReactNodeList, // 渲染组件
container: Container, // 容器
forceHydrate: boolean, // 是否强制注入
callback: ?Function, // 回调函数
) {
//...
// 查看是否有复用的节点
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// 初始化加载,在root上挂载创建的ReactRooter
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// _internalRoot 也就是内部使用的fiberRoot
fiberRoot = root._internalRoot;
//...
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
//...
updateContainer(children, fiberRoot, parentComponent, callback);
}
// 最终都是调用了 updateContainer,并把创将的fiberRoot传入
return getPublicRootInstance(fiberRoot);
}
legacyCreateRootFromDOMContainer
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
// 直接返回原生节点
function getReactRootElementInContainer(container: any) {
if (!container) {
return null;
}

if (container.nodeType === DOCUMENT_NODE) {
return container.documentElement;
} else {
return container.firstChild;
}
}

// 如果原生节点存在 shouldHydrate = true
function shouldHydrateDueToLegacyHeuristic(container) {
const rootElement = getReactRootElementInContainer(container);
return !!(
rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
);
}

function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 清空容器的子节点
while ((rootSibling = container.lastChild)) {
if (__DEV__) {
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
// ROOT_ATTRIBUTE_NAME 用于服务端渲染的判断,是否需要合并节点
(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
console.error(
'render(): Target node has markup rendered by React, but there ' +
'are unrelated nodes as well. This is most commonly caused by ' +
'white-space inserted around server-rendered markup.',
);
}
}
container.removeChild(rootSibling);
}
}
// ...
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}
createLegacyRoot 创建ReactRoot
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
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
//
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}

function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}

function createRootImpl(
container: Container,
// 常量0,标识根节点
tag: RootTag,
options: void | RootOptions,
) {
// createContainer 最终调用了createFiberRoot
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
//...
return root;
}
createFiberRoot
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
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 容器节点本身是FiberRoot对象
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// 创建的Fiber对象
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
// 为原始的fiber节点添加updateQueue信息
// function initializeUpdateQueue(fiber) {
// var queue = {
// baseState: fiber.memoizedState,
// firstBaseUpdate: null,
// lastBaseUpdate: null,
// shared: {
// pending: null
// },
// effects: null
// };
// fiber.updateQueue = queue;
// }
initializeUpdateQueue(uninitializedFiber);
return root;
}
updateContainer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// container = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate)._internalRoot;
const current = container.current;
const eventTime = requestEventTime();

//创将事件优先级
const lane = requestUpdateLane(current);

// 创将更新对象
const update = createUpdate(eventTime, lane);
update.payload = {element};

// 加入到更新队列中
enqueueUpdate(current, update);
// 调度更新
scheduleUpdateOnFiber(current, lane, eventTime);

return lane;
}

Express实践 ② 完善核心功能

密码加密/校验

  • 密码不可以明文存储,需要生成 hash,在模型文件中,可以使用 Getters, Setters & Virtuals 方法并配合 bcrypt 模块 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // models\user.js

    const { Model, Op } = require("sequelize");
    const bcrypt = require("bcrypt");
    module.exports = (sequelize, DataTypes) => {
    class User extends Model {}
    User.init(
    {
    password: {
    type: DataTypes.STRING,
    set(value) {
    // 在设置值或更新值得时候会执行这个方法
    this.setDataValue("password", bcrypt.hashSync(value, 10));
    },
    },
    },
    {
    sequelize,
    modelName: "User",
    }
    );
    return User;
    };
  • 密码校验,模型校验方法只有设置或更新数据时触发,所以可以考虑将验证方法写在模型的类上,或是在路由中实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // routes\user.js
    // 路由中校验数据

    router.post("/login", async function (req, res, next) {
    try {
    const data = await User.findOne({
    where: {
    username: req.body.username,
    email: req.body.email,
    },
    });

    if (!bcrypt.compareSync(req.body.password, data.password)) {
    throw new Error("密码错误");

    res.status(401);
    return;
    }

    res.end();
    } catch (err) {}
    });
  • 生成 jwt,用户保存用户的登录状态, 需要依赖 jsonwebtoken

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // routes\user.js

    const bcrypt = require("bcrypt");
    const jwt = require("jsonwebtoken");

    router.post("/login", async function (req, res, next) {
    try {
    const data = await User.findOne({
    // ...
    });
    const token = jwt.sign(
    {
    data: "foobar",
    },
    process.env.SECRET,
    { expiresIn: "30d" }
    );
    res.end(token);
    } catch (err) {}
    });
  • SECRET 通常作为环境变量,且禁止提交到仓库, 可以使用 crypto 生成随机密钥

    1
    crypto.randomBytes(64).toString("hex");

中间件/校验 jwt

对接口的校验权限的校验不止一个,所以不会再每个路由文件中实现一边逻辑,这就是用到了 中间件

下面是一个路由级别的中间件,用于处理用户的 token 是否有效

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
// middleware\auth.js

const jwt = require("jsonwebtoken");
const { User } = require("../models");

module.exports = async function (req, res, next) {
try {
const { token } = req.headers;
if (!token) {
throw new Error("用户未登录");
}

const { userId } = jwt.verify(token, process.env.SECRET);

const user = await User.findByPk(userId);

if (!user) throw new Error("用户不存在");

if (user.role === "[some role]") throw new Error("用户权限错误");

// 可以直接再路由的req上获取用户信息
req.user = user;
next();
} catch (err) {
// 客户端需要处理token失效的情况

res.status(500).json({
err: err.message,
});
}
};

嵌套关联查询

上一章讲解了一对多的关联查询,但是关联字段中可能还需要关联另一张表,也就是嵌套关联查询

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
router.get("/", async function (req, res, next) {
try {
const condition = {
order: [["id", "DESC"]],
// 全局过滤 两个关联的外键
attributes: {
exclude: ["categoryId", "userId"],
},
include: [
// as 关联字段别名,需要和model中的对应
// attributes 用于过滤关联模型中的字段
{ model: User, as: "user", attributes: ["id", "username"] },
{
model: Category,
as: "category",
attributes: ["id", "name"],

// 通过嵌套 include 关联查询
include: {
model: Article,
as: "article",
attributes: ["id", "title"],
},
},
],
};
const page = req.query.page || 1;
const pageSize = req.query.pageSize || 10;

condition.limit = pageSize;
condition.offset = (page - 1) * pageSize;

const { count, rows } = await Course.findAndCountAll(condition);
res.json(rows);
} catch (err) {}
});

这会查出来如下嵌套的返回格式, 他们的关系是

  • 一个课程数据多个用户,也属于多个分类。或者说一个用户有多个课程,一个分类包含多个课程。
  • 一个分类属于多篇文章。或者说一个文章有多个分类。
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
{
"status": true,
"message": null,
"data": {
"course": [
{
"id": 12,
"name": "后端课程2",
"image": "xxx",
"recommended": true,
"introductory": false,
"content": "后端课程1,描述",
"likesCount": 10,
"chaptersCount": 10,
"createdAt": "2025-01-17T12:48:54.000Z",
"updatedAt": "2025-01-17T12:48:54.000Z",
"user": {
"id": 7,
"username": "user1"
},
"category": {
"id": 2,
"name": "后端课程",
"article": {
"id": 16,
"title": "标题100"
}
}
}
// ...剩余7条数据
],
"pagination": {
"total": 8,
"page": 1,
"pageSize": 10
}
}
}

但是其我们是以课程的视角去查询,用户,分类,以及文章都属于当前课程的附属属性,并不需要用层级关系去体现。会希望他们以同级的字段展示。这也是 sequelize 中 Eager Loading vs Lazy Loading 的概念。

include 关联字段表示 Eager Loading, 会直接把数据返回。而 Lazy Loading 允许用方法调用的方式,选择哪些数据需要,而这种方式正好处理数据的层级问题。

上面的 findAndCountAll 是无法使用 Lazy 关联查询的,因为它查询的是整个列表,必须明确用数据结构说面关联关系,并不是针对某个课程的关联查询

下面是查询单独一个课程的关联信息, 需要查询出关联的字段

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
router.get("/:id", async function (req, res, next) {
try {
const condition = {
order: [["id", "DESC"]],
// 注意不要排除了关联 ID, 否则查询不到
// attributes: {
// exclude: ["categoryId", "userId"],
// },
};

const course = await Course.findByPk(req.params.id, condition);

// 通过课程查询分类,注意要带上关联的 articleId
const category = await course.getCategory({
attributes: ["id", "name", "articleId"],
});

// 通过分类查询文章
const article = await category.getArticle({
attributes: ["id", "title"],
});

res.json({
status: true,
message: null,
data: {
course,
category,
article,
},
});
} catch (err) {}
});

多对多的查询

实现一个点赞的功能:

  • 一个用户可以可以给多个课程点赞
  • 一个课程也可以被多个用户点赞

虽然可以直接在用户表中储存点赞课程,但不是一个好的做法, 存在以下问题:

无法保证数据一致性:难以通过外键验证课程是否存在。
查询复杂:查找特定课程的所有学生需要额外的解析逻辑。
扩展性差:不适合大规模、多条件查询的场景。

最佳实践是使用中间表:

数据一致性:中间表中的外键可以确保关联数据的完整性。
灵活性:中间表允许添加额外的信息(如时间戳、状态等)。
查询效率:数据库的索引可以优化多对多查询性能。
扩展性:关系较复杂时,可以轻松扩展中间表的结构。

修改模型,通过点赞中间表建立课程表和用户之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// models\user.js

class User extends Model {
static associate(models) {
models.User.belongsToMany(models.Course, {
through: models.Like,
as: "likeCourse",
});
}
}

// models\course.js
class User extends Model {
static associate(models) {
models.Course.belongsToMany(models.User, {
through: models.Like,
as: "courseWithUsers",
});
}
}

查询某个用户喜欢的课程, 如果使用 include 做查询在多对多的关系中无法实现分页,在 include 中无法对关联查询的数据分页,且模型中的 foreignKey 是必填的,否则会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
router.get("/:id", async function (req, res, next) {
try {
// 获取user实例
const user = await User.findByPk(req.params.id, {
include: [
{
model: Course,
as: "likeCourses",
// 无法使用分页
// limit: pageSize,
// offset: (page - 1) * pageSize,
},
],
});

res.json({
//...
});
} catch (err) {}
});

所以可以使用 Lazy Loading 手动请求数据, 模型中提供了内置的方法,可以用于获取分页数据

另外需要控制 不显示中间表的数据

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
router.get("/:id", async function (req, res, next) {
try {
const page = req.query.page || 1;
const pageSize = req.query.pageSize || 10;

const limit = pageSize;
const offset = (page - 1) * pageSize;
const user = await User.findByPk(req.params.id);

// getLikeCourses 是模型内部映射的方法,通过 get + as别名 命名
const course = await user.getLikeCourses({
// 不显示中间表的数据
joinTableAttributes: [],
attributes: ["id", "name"],
limit,
offset,
});

// 另一个映射方法用于获取总条数
const total = await user.countLikeCourses();

res.json({
//...
});
} catch (err) {}
});

处理跨域

  • nginx
  • cors

Express实践 ① 用 sequelize 实现 CRUD

安装

初始化项目 express-generator

1
2
# --no-view 表示不生成视图文件
npx express-generator --no-view [项目名称]

安装 ORM 相关依赖,并初始化

1
2
3
4
npm install -D sequelize mysql2

# 初始化ORM
npx sequelize-cli init

修改 sequelize 配置文件

所有的参数都需要使用字符串格式

1
2
3
4
5
6
7
8
9
10
11
12
13
// config/config.js

{
"development": {
"username": "用户名",
"password": "密码",
"port": "端口号",
"database": "数据库名称",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+8:00"
}
}

创建数据库

字符集 utf8mb4
utf8mb4 支持 Unicode 字符集中的所有字符,包括表情符号(emoji)。一些其他语言字符,utf8 字符集无法处理的字符。

**字符集排序规则 utf8mb4_general_ci **

是一种常见的 不区分大小写(case-insensitive, CI) 排序规则,ci 代表 Case Insensitive,即不区分字母的大小写。
它会将大写字母和小写字母视为相同的字符进行排序。例如,字母 a 和 A 会被视为相同,而在某些排序规则下它们可能会被视为不同。

utf8mb4_unicode_ci — 精确排序

适用场景:多语言支持、需要精确排序的应用。
优点:符合 Unicode 排序规则,处理复杂字符(如带有重音符号的字母)时表现更好,能够准确地处理所有 Unicode 字符,适用于全球多种语言的应用。
缺点:与 utf8mb4_general_ci 相比,性能略差一些,因为它会考虑更多的字符特性进行排序。

创建模型

Creating the first Model (and Migration), 模型的名字用单数命名,生成的表名是复数

1
npx sequelize-cli model:generate --name User --attributes firstName:string,lastName:string,email:string

可以为模型添加校验条件 Validations & Constraints

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
// models\user.js

"use strict";
const { Model } = require("sequelize");

module.exports = (sequelize, DataTypes) => {
class User extends Model {
static associate(models) {
// define association here
}
}
User.init(
{
email: DataTypes.STRING,
username: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: "用户名不能为空",
},
notEmpty: {
msg: "用户名不能为空",
},
async isUnique(value) {
const user = await User.findOne({ where: { username: value } });
if (user) {
throw new Error("用户名已存在");
}
},
len: {
args: [2, 20],
msg: "用户名长度在 2 到 20 之间",
},
},
},
password: DataTypes.STRING,
nickname: DataTypes.STRING,
sex: DataTypes.TINYINT,
company: DataTypes.STRING,
introduce: DataTypes.TEXT,
role: DataTypes.TINYINT,
avatar: DataTypes.STRING,
},
{
sequelize,
modelName: "User",
}
);
return User;
};

还可以通过 mysql-workbench 创建 ER 图,其中实线表示关联表没有自己的主键。

如果数据库不是在 Docker 环境,而是使用集成环境运行,迁移后表名可能是小写的,这会导致部署到服务器上之后 Mysql 严格区分大小写导致报错,可以在服务器上重新运行迁移

对于已经存在的表,如果可以在模型中对表名做映射的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User extends Model {
static associate(models) {
models.User.belongsToMany(models.Course, {
through: models.Like,
// 指明特殊的外键字段
foreignKey: "user_name",
as: "likeCourses",
});
}
}
User.init(
{
user_name: DataTypes.STRING,
},
{
sequelize,
modelName: "User",
//指定模型和表名的对应关系
tableName: "user_table",
// 不需要时间字段
timestamps: false,
}
);

执行迁移文件

Running Migrations, 创建模型会自动生成迁移文件,迁移文件需要按情况手动修改,修改字段类型,添加索引,等操作都是在迁移文件中完成的。

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
// migrations\20250117092337-create-user.js

module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
await queryInterface.createTable("Users", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER.UNSIGNED,
},
email: {
allowNull: false,
type: Sequelize.STRING,
},
username: {
allowNull: false,

type: Sequelize.STRING,
},
password: {
allowNull: false,
type: Sequelize.STRING,
},
nickname: {
allowNull: false,
type: Sequelize.STRING,
},
sex: {
type: Sequelize.TINYINT,
},
company: {
type: Sequelize.STRING,
},
introduce: {
type: Sequelize.TEXT,
},
role: {
type: Sequelize.TINYINT,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});

//添加索引
await queryInterface.addIndex("Users", ["email"], {
unique: true,
});

await queryInterface.addIndex("Users", ["username"], {
unique: true,
});
},
async down(queryInterface, Sequelize) {
// 回滚
await queryInterface.dropTable("Users");
},
};

修改完成后,执行迁移文件,这会在数据库中创建真正的数据表。

1
npx sequelize-cli db:migrate

如果数据表已经存在数据可以使用一下命令回归,这会执行迁移文件中的 down 方法,删除数据表

1
npx sequelize-cli db:migrate:undo  --name [迁移文件名称]

如果数据表数据有效不能删除,还需要对字段修改,就需要额外创建一个迁移文件, 并单独执行这个迁移文件, 并同步修改 model 文件新增或修改字段属性

1
2
3
npx sequelize-cli migration:generate --name add-avatar-to-user

npx sequelize-cli db:migrate --name xxxx-add-avatar-to-user

创建种子文件

Creating the first Seed 用于快速生成初始化的测试数据。

seed:all 会执行所有的种子文件,因此已经用 seed 初始化的表,还会再插入一遍数据

种子文件字段校验受到模型(索引字段),迁移文件(校验字段,类型)影响,需要注意字段是否匹配,否则命令执行可能报错

1
npx sequelize-cli seed:generate --name demo-user

需要手动修改种子文件,添加新增数据和删除数据的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// seeders\20250117101059-user.js

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkInsert("Users", [
{
email: "admin@live.com",
username: "admin",
password: "12345",
nickname: "admin",
sex: 1,
role: 0,
createdAt: new Date(),
updatedAt: new Date(),
},
]);
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete("Users", null);
},
};

开发路由

  • 在 router 文件夹下添加一个新的路由文件,并注册到 express 中

    1
    2
    3
    4
    // app.js
    var app = express();
    var courseRouter = require("./routes/course");
    app.use("/course", courseRouter);
  • 使用 模糊查询

  • 常用 查询方法

  • 201 响应码表示请求成功,且修改或创建了资源

  • 必须对用户提交的数据过滤,字段验证可以用模型实现,而且可以自定义异步方法,从数据库中查询校验

  • 避免孤儿数据
    外键约束影响性能,一般不允许使用。
    删除关联数据有风险,有些关联数据是用户数据不能删除
    可以检测只有没有关联信息的数据才能被删除

  • 查询 关联字段,文档只有语法,但是没有自动生成文件中如何添加的示例。

    在 model 文件中添加关联信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // models\course.js

    const { Model } = require("sequelize");
    module.exports = (sequelize, DataTypes) => {
    class Course extends Model {
    static associate(models) {
    // as 关联字段的别名, 在路由中使用的代码也需要改
    models.Course.belongsTo(models.User, { as: "user" });
    models.Course.belongsTo(models.Category, { as: "category" });
    }
    }
    // ...
    return Course;
    };

    查询条件中需要添加关联字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const condition = {
    // 排序字段
    order: [["id", "DESC"]],
    // 全局过滤 两个关联的外键
    attributes: {
    exclude: ["categoryId", "userId"],
    },
    include: [
    // as 关联字段别名,需要和model中的对应
    // attributes 用于过滤关联模型中的字段
    { model: User, as: "user", attributes: ["id", "username"] },
    { model: Category, as: "category", attributes: ["id", "name"] },
    ],
    };
  • 路由代码如下, 可以优化统一的错误处理以及参数的解析

    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
    // routes\course.js

    var express = require("express");
    var router = express.Router();
    const { Course, User, Category } = require("../../models");
    const { Op } = require("sequelize");

    router.get("/", async function (req, res, next) {
    try {
    const condition = {
    order: [["id", "DESC"]],
    // 全局过滤 两个关联的外键
    attributes: {
    exclude: ["categoryId", "userId"],
    },
    include: [
    // as 关联字段别名,需要和model中的对应
    // attributes 用于过滤关联模型中的字段
    { model: User, as: "user", attributes: ["id", "username"] },
    { model: Category, as: "category", attributes: ["id", "name"] },
    ],
    };

    const page = req.query.page || 1;
    const pageSize = req.query.pageSize || 10;

    condition.limit = pageSize;
    condition.offset = (page - 1) * pageSize;

    if (req.query.title) {
    condition.where = {
    title: {
    [Op.like]: `%${req.query.title}%`,
    },
    };
    }

    const { count, rows } = await Course.findAndCountAll(condition);

    res.json({
    status: true,
    message: null,
    data: {
    course: rows,
    pagination: {
    total: count,
    page,
    pageSize,
    },
    },
    });
    } catch (err) {
    console.log(err);
    res.status(500).json({
    status: false,
    message: err,
    });
    }
    });

    router.get("/:id", async function (req, res, next) {
    try {
    const { id } = req.params;
    const data = await Article.findByPk(id);

    if (data) {
    res.json({
    status: true,
    message: null,
    data,
    });
    } else {
    res.status(404).json({
    status: false,
    });
    }
    } catch {
    res.status(500).json({
    status: false,
    });
    }
    });

    router.post("/", async function (req, res, next) {
    try {
    const data = await Article.create(req.body);
    res.status(201).json({
    status: true,
    });
    } catch (err) {
    res.status(500).json({
    err: err,
    });
    }
    });

    router.delete("/:id", async function (req, res, next) {
    try {
    const data = await Article.destroy({
    where: {
    id: req.params.id,
    },
    });
    console.log(data);
    res.status(201).json({
    status: true,
    });
    } catch {
    res.status(500).json({
    status: false,
    });
    }
    });

    router.put("/:id", async function (req, res, next) {
    try {
    const count = await Article.count({
    where: { id: req.params.id },
    });
    if (count == 0) {
    res.status(404).json({
    status: false,
    message: "文章不存在",
    });
    } else {
    await Article.update(req.body, {
    where: { id: req.params.id },
    });
    res.status(201).json({
    status: true,
    message: "更新成功",
    });
    }
    } catch {
    res.status(500).json({
    status: false,
    });
    }
    });
    module.exports = router;

集合

Set集合的实现

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 Set {
constructor() {
this.set = { "a": 1, __proto__: { b: 1 } };
}
has(element) {
return Object.prototype.hasOwnProperty.call(this.set, element)
}
add(element) {
if (!this.has(element)) {
this.set[element] = element;
return true;
}
return false;
}
delete(element) {
if (this.has(element)) {
Reflect.deleteProperty(this.set, element)
return true;
}
return false;
}
clear() {
this.items = {};
}
size() {
return Reflect.ownKeys(this.set).length
}
values() {
return Object.values(this.items);
}
}

集合运算

  • 并集
1
2
3
4
5
6
7
8
9
10
function union(A, B) {
const set = new Set();
A.values().forEach(v => {
set.add(v)
});
B.values().forEach(v => {
set.add(v)
});
return set;
}
  • 交集
1
2
3
4
5
6
7
8
9
function intersection(A, B) {
const set = new Set();
A.values().forEach(item => {
if (B.has(item)) {
set.add(item);
}
});
return set;
}
  • 差集

A与B的差集表示存在于A,但是不存在于B

B与A的差集表示存在于B,但是不存在于A

1
2
3
4
5
6
7
8
9
function differenceSet(A, B) {
const set = new Set();
A.values().forEach(item => {
if (!B.has(item)) {
set.add(item);
}
});
return set;
}

只需要遍历集合长度较小的一个即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function differenceSet(A, B) {
const set = new Set();
let smallSet = A;
let bigSet = B;
if (smallSet.values().length > bigSet.values().length) {
smallSet = B;
bigSet = A;
}
smallSet.values().forEach(item => {
if (!bigSet.has(item)) {
set.add(item);
}
});
return set;
}
  • 子集
1
2
3
function childSet(A, B) {
return B.values().every(item => A.has(item))
}

Nodejs 基础 API

Buffer

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
// 20个字节
const b1 = Buffer.alloc(20);

// 直接去一块连续内存,可能有没有回收的垃圾数据
const b2 = Buffer.allocUnsafe(20);

// 为什么是 31 而不是01, 因为uft8 编码下并不是1
const b3 = Buffer.from("1");

// 无法保存文字的信息, 数组中的每一个必须是16进制 8 或2
const b4 = Buffer.from([1, "维护"]);

// 两块内存不会相同,内存上是分开的
const b5 = Buffer.alloc(3);
const b6 = Buffer.from(b5);

// 填充

const b7 = Buffer.alloc(5);
//会把整个buffer填满,如果长度不够,会截断
// 开始字节位置,结束字节位置
console.log(b7.fill("中", 1, 2));
console.log(b7.toString());

// 写入
const b8 = Buffer.alloc(5);
console.log(b8.write("中", 1, 4));

// toString 可以选择从哪一个字节下标开始解析

console.log(b7.toString("utf-8", 3, 6));

// slice 截取操作
const b9 = Buffer.from("为各位");
console.log(b9.slice());

// 查找可以指定从哪一位后面开始查找
console.log(b9.indexOf("为", 2));

// copy
const b10 = Buffer.from("为各位");

const b11 = Buffer.alloc(39);
// 开始结束位置
b10.copy(b11, 3, 6);

// concat

const b12 = Buffer.from("1");
const b13 = Buffer.from("2");
const b14 = Buffer.concat([b12, b13]);

// slice 方法已经被标记为过时,使用 subarray 从 Buffer 中截取一段数据
// start end
const b15 = b14.subarray(02);

// 判断
Buffer.isBuffer;

// 实现对 Buffer 的split
Buffer.prototype.split = function (sep) {
const length = Buffer.from(sep).length;
let ret = [];
let start = 0;
let offset = 0;
while ((offset = this.indexOf(sep, start)) !== -1) {
ret.push(this.subarray(start, offset).toString());
start = offset + length;
}
ret.push(this.subarray(start).toString());
return ret;
};

// 向buffer中写入数据
// value 必须是指定进制的整数
// offset从哪个字节开始写入
// LE 表示按小端字节序写入 1 => [01,00,00,00]
// LB 表示按大端字节序写入 1 => [00,00,00,01]
buf.writeInt32LE(value,offset);

// 从buffer中读取数据
// offset 表示从哪个位置开始读取
// 注意: 不能设置读取长度,他读取的长度是固定的,4个字节32位
buf.readInt32LE(offset)

Event Loop

Node.js 的事件循环基于 libuv,用于管理异步操作。事件循环分为多个阶段,每个阶段负责处理特定类型的回调。

1. Timers 阶段

  • 作用:处理由 setTimeout()setInterval() 创建的回调。
  • 行为
    • 检查到期的定时器,并执行其回调。
    • 如果没有到期的定时器,则直接跳到下一个阶段。

2. I/O Callbacks 阶段

  • 作用:处理几乎所有延迟的 I/O 回调(除了 close 事件、setImmediate 和定时器回调)。
  • 行为
    • 处理如网络请求错误等 I/O 回调。
    • 较少用在用户代码中,因为大多数 I/O 会在 Poll 阶段完成。

3. Idle, Prepare 阶段

  • 作用:供内部使用,用于准备事件循环和一些内部操作。
  • 行为
    • 通常不处理用户定义的回调。
    • 主要为 Node.js 引擎的内部运行服务。

4. Poll 阶段

  • 作用:处理新到达的 I/O 事件并执行相关回调。
  • 行为
    • 如果没有到期的定时器,这个阶段会阻塞,等待新的 I/O 事件。
    • 如果有定时器到期,会直接跳到 Timers 阶段。

5. Check 阶段

  • 作用:执行 setImmediate() 的回调。
  • 行为
    • 优先于下一次 Poll 阶段任务执行。
    • Poll 阶段结束后立即执行。

6. Close Callbacks 阶段

  • 作用:处理关闭事件的回调。
  • 行为
    • 例如处理 socket.on('close')fs.close() 的回调。
    • 用于释放资源时执行相关任务。

特殊任务:微任务队列(Microtask Queue)

  • 并不属于事件循环的某个阶段,而是会在每个阶段完成后执行。

  • 包括以下两种任务:

    • process.nextTick() 回调。
    • Promise.then().catch().finally()
  • 微任务优先于下一个事件循环阶段执行。

  • 例如,在 Timers 阶段执行完所有回调后,会立即执行微任务队列中的任务。

1
2
3
4
5
6
7
8
9
10
// setTimeout  setImmediate 执行时机并不确定
setTimeout(() => console.log("Timers"), 0);
setImmediate(() => console.log("Check"));
Promise.resolve().then(() => console.log("Microtask"));
process.nextTick(() => console.log("Next Tick"));

// 1. Next Tick
// 2. Microtask
// 3. Timers
// 4. Check
1
2
3
4
5
6
fs.readFileSync(() => {
setTimeout(() => console.log("Timers"), 0);
// setImmediate 永远先执行,因为 fs队列是在poll中
// 下一步会执行 check 队列中的 setImmediate
setImmediate(() => console.log("Check"));
});

模拟一个模块加载函数

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
const path = require("path");
const fs = require("fs");
const vm = require("vm");

class Module {
// 模块的导出对象,类似于 CommonJS 中的 `module.exports`
exports = {};

// 模块缓存,用于存储已加载的模块,避免重复加载
static cache = {};

// 文件扩展名及对应的加载器方法
static extensions = {
".js": (module) => {
// 读取 JavaScript 文件内容
let content = fs.readFileSync(module.id, "utf-8");

// 将文件内容包装成 CommonJS 模块的函数形式
content = Module.wrap(content);

// 将包装后的代码编译为可执行的函数
const script = new vm.Script(content);
const fn = script.runInThisContext();

// 调用包装函数,将 `exports`、`require` 等注入到模块上下文
fn(module.exports, myRequire, module, module.id, path.dirname(module.id));
},
".json": (module) => {
// 读取 JSON 文件内容
const content = fs.readFileSync(module.id, "utf-8");

// 将 JSON 内容解析为对象,并赋值给模块的 `exports`
module.exports = JSON.parse(content);
},
};

constructor(id) {
// 模块的唯一标识符,通常为文件的绝对路径
this.id = id;
}

// 包装模块代码,模拟 CommonJS 的运行环境
static wrap(content) {
return `(function(exports, require, module, __filename, __dirname) { ${content} })`;
}

// 解析文件名,包括支持自动添加扩展名
static resolveFileName(filePath) {
// 将文件路径转换为绝对路径
const absolutePath = path.resolve(filePath);

// 检查文件是否存在
if (fs.existsSync(absolutePath)) {
return absolutePath;
}

// 检查支持的文件扩展名
for (const ext of Object.keys(Module.extensions)) {
const fullPath = absolutePath + ext;
if (fs.existsSync(fullPath)) {
return fullPath;
}
}

// 如果文件未找到,抛出错误
throw new Error(`无法找到模块 '${filePath}'`);
}

// 加载模块,根据扩展名选择合适的加载器
load() {
const ext = path.extname(this.id); // 获取文件扩展名
const loader = Module.extensions[ext]; // 根据扩展名找到对应的加载器

if (loader) {
loader(this); // 执行加载器
} else {
throw new Error(`不支持的文件扩展名 '${ext}'`);
}
}
}

// 自定义的 `require` 方法,用于加载模块
function myRequire(filePath) {
// 解析文件名,确保获取的是有效路径
const resolvedPath = Module.resolveFileName(filePath);

// 如果模块已缓存,直接返回缓存中的 `exports`
if (Module.cache[resolvedPath]) {
return Module.cache[resolvedPath].exports;
}

// 创建一个新的模块实例
const module = new Module(resolvedPath);

// 将模块实例存入缓存
Module.cache[resolvedPath] = module;

// 加载模块内容
module.load();

// 返回模块的导出对象
return module.exports;
}

// 示例:加载一个模块
const res = myRequire("./path");

模拟一个 WriteStream

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
const fs = require("fs");
const EventEmitter = require("events");

function createWriteStream(path, options = {}) {
// 解构并设置默认参数
const {
flags = "w", // 文件打开模式(默认为写入模式)
encoding = "utf8", // 默认编码
autoClose = true, // 是否自动关闭文件描述符
emitClose = true, // 是否在关闭时触发 "close" 事件
start = 0, // 文件写入的起始位置
highWaterMark = 16 * 1024, // 写入的高水位标记(默认 16KB)
} = options;

// 内部状态变量
let offset = start; // 文件写入偏移量
let written = 0; // 当前已写入的字节数
let writing = false; // 是否正在写入
let cache = []; // 缓存区,用于存储等待写入的数据

// 定义一个可写流类
class WriteStream extends EventEmitter {
fd = null; // 文件描述符

// 打开文件
open() {
fs.open(path, flags, (err, fd) => {
if (err) {
this.emit("error", err); // 打开文件失败,触发错误事件
if (autoClose) {
this.destroy(); // 发生错误时自动关闭
}
return;
}
this.fd = fd; // 保存文件描述符
this.emit("open", fd); // 触发 open 事件
});
}

// 写入数据
write(chunk, encoding = "utf8", callback = () => {}) {
const buffer = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk, encoding);

const canWrite = written < highWaterMark; // 判断当前写入是否超出高水位标记

// writeStream 首次会直接写入数据,后面的数据会写入缓存
if (writing) {
// 如果正在写入,缓存数据并延迟写入
cache.push({ buffer, callback });
} else {
writing = true;
this._write(buffer, callback);
}

return canWrite; // 返回是否可以继续写入
}

// 实际写入操作
_write(buffer, callback) {
if (this.fd === null) {
// 如果文件描述符尚未打开,等待 open 事件后再写入
return this.once("open", () => this._write(buffer, callback));
}

fs.write(
this.fd,
buffer,
0,
buffer.length,
offset,
(err, bytesWritten) => {
if (err) {
this.emit("error", err); // 写入失败,触发错误事件
if (autoClose) {
this.destroy();
}
return;
}

offset += bytesWritten; // 更新写入偏移量
written -= bytesWritten; // 更新已写入的字节数

// 检查缓存区中是否还有待写入的数据
const next = cache.shift();
if (next) {
this._write(next.buffer, next.callback);
} else {
writing = false; // 当前写入完成
callback(); // 执行回调
if (written < highWaterMark) {
this.emit("drain"); // 缓存区清空,触发 drain 事件
}
}
}
);
}

// 销毁流(关闭文件描述符)
destroy() {
if (this.fd !== null) {
fs.close(this.fd, (err) => {
if (emitClose) {
this.emit("close"); // 触发 close 事件
}
});
this.fd = null;
}
}
}

// 创建流实例并打开文件
const stream = new WriteStream();
stream.open();

return stream;
}

// 使用示例
const writeStream = createWriteStream("./package.txt");

// 监听事件
writeStream.on("open", (fd) => console.log("文件已打开,描述符:", fd));
writeStream.on("error", (err) => console.error("发生错误:", err));
writeStream.on("drain", () => console.log("缓存已清空,可以继续写入"));

// 写入数据
writeStream.write("Hello, World!", "utf-8", (err) => {
if (err) console.error("写入失败:", err);
else console.log("写入成功");
});

writeStream.write("Another line.", "utf-8", (err) => {
if (err) console.error("写入失败:", err);
else console.log("写入成功");
});

模拟 TCP 粘包的解决方法

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
// util.js
const Buffer = require("buffer").Buffer;

// 获取数据包长度
const getLength = (chunk) => {
if (chunk.length < 12) return 0; // 不满足最小长度的包直接返回 0
return 12 + chunk.readInt32LE(4); // 长度字段存储在偏移量为 4 的位置
};

// 编码消息为特定格式的 Buffer
const encode = (string, id = 1) => {
const strBuffer = Buffer.from(string); // 将字符串转为 Buffer
const headerBuffer = Buffer.allocUnsafe(12); // 创建 12 字节的头部
headerBuffer.writeInt32LE(id, 0); // 写入消息 ID (4 字节)
headerBuffer.writeInt32LE(strBuffer.length, 4); // 写入消息长度 (4 字节)
return Buffer.concat([headerBuffer, strBuffer]); // 合并头部和消息
};

// 解码 Buffer 为字符串
const decode = (chunk) => {
const bodyLength = chunk.readInt32LE(4); // 获取消息体的长度
return chunk.subarray(12, 12 + bodyLength).toString(); // 提取消息体并转为字符串
};

module.exports = { getLength, encode, decode };
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
// server.js
const net = require("net");
const { getLength, encode, decode } = require("./utils");

// 创建服务端
const server = net.createServer();

server.listen(8080, "127.0.0.1", () => {
console.log("服务端启动,监听端口 8080");
});

server.on("connection", (socket) => {
console.log("客户端连接成功");

let unReadChunks = Buffer.alloc(0); // 存储未处理的缓冲区数据

socket.on("data", (chunk) => {
unReadChunks = Buffer.concat([unReadChunks, chunk]); // 拼接收到的数据
let len;

// 循环处理完整的数据包
while ((len = getLength(unReadChunks))) {
const cur = unReadChunks.subarray(0, len); // 提取当前完整数据包
unReadChunks = unReadChunks.subarray(len); // 更新未处理数据

// 解码消息,生成回复
const response = encode(decode(cur) + " 服务端恢复", cur.readInt32LE(0));
socket.write(response); // 发送回复消息
}
});

socket.on("close", () => console.log("客户端连接关闭"));
socket.on("error", (err) => console.error("服务端错误:", err));
});
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
// client.js
const net = require("net");
const { getLength, encode, decode } = require("./utils");

// 创建客户端
const client = net.createConnection(8080, "127.0.0.1", () => {
console.log("客户端连接成功");

// 发送多条消息
client.write(encode("消息1"));
client.write(encode("消息2"));
client.write(encode("消息3"));
client.write(encode("消息4"));
client.write(encode("消息5"));
});

let unReadChunks = Buffer.alloc(0); // 存储未处理的缓冲区数据

client.on("data", (chunk) => {
unReadChunks = Buffer.concat([unReadChunks, chunk]); // 拼接收到的数据
let len;

// 循环处理完整的数据包
while ((len = getLength(unReadChunks))) {
const cur = unReadChunks.subarray(0, len); // 提取当前完整数据包
unReadChunks = unReadChunks.subarray(len); // 更新未处理数据

// 解码消息并输出
console.log("收到服务端回复:", decode(cur));
}
});

client.on("close", () => console.log("客户端连接关闭"));
client.on("error", (err) => console.error("客户端错误:", err));
  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信