title |
---|
状态与副作用 |
在前文我们已经分析了fiber树
从构造
到渲染
的关键过程. 本节我们站在fiber
对象的视角, 考虑一个具体的fiber
节点如何影响最终的渲染.
回顾fiber 数据结构, 并结合前文fiber树构造
系列的解读, 我们注意到fiber
众多属性中, 有 2 类属性十分关键:
-
fiber
节点的自身状态: 在renderRootSync[Concurrent]
阶段, 为子节点提供确定的输入数据, 直接影响子节点的生成. -
fiber
节点的副作用: 在commitRoot
阶段, 如果fiber
被标记有副作用, 则副作用相关函数会被(同步/异步)调用.
export type Fiber = {|
// 1. fiber节点自身状态相关
pendingProps: any,
memoizedProps: any,
updateQueue: mixed,
memoizedState: any,
// 2. fiber节点副作用(Effect)相关
flags: Flags,
subtreeFlags: Flags, // v17.0.2未启用
deletions: Array<Fiber> | null, // v17.0.2未启用
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
|};
与状态
相关有 4 个属性:
fiber.pendingProps
: 输入属性, 从ReactElement
对象传入的 props. 它和fiber.memoizedProps
比较可以得出属性是否变动.fiber.memoizedProps
: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps
, 生成子节点之后会把pendingProps
赋值给memoizedProps
用于下一次比较.pendingProps
和memoizedProps
比较可以得出属性是否变动.fiber.updateQueue
: 存储update更新对象
的队列, 每一次发起更新, 都需要在该队列上创建一个update对象
.fiber.memoizedState
: 上一次生成子节点之后保持在内存中的局部状态.
它们的作用只局限于fiber树构造
阶段, 直接影响子节点的生成.
与副作用
相关有 4 个属性:
fiber.flags
: 标志位, 表明该fiber
节点有副作用(在 v17.0.2 中共定义了28 种副作用).fiber.nextEffect
: 单向链表, 指向下一个副作用fiber
节点.fiber.firstEffect
: 单向链表, 指向第一个副作用fiber
节点.fiber.lastEffect
: 单向链表, 指向最后一个副作用fiber
节点.
通过前文fiber树构造
我们知道, 单个fiber
节点的副作用队列最后都会上移到根节点上. 所以在commitRoot
阶段中, react
提供了 3 种处理副作用的方式(详见fiber 树渲染).
另外, 副作用
的设计可以理解为对状态
功能不足的补充.
状态
是一个静态
的功能, 它只能为子节点提供数据源.- 而
副作用
是一个动态
功能, 由于它的调用时机是在fiber树渲染阶段
, 故它拥有更多的能力, 能轻松获取突变前快照, 突变后的DOM节点等
. 甚至通过调用api
发起新的一轮fiber树构造
, 进而改变更多的状态
, 引发更多的副作用
.
fiber
对象的这 2 类属性, 可以影响到渲染结果, 但是fiber
结构始终是一个内核中的结构, 对于外部来讲是无感知的, 对于调用方来讲, 甚至都无需知道fiber
结构的存在. 所以正常只有通过暴露api
来直接或间接的修改这 2 类属性.
从react
包暴露出的api
来归纳, 只有 2 类组件支持修改:
本节只讨论使用
api
的目的是修改fiber
的状态
和副作用
, 进而可以改变整个渲染结果. 本节先介绍 api 与状态
和副作用
的联系, 有关api
的具体实现会在class组件
,Hook原理
章节中详细分析.
class App extends React.Component {
constructor() {
this.state = {
// 初始状态
a: 1,
};
}
changeState = () => {
this.setState({ a: ++this.state.a }); // 进入reconciler流程
};
// 生命周期函数: 状态相关
static getDerivedStateFromProps(nextProps, prevState) {
console.log('getDerivedStateFromProps');
return prevState;
}
// 生命周期函数: 状态相关
shouldComponentUpdate(newProps, newState, nextContext) {
console.log('shouldComponentUpdate');
return true;
}
// 生命周期函数: 副作用相关 fiber.flags |= Update
componentDidMount() {
console.log('componentDidMount');
}
// 生命周期函数: 副作用相关 fiber.flags |= Snapshot
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate');
}
// 生命周期函数: 副作用相关 fiber.flags |= Update
componentDidUpdate() {
console.log('componentDidUpdate');
}
render() {
// 返回下级ReactElement对象
return <button onClick={this.changeState}>{this.state.a}</button>;
}
}
-
状态相关:
fiber树构造
阶段. -
副作用相关:
fiber树渲染
阶段.- 生命周期:
getSnapshotBeforeUpdate
在fiber树渲染
阶段(commitRoot->commitBeforeMutationEffects->commitBeforeMutationEffectOnFiber
)执行(链接). - 生命周期:
componentDidMount
在fiber树渲染
阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber
)执行(链接). - 生命周期:
componentDidUpdate
在fiber树渲染
阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber
)执行(链接).
- 生命周期:
可以看到, 官方api
提供的class组件
生命周期函数实际上也是围绕fiber树构造
和fiber树渲染
来提供的.
注: function组件
与class组件
最大的不同是: class组件
会实例化一个instance
所以拥有独立的局部状态; 而function组件
不会实例化, 它只是被直接调用, 故无法维护一份独立的局部状态, 只能依靠Hook
对象间接实现局部状态(有关更多Hook
实现细节, 在Hook原理
章节中详细讨论).
在v17.0.2
中共定义了14 种 Hook, 其中最常用的useState, useEffect, useLayoutEffect等
function App() {
// 状态相关: 初始状态
const [a, setA] = useState(1);
const changeState = () => {
setA(++a); // 进入reconciler流程
};
// 副作用相关: fiber.flags |= Update | Passive;
useEffect(() => {
console.log(`useEffect`);
}, []);
// 副作用相关: fiber.flags |= Update;
useLayoutEffect(() => {
console.log(`useLayoutEffect`);
}, []);
// 返回下级ReactElement对象
return <button onClick={changeState}>{a}</button>;
}
- 状态相关:
fiber树构造
阶段.useState
在fiber树构造
阶段(renderRootSync[Concurrent]
)执行, 可以修改Hook.memoizedState
.
- 副作用相关:
fiber树渲染
阶段.
这里有 2 个细节:
useEffect(function(){}, [])
中的函数是异步执行, 因为它经过了调度中心(具体实现可以回顾调度原理).useLayoutEffect
和Class组件
中的componentDidMount,componentDidUpdate
从调用时机上来讲是等价的, 因为他们都在commitRoot->commitLayoutEffects
函数中被调用.- 误区: 虽然官网文档推荐尽可能使用标准的
useEffect
以避免阻塞视觉更新 , 所以很多开发者使用useEffect
来代替componentDidMount,componentDidUpdate
是不准确的, 如果完全类比,useLayoutEffect
比useEffect
更符合componentDidMount,componentDidUpdate
的定义.
- 误区: 虽然官网文档推荐尽可能使用标准的
为了验证上述结论, 可以查看codesandbox 中的例子.
本节从fiber
视角出发, 总结了fiber
节点中可以影响最终渲染结果的 2 类属性(状态
和副作用
).并且归纳了class
和function
组件中, 直接或间接更改fiber
属性的常用方式. 最后从fiber树构造和渲染
的角度对class的生命周期函数
与function的Hooks函数
进行了比较.