背景知识
React16架构可以分为三层:
Fiber的含义
渲染流程概览
图例
render阶段
目的:Scheduler 调度 Reconciler 找出变化的组件,在其虚拟DOM上打上代表增/删/更新的标记
React16 的 Fiber Reconciler 通过遍历的方式实现可中断的递归, Scheduler判断当前帧是否还有时间剩余,并且根据任务优先级,调度运行 Reconciler 的任务
整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer(下一阶段commit)
“递”阶段 (beginWork)
“归”阶段 (completeWork)
commit阶段
commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参
在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的 单向链表effectList,这些Fiber节点的 updateQueue 中保存了变化的props
这些副作用对应的DOM操作在commit阶段执行,此外一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)需要在commit阶段执行
before mutation阶段(执行DOM操作前)
mutation阶段(执行DOM操作)
layout阶段(执行DOM操作后)
diff算法
执行时机
在render 阶段的 beginWork 中,在 shouldComponentUpdate 生命周期函数之后
对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber节点。
diff算法的输入输出
Diff的瓶颈以及React如何应对
由于Diff操作本身也会带来性能损耗,React文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O(n 3 ),其中n是树中元素的数量。
如果在React中使用了该算法,那么展示1000个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。
为了降低算法复杂度,React的diff会预设三个限制:
只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。
Diff算法的实现
useState的简易实现
代码实现
javascript1
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
106const fiber = {
// 将多个hook的数据以链表形式存储,hooks指向第一个hook
hooks: null,
// 指向当前正在工作的hook
currentHook: null,
// 指向组件的第一个hook
firstHook: null,
// 组件是 mount 阶段还是 update 阶段
isMount: true,
stateNode: App,
}
// 构建hook.queue中存储update的环状链表
function dispatchAction(hook, action) {
const update = {
action,
next: null,
}
// 构建update的环状链表,queue指向最后一个update
if (hook.queue === null) {
update.next = update
} else {
update.next = hook.queue.next
hook.queue.next = update
}
hook.queue = update
// 触发重渲染
render()
}
function useState(initialState) {
let hook
if (fiber.isMount) {
hook = {
// 环状链表,存储了最后一个update的指针
queue: null,
memoizedState: initialState,
next: null
}
if (!fiber.firstHook) {
fiber.firstHook = hook
} else {
fiber.currentHook.next = hook
}
fiber.currentHook = hook
} else {
hook = fiber.currentHook
fiber.currentHook = fiber.currentHook.next
// 遍历hook.queue中存储的update,baseState + update = newState
let baseState = hook.memoizedState
if (hook.queue) {
// hook.queue存储了最后一个update的指针,所以hook.queue.next就是第一个update的指针
let firstUpdate = hook.queue.next
do {
const action = firstUpdate.action
baseState = action(baseState)
firstUpdate = firstUpdate.next
} while (firstUpdate !== hook.queue.next)
hook.memoizedState = baseState
// 计算结束,清除update链表
hook.queue = null
}
}
return [hook.memoizedState, dispatchAction.bind(null, hook)]
}
function render() {
// 重置currentHook为第一个hook
fiber.currentHook = fiber.firstHook
const app = fiber.stateNode()
fiber.isMount = false
return app
}
// 组件
function App() {
const [num, updateNum] = useState(0)
const [name, updateName] = useState('a')
console.log('------------------')
console.log('isMount: ', fiber.isMount)
console.log('num: ', num)
console.log('name: ', name)
return {
onClickNum() {
updateNum(num => num + 1)
},
onClickName() {
updateName(name => name + 'a')
},
}
}
// 初始渲染组件,赋值给app变量方便在控制台触发更新
window.app = render()使用说明
- 在浏览器控制台手动调用app的方法:app.onClickNum(), app.onClickName()
- 查看打印的状态变化javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20------------------
isMount: true
num: 0
name: a
$ app.onClickNum()
------------------
isMount: false
num: 1
name: a
undefined
$ app.onClickName()
------------------
isMount: false
num: 1
name: aa
undefined
核心解析:
- fiber是工作单元,单个组件的数据都存储在fiber数据结构中
- fiber.hooks存储了组件中调用的多个useState对应的hook数据,以链表的数据结构存储
- 单个hook中的queue存储了单次更新中多次setXXX方法触发的update的数据,以链表的数据结构存储
- 每次更新,遍历fiber.hooks链表,遍历hook.queue链表,计算更新后的状态
Hook理解
Hook的数据结构:组件中每一次 useXXX API 的调用都会创建一个hook对象,存入Fiber存储的hook链表中
javascript1
2
3
4
5
6
7
8
9const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};memoizedState
不同类型hook的memoizedState保存不同类型数据,具体如下:
useState:对于const [state, updateState] = useState(initialState),memoizedState保存state的值
useReducer:对于const [state, dispatch] = useReducer(reducer, {});,memoizedState保存state的值
useEffect:memoizedState保存包含useEffect回调函数、依赖项等的链表数据结构effect,你可以在 这里 看到effect的创建过程。effect链表同时会保存在fiber.updateQueue中
useRef:对于useRef(1),memoizedState保存{current: 1}
useMemo:对于useMemo(callback, [depA]),memoizedState保存[callback(), depA]
useCallback:对于useCallback(callback, [depA]),memoizedState保存[callback, depA]。与useMemo的区别是,useCallback保存的是callback函数本身,而useMemo保存的是callback函数的执行结果
有些hook是没有memoizedState的,比如:
- useContext
useState和useReducer
useState和useReducer极为相似,useState即reducer参数为basicStateReducer的useReducer
mount时:创建hook对象(保存初始状态),接入Fiber存储的hooks链表中
update时:找到对应的hook对象,根据update对象链表计算该hook的新state并返回
useEffect
useEffect的执行包括两个阶段:销毁函数的执行、回调函数的执行
需要保证所有组件useEffect的销毁函数必须都执行完后才能执行任意一个组件的useEffect的回调函数
layout阶段:创建销毁函数数组和回调函数数组
layout阶段之后浏览器空闲时:消费销毁函数数组和回调函数数组,先遍历执行完销毁函数数组里的方法,再遍历执行回调函数数组里的方法
useRef
对于FunctionComponent,useRef负责创建Hook对象,保存初始数据在hook对象中,并返回对应的ref ({ current: xxx })
对于赋值了ref属性的HostComponent与ClassComponent,会在render阶段经历赋值Ref effectTag,在 commit阶段 执行对应ref操作
useMemo和useCallback
两者区别:
- mountMemo 会将回调函数的执行结果作为 value 保存
- mountCallback 会将回调函数作为 value 保存
mount时:useMemo和useCallback负责创建Hook对象,保存value和依赖项([value, deps])在hook对象中,并返回对应的value
update时:浅比较依赖项,决定是否返回新的value