这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
了解了React Fiber架构的原理和基本过程,看一些关键问题上的具体实现也会更加有条理了~
本文就React中的Diff算法与状态更新具体是如何实现的做个总结,一起来看吧~
Diff算法
Diff 发生在 render 阶段的“递”操作中,对于需要更新的组件,比较current Fiber
和JSX对象
生成workInProgress Fiber
。
我们知道,将前后两棵树完全比对的算法的复杂程度为 O(n^3 )
,其中n
是树中元素的数量。
为了降低算法复杂度,React
的diff
会预设三个限制:
- 只对同级元素进行
Diff
。如果一个DOM节点
在前后两次更新中跨越了层级,那么React
不会尝试复用他。 - 两个不同类型的元素会产生出不同的树。如果元素由
div
变为p
,React会销毁div
及其子孙节点,并新建p
及其子孙节点。 - 开发者可以通过
key prop
来暗示哪些子元素在不同的渲染下能保持稳定。
单节点
判断key
和type
是否相同,都相同时一个DOM节点
才能复用。
多节点
React团队
发现,在日常开发中,相较于新增
和删除
,更新
组件发生的频率更高。基于以上原因,Diff算法
的整体逻辑会经历两轮遍历:第一轮遍历:处理更新
的节点;第二轮遍历:处理剩下的不属于更新
的节点。
举个例子:
- 有一组待更新的列表,首先进行第一遍遍历;
- indexOld = 0, indexNew = 0, 发现key不变,组件可复用,A不变,lastPlacedIndex = 0;
- indexOld = 1, indexNew = 1,key改变,不能复用,则跳出第一轮遍历;
- 第二轮遍历开始,新旧列表都没遍历完,将旧列表保存为map结构,只遍历新列表;
- indexNew = 1,在旧列表中是否存在相同的key,找到了indexOld = 2,因为indexOld > lastPlacedIndex(0), 所以C可以复用,不用移动,赋值lastPlacedIndex = 2;
注意: 如果没有找到,就创建新的节点,并标记新增; 找到了,且indexOld >= lastPlacedIndex,则组件复用,无需移动,赋值lastPlacedIndex为indexOld; 找到了,但是indexOld < lastPlacedIndex,则组件复用,但是向右移动(indexOld - lastPlacedIndex)位。
- 继续遍历新列表,indexNew = 2,找到了indexOld = 3,indexOld > lastPlacedIndex(2), 所以D也可以复用,不用移动,赋值lastPlacedIndex = 3;
- 继续遍历新列表,indexNew = 3,找到了indexOld = 1,indexOld < lastPlacedIndex(3), 所以B标记移动;
- 假如旧列表存在没有遍历到的节点,标记删除,结束。
状态更新
流程
Update对象分类
首先,我们将可以触发更新的方法所隶属的组件分类:
- ReactDOM.render —— HostRoot
- this.setState —— ClassComponent
- this.forceUpdate —— ClassComponent
- useState —— FunctionComponent
- useReducer —— FunctionComponent
ClassComponent
与HostRoot
共用一套Update
结构,FunctionComponent
单独使用一种Update
结构。
ClassComponent的Update对象
下面是ClassComponent
对用的Update
结构:
export type Update<State> = {
eventTime: number, // 任务时间
lane: Lane, // 优先级相关字段
tag: 0 | 1 | 2 | 3, // 更新的类型,包括`UpdateState` | `ReplaceState` | `ForceUpdate` | `CaptureUpdate`
payload: any, // 更新挂载的数据,不同类型组件挂载的数据不同。对于`ClassComponent`,为`this.setState`的第一个传参。对于`HostRoot`,为`ReactDOM.render`的第一个传参
callback: (() => mixed) | null, // 更新的回调函数
next: Update<State> | null, // 与其他`Update`连接形成链表
};
export type SharedQueue<State> = {
pending: Update<State> | null, // 触发更新时,产生的`Update`会保存在`shared.pending`中形成单向环状链表。当由`Update`计算`state`时这个环会被剪开并连接在`lastBaseUpdate`后面
interleaved: Update<State> | null,
lanes: Lanes,
};
// UpdateQueue 对象被存放在fiber.updateQueue
export type UpdateQueue<State> = {
baseState: State, // 本次更新前该`Fiber节点`的`state`,`Update`基于该`state`计算更新后的`state`
// 之所以在更新产生前该`Fiber节点`内就存在`Update`,是由于某些`Update`优先级较低所以在上次`render阶段`由`Update`计算`state`时被跳过
firstBaseUpdate: Update<State> | null, // 本次更新前该`Fiber节点`已保存的`Update`链表头
lastBaseUpdate: Update<State> | null, // 本次更新前该`Fiber节点`已保存的`Update`链表尾
shared: SharedQueue<State>,
effects: Array<Update<State>> | null, // 保存`update.callback !== null`的`Update`
};
小结一下:
fiber.updateQueue.firstBaseUpdate
和fiber.updateQueue.lastBaseUpdate
保存上次更新时因为优先级低被跳过的Update
- 新的
Update
以环状链表的方式保存在fiber.updateQueue.shared.pending
- 更新调度完成后进入render阶段,此时
fiber.updateQueue.shared.pending
的环被剪开并连接在fiber.updateQueue.lastBaseUpdate
后面 - 以
fiber.updateQueue.baseState
为初始state,从fiber.updateQueue.firstBaseUpdate
开始依次与遍历到的每个Update
计算并产生新的state
,如果有优先级低的Update
会被跳过 state
的变化在render阶段产生与上次更新不同的JSX
对象,通过Diff算法
产生effectTag
,在commit阶段渲染在页面上- 渲染完成后
workInProgress Fiber树
变为current Fiber树
,整个更新流程结束
优先级
React
在render阶段前,会通过Scheduler
调度任务,调用Scheduler
提供的方法runWithPriority
,优先级最终会反映到update.lane
变量上。
两个问题:
render阶段
可能被中断。如何保证updateQueue
中保存的Update
不丢失?
答:得益于Fiber的双缓存,当render阶段
被中断后重新开始时,会基于current updateQueue
克隆出workInProgress updateQueue
。由于current updateQueue.lastBaseUpdate
已经保存了上一次的Update
,所以不会丢失。当commit阶段
完成渲染,由于workInProgress updateQueue.lastBaseUpdate
中保存了上一次的Update
,所以 workInProgress Fiber树
变成current Fiber树
后也不会造成Update
丢失。
- 有时候当前
状态
需要依赖前一个状态
。如何在支持跳过低优先级状态
的同时保证状态依赖的连续性?
答:Update
对象内部维护baseState
,作为该更新发生之前的state
, 而fiber
上还会保留memoizedState
,作为上一次render阶段
之前计算出来的state
,会用于渲染在页面。当有Update
更新被跳过时,baseState
不等于fiber.memoizedState
。而Update
链还是原来的顺序,不会随着状态一起跳过。
FunctionComponent的Hook对象
不同于ClassComponent
,更新对象保存在fiber.updateQueue
上,FunctionComponent
的更新对象以Hook
对象连成的链表的形式,保存在fiber.memoizedState
上。
Hook
对象结构如下:
export type Hook = {
memoizedState: any, // 保存的单一`hook`对应的数据
baseState: any,
baseQueue: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null, // 保存update对象
interleaved: Update<S, A> | null,
lanes: Lanes,
dispatch: (A => mixed) | null, // 保存dispatchAction.bind()的值,保存了参数
lastRenderedReducer: ((S, A) => S) | null, // 上一次render时使用的reducer
lastRenderedState: S | null, // 上一次render时的state
};
type Update<S, A> = {
lane: Lane,
action: A,
eagerReducer: ((S, A) => S) | null,
eagerState: S | null,
next: Update<S, A>,
};
注意:
- 一个Hook对应一个useState / useReducer
- Hook内的queue是对应Hook的触发情况
Hook的memoizedState数据
不同类型hook
的memoizedState
保存不同类型数据,具体如下:
- useState:对于
const [state, updateState] = useState(initialState)
,memoizedState
保存state
的值 - useReducer:对于
const [state, dispatch] = useReducer(reducer, {});
,memoizedState
保存state
的值 - useEffect:
memoizedState
保存包含useEffect回调函数
、依赖项
等的链表数据结构effect
,你可以在这里 (opens new window)看到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
Hook嵌套问题
因为FunctionComponent
组件mount
时的hook
与update
时的hook
来源于不同的dispatcher
对象,其对应的fiber
通过current === null || current.memoizedState === null
来判断是否是mount
,从而赋值ReactCurrentDispatcher.current
。
另外,不同的调用栈上下文ReactCurrentDispatcher.current
的赋值也不一样。
因此,在一个Hook中调用另一个Hook会出错。
不同类型Hook的具体实现
这一部分说实话没太弄明白,先留个坑吧~等多看几遍源码之后再来填它~