• 【React源码】(十二)Hook源码分析 状态与副作用


    状态与副作用

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

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

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

    2. fiber节点的副作用: 在commitRoot阶段, 如果fiber被标记有副作用, 则副作用相关函数会被(同步/异步)调用.

     
    
    1. export type Fiber = {|
    2. // 1. fiber节点自身状态相关
    3. pendingProps: any,
    4. memoizedProps: any,
    5. updateQueue: mixed,
    6. memoizedState: any,
    7. // 2. fiber节点副作用(Effect)相关
    8. flags: Flags,
    9. subtreeFlags: Flags, // v17.0.2未启用
    10. deletions: Array<Fiber> | null, // v17.0.2未启用
    11. nextEffect: Fiber | null,
    12. firstEffect: Fiber | null,
    13. lastEffect: Fiber | null,
    14. |};

    状态

    状态相关有 4 个属性:

    1. fiber.pendingProps: 输入属性, 从ReactElement对象传入的 props. 它和fiber.memoizedProps比较可以得出属性是否变动.
    2. fiber.memoizedProps: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps, 生成子节点之后会把pendingProps赋值给memoizedProps用于下一次比较.pendingPropsmemoizedProps比较可以得出属性是否变动.
    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 组件

     
    
    1. class App extends React.Component {
    2. constructor() {
    3. this.state = {
    4. // 初始状态
    5. a: 1,
    6. };
    7. }
    8. changeState = () => {
    9. this.setState({ a: ++this.state.a }); // 进入reconciler流程
    10. };
    11. // 生命周期函数: 状态相关
    12. static getDerivedStateFromProps(nextProps, prevState) {
    13. console.log('getDerivedStateFromProps');
    14. return prevState;
    15. }
    16. // 生命周期函数: 状态相关
    17. shouldComponentUpdate(newProps, newState, nextContext) {
    18. console.log('shouldComponentUpdate');
    19. return true;
    20. }
    21. // 生命周期函数: 副作用相关 fiber.flags |= Update
    22. componentDidMount() {
    23. console.log('componentDidMount');
    24. }
    25. // 生命周期函数: 副作用相关 fiber.flags |= Snapshot
    26. getSnapshotBeforeUpdate(prevProps, prevState) {
    27. console.log('getSnapshotBeforeUpdate');
    28. }
    29. // 生命周期函数: 副作用相关 fiber.flags |= Update
    30. componentDidUpdate() {
    31. console.log('componentDidUpdate');
    32. }
    33. render() {
    34. // 返回下级ReactElement对象
    35. return <button onClick={this.changeState}>{this.state.a}button>;
    36. }
    37. }
    1. 状态相关: fiber树构造阶段.

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

      1. 生命周期: getSnapshotBeforeUpdatefiber树渲染阶段(commitRoot->commitBeforeMutationEffects->commitBeforeMutationEffectOnFiber)执行(链接).
      2. 生命周期: componentDidMountfiber树渲染阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber)执行(链接).
      3. 生命周期: componentDidUpdatefiber树渲染阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber)执行(链接).

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

    function 组件

    注: function组件class组件最大的不同是: class组件会实例化一个instance所以拥有独立的局部状态; 而function组件不会实例化, 它只是被直接调用, 故无法维护一份独立的局部状态, 只能依靠Hook对象间接实现局部状态(有关更多Hook实现细节, 在Hook原理章节中详细讨论).

    v17.0.2中共定义了14 种 Hook, 其中最常用的useState, useEffect, useLayoutEffect等

     
    
    1. function App() {
    2. // 状态相关: 初始状态
    3. const [a, setA] = useState(1);
    4. const changeState = () => {
    5. setA(++a); // 进入reconciler流程
    6. };
    7. // 副作用相关: fiber.flags |= Update | Passive;
    8. useEffect(() => {
    9. console.log(`useEffect`);
    10. }, []);
    11. // 副作用相关: fiber.flags |= Update;
    12. useLayoutEffect(() => {
    13. console.log(`useLayoutEffect`);
    14. }, []);
    15. // 返回下级ReactElement对象
    16. return <button onClick={changeState}>{a}button>;
    17. }
    1. 状态相关: fiber树构造阶段.
      1. useStatefiber树构造阶段(renderRootSync[Concurrent])执行, 可以修改Hook.memoizedState.
    2. 副作用相关: fiber树渲染阶段.
      1. useEffectfiber树渲染阶段(commitRoot->commitBeforeMutationEffects->commitBeforeMutationEffectOnFiber)执行(注意是异步执行, 链接).
      2. useLayoutEffectfiber树渲染阶段(commitRoot->commitLayoutEffects->commitLayoutEffectOnFiber->commitHookEffectListMount)执行(同步执行, 链接).

    细节与误区

    这里有 2 个细节:

    1. useEffect(function(){}, [])中的函数是异步执行, 因为它经过了调度中心(具体实现可以回顾调度原理).
    2. useLayoutEffectClass组件中的componentDidMount,componentDidUpdate从调用时机上来讲是等价的, 因为他们都在commitRoot->commitLayoutEffects函数中被调用.
      • 误区: 虽然官网文档推荐尽可能使用标准的 useEffect 以避免阻塞视觉更新 , 所以很多开发者使用useEffect来代替componentDidMount,componentDidUpdate是不准确的, 如果完全类比, useLayoutEffectuseEffect更符合componentDidMount,componentDidUpdate的定义.

    为了验证上述结论, 可以查看codesandbox 中的例子.

    总结

    本节从fiber视角出发, 总结了fiber节点中可以影响最终渲染结果的 2 类属性(状态副作用).并且归纳了classfunction组件中, 直接或间接更改fiber属性的常用方式. 最后从fiber树构造和渲染的角度对class的生命周期函数function的Hooks函数进行了比较.

  • 相关阅读:
    一个全响应式的企业级物联网平台,开源了
    将less、scss编译成css样式的方法
    实用Pycharm插件
    给一个喝酒青年的公开状
    使用chat-GPT接口提取合同中关键信息
    【Python】文件操作
    TCP三次握手和四次挥手详解
    基于ssm的图书管理系统的设计与实现
    ADAU1860调试心得(3)接口说明以及硬件搭建步骤
    【数据结构】动态规划:如何通过最优子结构,完成复杂问题求解
  • 原文地址:https://blog.csdn.net/weixin_44828588/article/details/126525439