React理念与核心流程
# 代数效应
代数效应是 FP 中的理念,算是一种处理 side effect 的一种机制吧.
假如有一种叫try...handle
的语法,(可以理解为是一个不报错的try...catch
),执行完 hanle 之后仍然会回到之前的状态,于是就能够实现中断-恢复
这一重要机制.
假如说 es2025 能有个这语法,他们实现异步可中断的更新
应该会容易许多.
# 为啥不用generate
实现异步可中断?
优先级.因为 generate 只能实现中断,不能实现优先级.
# 老的 React 架构
只有协调器和渲染器
协调器管 diff,渲染器进行渲染,并且不支持异步可中断的更新.
以前的协调器也叫 stack reconciler 16 以后的协调器叫 fiber reconciler
# JSX
JSX 是 React.createElement
的语法糖.
它接收三个参数,并且返回一个名为 ReactElement 的新函数作为返回值,该函数返回一个包含组件数据的对象
function createElement(type:string|Component, config, children) {
//...
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props
);
}
2
3
4
5
6
7
8
9
10
11
12
第一个参数为 type, type 为原生 html 节点字符串或者 Component (Component 有可能为 classComponent,也有可能为 functionComponent)
第二个参数为节点上的属性,第三个参数为 jsx 上的子元素(是解构传进去的)
当调用完React.createElement
之后,此次就有了组件的内容和结构,这为接下来创建 fiber 做了铺垫.
# render 阶段
# 挂载阶段
会对 上面的 jsx 对象 上的每一个节点进行 dfs,也就是说每一个节点都会执行 beiginWork 和 completeWork
在递进去的时候会执行 beginWork 在归出来的时候会执行 completeWork,最终会返回 一颗完整的 workInprogress fiber.
# 更新阶段
更新时 current 已经存在了,考虑到性能,如果我们要生成一颗新的 workInprogress fiber,最好的做法是尽量复用之前的 current fiber,而不是每次都去重新 dfs 我们要生成的 jsx。那怎么才能做到尽量复用呢?关键就是要看我们如何去找出差异的地方。
这里的“找差异”,就是所谓的 diff 算法,下文中会对 react 的 diff 算法做详细的介绍。
当 diff 走完以后,就会对要操作的 dom 打上 tag,这些 tag 会串成一根双向链表,然后放到 commit 阶段统一处理。
# 更新阶段中 diff 算法
在 render 阶段,会执行reconcileChildFibers
,这个方法会把 jsx 对象和当前的 fiber(current fiber)进行比对,在内存中生成一个 vdom(React 中叫 workInProgress Fiber).
# React 中 diff 算法的剪枝策略:
- 只对同级元素进行 diff,如果一个 dom 在更新的过程中跨了层级,React 不会去尝试复用这个 dom.
- tag 和 key 不同,则直接销毁.
# 单一节点的 diff
单节点的 diff 逻辑很简单,就是比较前后的 key 和 tagName,完全相同才复用.
# 多节点的 diff
多节点的要处理的情况有点多,总共需要遍历两轮,第一轮处理更新,第二轮处理增删和位置变化.
# 第一轮遍历
遍历比较 jsx 里的newChildren
和oldFiber
.遍历会走接下来的逻辑
- 只要 key 不同,就会跳出当前遍历
- 如果 type 不同,会在这步先给 oldFiber 打上
DELETION
的标记,然后创建一个新的 fiber,并打上Placement
oldFiber
和newChildren
中只要有一个遍历完,就会跳出当前循环
# 第二轮遍历
对于第一轮遍历,会产生四种结果.
第一轮遍历结束后,会会产生四种结果.
- newChildren 和 oldFiber 都遍历完
说明他们一样长,此时不需要处理,第一轮就已经更新好了
- 只有 newChildren 没跑完
此时说明有新节点,接下来只需要遍历新节点并给他们打上Placement
标记
- 只有 oldFiber 没跑完
此时说明删节点了,只需要遍历 oldFiber 并给他们打上DELETION
标记
- 这俩都没遍历完
想想什么情况下这俩都没遍历完呢?没错,就是上面由于 key 不同导致跳出的循环! 此时说明节点位置发生了变化.
那如何处理移动的节点呢?
首先,先将所有没处理的oldFiber
存到一个 map 中,key 为oldFiber
中的 key,value 为oldFiber
自身
接下来一边遍历 newChildren,一边往 oldFiber 上打 tag,同时用一个变量来存储上一次复用节点的位置(lastPlacedIndex)
在接下来的过程中,如果 newChildren 中所对应的oldIndex>=lastPlacedIndex
,就通过 map,给旧的 fiber 上打上"复用"的标记
否则就移动 oldFiber 中对应的节点.
(说人话就是我们需要看当前遍历对象已经存在的索引位置是不是比上次 fiber 中的索引位置小,如果小的话就往右边挪)
# 拓展: vue3 中的 diff 算法
vue3 中的 diff 算法和 react 在处理单节点 diff 的思路一模一样,都是根据 key 和 tagname 进行判断。
不同的是 vue 在处理多节点 diff 时用了一些很有趣的思路,这和 react 是完全不同的。
首先 vue 会先用"双端比较",处理头尾的节点,vue2 也是用的该思路。
对于中间的部分,在 vue3 中用了一个非常聪明的小算法。
假设现在处理后的新旧节点如下所示。
// null 是经过双端比较已经处理过的元素
const oldChildren = [null,null,"c","d","e","f",null]
// 0 1 2 3 4 5 6
const newChildren = [null,null,"f","c","d","e","h",null]
function getOriPosition(oldChildren,newChildren){
// ...
return [......]
}
2
3
4
5
6
7
8
9
10
11
首先获取 newChildren 中每一个元素原来的位置。
当我们调用完 getOriPosition()
后,会得到 [5,2,3,4,-1] 这份原来的位置信息。
const oriPosition = getOriPosition(oldChildren, newChildren);
console.log(oriPosition);
// [5,2,3,4,-1]
// 由于h是新增的节点,所以我们找不到其原来的位置信息。
2
3
4
5
6
接下来,我们要找出oriPosition
中的最长递增子序列 (opens new window)。
得到 [2,3,4]。
至此,我们的任务就完成了,因为[2,3,4]所对应的[c,d,e]正是能复用的最长部分。接下来我们只需要把f
挪到这部分的最前面,并且在这部分的最后新增一个h
,diff 完成。
# commit 阶段
# before mutation
执行commitBeforeMutationEffect
.
这个函数主要做了两件事.
- 获取当前的 dom 快照
- 根据优先级调度副作用
# mutation
遍历 effectlist,依次执行commitMutationEffects
,在遍历的过程中会根据EffectTag
的类型来调用不同的函数处理 fiber
在这两个阶段之间,root.current 会暂时指向 finishedWork.
# layout
会再次遍历 effectlist,并依次执行commitMutationEffects
,该方法依然会根据EffectTag
类型来处理 fiber,并最终更新 ref.
# hooks
# 实现useState
type Update<State> = null | {
next: Update<State>;
action: (state: State) => State;
};
type Queue = {
pending: Update<any> | null;
};
type Hook = null | {
queue: Queue;
memoizedState: any;
next: Hook;
};
interface Fiber {
memoizedState: Hook;
stateNode: Function;
}
let isMount = true;
let workInProgressHook: Hook = null; // 用来指示当前正在处理的hook
const fiber: Fiber = {
memoizedState: null,
stateNode: App,
};
// 模拟render阶段
function run() {
workInProgressHook = fiber.memoizedState;
const app = fiber.stateNode();
isMount = false; // render之后,commit之前的isMount为false
return app;
}
function dispatchAction(queue: Queue, action) {
console.log('触发action');
const curUpdate: Update<any> = {
next: null,
action,
};
if (queue.pending == null) {
// 初始化环状链表
curUpdate.next = curUpdate;
} else {
// 在环状链表中插入元素
const firstUpdate = queue.pending.next;
curUpdate.next = firstUpdate; // 当前的更新直接挂到前面(我们先不考虑优先级的问题)
queue.pending.next = curUpdate; // 尾节点也要跟着更新
}
// 我们定义,queue.pending保存最后一个update
queue.pending = curUpdate;
// dispatchAction之后触发render
run();
}
const useState = <T>(initState: T) => {
let hook: Hook = null;
if (isMount) {
hook = {
queue: {
pending: null,
},
memoizedState: initState,
next: null,
};
if (fiber.memoizedState == null) {
// 最终把这个初始化的hook挂载到memoizedState上
fiber.memoizedState = hook;
} else {
// 如果已经挂载过了,就追加到后面形成链表(多个hooks的情况)
workInProgressHook.next = hook;
}
workInProgressHook = hook;
} else {
// 因为我们在mount时已经在workInProgressHook变量上保存了
hook = workInProgressHook;
workInProgressHook.next = hook;
}
let baseState = hook.memoizedState;
const lastUpdate = hook.queue?.pending; // 我们在dispatchAction中会定义
// 环状链表,此时cur指向的是链表头
const firstUpdate = lastUpdate?.next;
if (lastUpdate) {
let curUpdate = firstUpdate;
do {
const action = curUpdate.action;
baseState = action(baseState);
curUpdate = curUpdate.next;
} while (curUpdate !== firstUpdate);
// 计算完成,pending置空
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
};
function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(1);
console.log({ count, num });
return {
click() {
setCount((count) => count + 1);
setNum((num) => num + 1);
},
};
}
// @ts-ignore
window.app = run();
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