图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

状态与副作用

在前文我们已经分析了fiber树从构造到渲染的关键过程. 本节我们站在fiber对象的视角, 考虑一个具体的fiber节点如何影响最终的渲染.

回顾fiber 数据结构, 并结合前文fiber树构造系列的解读, 我们注意到fiber众多属性中, 有 2 类属性十分关键:

  1. fiber节点的自身状态: 在renderRootSync[Concurrent]阶段, 为子节点提供确定的输入数据, 直接影响子节点的生成.

  2. 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 个属性:

  1. fiber.pendingProps: 输入属性, 从ReactElement对象传入的 props. 它和fiber.memoizedProps比较可以得出属性是否变动.
  2. fiber.memoizedProps: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps, 生成子节点之后会把pendingProps赋值给memoizedProps用于下一次比较.pendingProps和memoizedProps比较可以得出属性是否变动.
  3. fiber.updateQueue: 存储update更新对象的队列, 每一次发起更新, 都需要在该队列上创建一个update对象.
  4. fiber.memoizedState: 上一次生成子节点之后保持在内存中的局部状态.

它们的作用只局限于fiber树构造阶段, 直接影响子节点的生成.

副作用

与副作用相关有 4 个属性:

  1. fiber.flags: 标志位, 表明该fiber节点有副作用(在 v17.0.2 中共定义了28 种副作用).
  2. fiber.nextEffect: 单向链表, 指向下一个副作用 fiber节点.
  3. fiber.firstEffect: 单向链表, 指向第一个副作用 fiber 节点.
  4. fiber.lastEffect: 单向链表, 指向最后一个副作用 fiber 节点.

通过前文fiber树构造我们知道, 单个fiber节点的副作用队列最后都会上移到根节点上. 所以在commitRoot阶段中, react提供了 3 种处理副作用的方式(详见fiber 树渲染).

另外, 副作用的设计可以理解为对状态功能不足的补充.

  • 状态是一个静态的功能, 它只能为子节点提供数据源.
  • 而副作用是一个动态功能, 由于它的调用时机是在fiber树渲染阶段, 故它拥有更多的能力, 能轻松获取突变前快照, 突变后的DOM节点等. 甚至通过调用api发起新的一轮fiber树构造, 进而改变更多的状态, 引发更多的副作用.

外部 api

fiber对象的这 2 类属性, 可以影响到渲染结果, 但是fiber结构始终是一个内核中的结构, 对于外部来讲是无感知的, 对于调用方来讲, 甚至都无需知道fiber结构的存在. 所以正常只有通过暴露api来直接或间接的修改这 2 类属性.

从react包暴露出的api来归纳, 只有 2 类组件支持修改:

本节只讨论使用api的目的是修改fiber的状态和副作用, 进而可以改变整个渲染结果. 本节先介绍 api 与状态和副作用的联系, 有关api的具体实现会在class组件,Hook原理章节中详细分析.

class 组件

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>;
}
}
  1. 状态相关: fiber树构造阶段.

    1. 构造函数: constructor实例化时执行, 可以设置初始 state, 只执行一次.
    2. 生命周期: getDerivedStateFromProps在fiber树构造阶段(renderRootSync[Concurrent])执行, 可以修改 state(链接).
    3. 生命周期: shouldComponentUpdate在, fiber树构造阶段(renderRootSync[Concurrent])执行, 返回值决定是否执行 render(链接).
  2. 副作用相关: fiber树渲染阶段.

    1. 生命周期: getSnapshotBeforeUpdate在fiber树渲染阶段(commitRoot->commitBeforeMutationEffects->commitBeforeMutationEffectOnFiber)执行(链接).
    2. 生命周期: componentDidMount在fiber树渲染阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber)执行(链接).
    3. 生命周期: componentDidUpdate在fiber树渲染阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber)执行(链接).

可以看到, 官方api提供的class组件生命周期函数实际上也是围绕fiber树构造和fiber树渲染来提供的.

function 组件

注: 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>;
}
  1. 状态相关: fiber树构造阶段.
    1. useState在fiber树构造阶段(renderRootSync[Concurrent])执行, 可以修改Hook.memoizedState.
  2. 副作用相关: fiber树渲染阶段.
    1. useEffect在fiber树渲染阶段(commitRoot->commitBeforeMutationEffects->commitBeforeMutationEffectOnFiber)执行(注意是异步执行, 链接).
    2. useLayoutEffect在fiber树渲染阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber->commitHookEffectListMount)执行(同步执行, 链接).

细节与误区

这里有 2 个细节:

  1. useEffect(function(){}, [])中的函数是异步执行, 因为它经过了调度中心(具体实现可以回顾调度原理).
  2. 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函数进行了比较.