React Hooks 的执行顺序

在 React 函数式组件中,当父组件包含多个子组件时,Hooks 的执行顺序遵循特定的规则。理解这个顺序对于调试和优化组件性能至关重要。

核心原则是:

  1. 渲染阶段 (Render Phase) :组件函数(包括父组件和所有子组件)会从上到下(父到子)依次执行。在这个阶段,useStateuseRefuseMemouseCallback 等 Hooks 会被处理。
  2. DOM 更新阶段 (DOM Mutation Phase) :React 会根据渲染阶段的输出更新 DOM。
  3. useLayoutEffect 阶段 :所有 useLayoutEffect 回调函数会同步执行,发生在 DOM 更新之后,浏览器绘制之前。它的执行顺序也是从父到子。
  4. 浏览器绘制阶段 (Paint Phase) :浏览器完成屏幕绘制。
  5. useEffect 阶段 (Commit Phase) :所有 useEffect 的清理函数(如果存在)会先执行(从子到父),然后 useEffect 回调函数会异步执行(从子到父)。

下面通过 5 个详细的代码示例来讲解各种情况。


示例 1: 基本父子组件的 Hooks 执行顺序

这个例子展示了在首次渲染和状态更新时,useStateuseLayoutEffectuseEffect 在父子组件中的基本执行顺序。

jsx 复制代码
import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom/client';

// 子组件
function ChildComponent({ id }) {
  const [count, setCount] = useState(0);

  console.log(`  Child ${id}: Rendered (useState initialized/updated)`);

  useLayoutEffect(() => {
    console.log(`  Child ${id}: useLayoutEffect executed`);
    return () => {
      console.log(`  Child ${id}: useLayoutEffect cleanup`);
    };
  }, [count]); // 依赖 count,每次 count 变化时重新执行

  useEffect(() => {
    console.log(`  Child ${id}: useEffect executed`);
    return () => {
      console.log(`  Child ${id}: useEffect cleanup`);
    };
  }, [count]); // 依赖 count,每次 count 变化时重新执行

  const handleClick = () => {
    setCount(prev => prev + 1);
  };

  return (
    <div style={{ border: '1px solid blue', margin: '10px', padding: '10px' }}>
      <h4>Child Component {id}</h4>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Child {id} Count</button>
    </div>
  );
}

// 父组件
function ParentComponent() {
  const [parentCount, setParentCount] = useState(0);

  console.log('Parent: Rendered (useState initialized/updated)');

  useLayoutEffect(() => {
    console.log('Parent: useLayoutEffect executed');
    return () => {
      console.log('Parent: useLayoutEffect cleanup');
    };
  }, [parentCount]);

  useEffect(() => {
    console.log('Parent: useEffect executed');
    return () => {
      console.log('Parent: useEffect cleanup');
    };
  }, [parentCount]);

  const handleParentClick = () => {
    setParentCount(prev => prev + 1);
  };

  return (
    <div style={{ border: '2px solid green', padding: '20px' }}>
      <h2>Parent Component</h2>
      <p>Parent Count: {parentCount}</p>
      <button onClick={handleParentClick}>Increment Parent Count</button>
      <ChildComponent id="A" />
    </div>
  );
}

const App = () => <ParentComponent />;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// 观察控制台输出

控制台输出分析:

首次渲染:

yaml 复制代码
Parent: Rendered (useState initialized/updated)
  Child A: Rendered (useState initialized/updated)
  Child A: useLayoutEffect executed
Parent: useLayoutEffect executed
  Child A: useEffect executed
Parent: useEffect executed
  • 渲染阶段 (Render Phase) :父组件函数先执行,然后子组件函数执行。
  • useLayoutEffect 阶段 :子组件的 useLayoutEffect 先执行,然后父组件的 useLayoutEffect 执行(从子到父)。
  • useEffect 阶段 :子组件的 useEffect 先执行,然后父组件的 useEffect 执行(从子到父)。

点击 "Increment Parent Count" 按钮(父组件状态更新):

yaml 复制代码
Parent: useLayoutEffect cleanup // 父组件的 useLayoutEffect 清理函数先执行
Parent: Rendered (useState initialized/updated)
  Child A: Rendered (useState initialized/updated) // 子组件因为父组件重新渲染而重新渲染
  Child A: useLayoutEffect cleanup // 子组件的 useLayoutEffect 清理函数(如果依赖变化)
  Child A: useLayoutEffect executed
Parent: useLayoutEffect executed
Parent: useEffect cleanup // 父组件的 useEffect 清理函数先执行
  Child A: useEffect cleanup // 子组件的 useEffect 清理函数(如果依赖变化)
  Child A: useEffect executed
Parent: useEffect executed
  • 清理函数执行 :在新的渲染周期开始前,上一次渲染的 useLayoutEffectuseEffect 的清理函数会执行。useLayoutEffect 的清理发生在新的 useLayoutEffect 执行之前,useEffect 的清理发生在新的 useEffect 执行之前。
  • 渲染顺序:依然是父组件先渲染,然后子组件渲染。
  • Effects 顺序useLayoutEffect 依然是子到父,useEffect 依然是子到父。

示例 2: 父组件包含多个相同类型的子组件

这个例子展示了当父组件渲染多个相同类型的子组件时,Hooks 的执行顺序。

jsx 复制代码
import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom/client';

// 子组件
function MultiChildComponent({ id }) {
  const [count, setCount] = useState(0);

  console.log(`  Child ${id}: Rendered`);

  useLayoutEffect(() => {
    console.log(`  Child ${id}: useLayoutEffect executed`);
    return () => {
      console.log(`  Child ${id}: useLayoutEffect cleanup`);
    };
  }, [count]);

  useEffect(() => {
    console.log(`  Child ${id}: useEffect executed`);
    return () => {
      console.log(`  Child ${id}: useEffect cleanup`);
    };
  }, [count]);

  const handleClick = () => {
    setCount(prev => prev + 1);
  };

  return (
    <div style={{ border: '1px solid blue', margin: '10px', padding: '10px' }}>
      <h4>Child Component {id}</h4>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment Child {id} Count</button>
    </div>
  );
}

// 父组件
function ParentWithMultipleChildren() {
  const [parentToggle, setParentToggle] = useState(false);

  console.log('Parent: Rendered');

  useLayoutEffect(() => {
    console.log('Parent: useLayoutEffect executed');
    return () => {
      console.log('Parent: useLayoutEffect cleanup');
    };
  }, [parentToggle]);

  useEffect(() => {
    console.log('Parent: useEffect executed');
    return () => {
      console.log('Parent: useEffect cleanup');
    };
  }, [parentToggle]);

  const handleToggle = () => {
    setParentToggle(prev => !prev);
  };

  return (
    <div style={{ border: '2px solid green', padding: '20px' }}>
      <h2>Parent With Multiple Children</h2>
      <button onClick={handleToggle}>Toggle Parent State</button>
      <MultiChildComponent id="1" />
      <MultiChildComponent id="2" />
      <MultiChildComponent id="3" />
    </div>
  );
}

const App = () => <ParentWithMultipleChildren />;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// 观察控制台输出

控制台输出分析:

首次渲染:

yaml 复制代码
Parent: Rendered
  Child 1: Rendered
  Child 2: Rendered
  Child 3: Rendered
  Child 1: useLayoutEffect executed
  Child 2: useLayoutEffect executed
  Child 3: useLayoutEffect executed
Parent: useLayoutEffect executed
  Child 1: useEffect executed
  Child 2: useEffect executed
  Child 3: useEffect executed
Parent: useEffect executed
  • 渲染阶段:父组件渲染,然后按照 JSX 中出现的顺序,子组件 1、2、3 依次渲染。
  • useLayoutEffect 阶段 :子组件 1、2、3 的 useLayoutEffect 依次执行,然后父组件的 useLayoutEffect 执行。
  • useEffect 阶段 :子组件 1、2、3 的 useEffect 依次执行,然后父组件的 useEffect 执行。

点击 "Toggle Parent State" 按钮(父组件状态更新):

yaml 复制代码
Parent: useLayoutEffect cleanup
Parent: Rendered
  Child 1: Rendered
  Child 2: Rendered
  Child 3: Rendered
  Child 1: useLayoutEffect cleanup // 如果子组件的依赖变化
  Child 2: useLayoutEffect cleanup
  Child 3: useLayoutEffect cleanup
  Child 1: useLayoutEffect executed
  Child 2: useLayoutEffect executed
  Child 3: useLayoutEffect executed
Parent: useLayoutEffect executed
Parent: useEffect cleanup
  Child 1: useEffect cleanup // 如果子组件的依赖变化
  Child 2: useEffect cleanup
  Child 3: useEffect cleanup
  Child 1: useEffect executed
  Child 2: useEffect executed
  Child 3: useEffect executed
Parent: useEffect executed
  • 清理函数和执行顺序与首次渲染类似,但会先执行清理函数。

示例 3: 父组件包含多个不同类型的子组件

这个例子与示例 2 类似,但使用不同名称的子组件,以强调顺序与组件类型无关,只与它们在 JSX 中的位置有关。

jsx 复制代码
import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom/client';

// 子组件 A
function ChildA() {
  const [count, setCount] = useState(0);
  console.log('  Child A: Rendered');
  useLayoutEffect(() => {
    console.log('  Child A: useLayoutEffect executed');
    return () => console.log('  Child A: useLayoutEffect cleanup');
  }, [count]);
  useEffect(() => {
    console.log('  Child A: useEffect executed');
    return () => console.log('  Child A: useEffect cleanup');
  }, [count]);
  return (
    <div style={{ border: '1px solid red', margin: '10px', padding: '10px' }}>
      <h5>Child A</h5>
      <button onClick={() => setCount(c => c + 1)}>Increment A</button>
    </div>
  );
}

// 子组件 B
function ChildB() {
  const [count, setCount] = useState(0);
  console.log('  Child B: Rendered');
  useLayoutEffect(() => {
    console.log('  Child B: useLayoutEffect executed');
    return () => console.log('  Child B: useLayoutEffect cleanup');
  }, [count]);
  useEffect(() => {
      console.log('  Child B: useEffect executed');
      return () => console.log('  Child B: useEffect cleanup');
  }, [count]);
  return (
    <div style={{ border: '1px solid orange', margin: '10px', padding: '10px' }}>
      <h5>Child B</h5>
      <button onClick={() => setCount(c => c + 1)}>Increment B</button>
    </div>
  );
}

// 父组件
function ParentWithDifferentChildren() {
  const [parentState, setParentState] = useState(0);
  console.log('Parent: Rendered');
  useLayoutEffect(() => {
    console.log('Parent: useLayoutEffect executed');
    return () => console.log('Parent: useLayoutEffect cleanup');
  }, [parentState]);
  useEffect(() => {
    console.log('Parent: useEffect executed');
    return () => console.log('Parent: useEffect cleanup');
  }, [parentState]);

  return (
    <div style={{ border: '2px solid purple', padding: '20px' }}>
      <h2>Parent With Different Children</h2>
      <button onClick={() => setParentState(s => s + 1)}>Update Parent</button>
      <ChildA />
      <ChildB />
    </div>
  );
}

const App = () => <ParentWithDifferentChildren />;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// 观察控制台输出

控制台输出分析:

首次渲染:

yaml 复制代码
Parent: Rendered
  Child A: Rendered
  Child B: Rendered
  Child A: useLayoutEffect executed
  Child B: useLayoutEffect executed
Parent: useLayoutEffect executed
  Child A: useEffect executed
  Child B: useEffect executed
Parent: useEffect executed
  • 与示例 2 相同,渲染顺序和 Hooks 执行顺序严格按照 JSX 中组件的声明顺序。

示例 4: 深度嵌套的组件

这个例子展示了当组件树有更深的层级时,Hooks 的执行顺序。

jsx 复制代码
import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom/client';

// 最内层子组件
function GrandchildComponent() {
  const [count, setCount] = useState(0);
  console.log('    Grandchild: Rendered');
  useLayoutEffect(() => {
    console.log('    Grandchild: useLayoutEffect executed');
    return () => console.log('    Grandchild: useLayoutEffect cleanup');
  }, [count]);
  useEffect(() => {
    console.log('    Grandchild: useEffect executed');
    return () => console.log('    Grandchild: useEffect cleanup');
  }, [count]);
  return (
    <div style={{ border: '1px solid yellowgreen', margin: '10px', padding: '10px' }}>
      <h6>Grandchild Component</h6>
      <button onClick={() => setCount(c => c + 1)}>Increment Grandchild</button>
    </div>
  );
}

// 中间层子组件
function MiddleChildComponent() {
  const [count, setCount] = useState(0);
  console.log('  MiddleChild: Rendered');
  useLayoutEffect(() => {
    console.log('  MiddleChild: useLayoutEffect executed');
    return () => console.log('  MiddleChild: useLayoutEffect cleanup');
  }, [count]);
  useEffect(() => {
    console.log('  MiddleChild: useEffect executed');
    return () => console.log('  MiddleChild: useEffect cleanup');
  }, [count]);
  return (
    <div style={{ border: '1px solid orange', margin: '10px', padding: '10px' }}>
      <h5>Middle Child Component</h5>
      <button onClick={() => setCount(c => c + 1)}>Increment Middle</button>
      <GrandchildComponent />
    </div>
  );
}

// 父组件
function DeepParentComponent() {
  const [parentCount, setParentCount] = useState(0);
  console.log('Parent: Rendered');
  useLayoutEffect(() => {
    console.log('Parent: useLayoutEffect executed');
    return () => console.log('Parent: useLayoutEffect cleanup');
  }, [parentCount]);
  useEffect(() => {
    console.log('Parent: useEffect executed');
    return () => console.log('Parent: useEffect cleanup');
  }, [parentCount]);
  return (
    <div style={{ border: '2px solid purple', padding: '20px' }}>
      <h2>Deep Parent Component</h2>
      <button onClick={() => setParentCount(c => c + 1)}>Update Parent</button>
      <MiddleChildComponent />
    </div>
  );
}

const App = () => <DeepParentComponent />;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// 观察控制台输出

控制台输出分析:

首次渲染:

yaml 复制代码
Parent: Rendered
  MiddleChild: Rendered
    Grandchild: Rendered
    Grandchild: useLayoutEffect executed
  MiddleChild: useLayoutEffect executed
Parent: useLayoutEffect executed
    Grandchild: useEffect executed
  MiddleChild: useEffect executed
Parent: useEffect executed
  • 渲染阶段:从顶层父组件开始,依次向下渲染,直到最深层的子组件。
  • useLayoutEffect 阶段 :从最深层的子组件开始,依次向上执行 useLayoutEffect
  • useEffect 阶段 :从最深层的子组件开始,依次向上执行 useEffect

这个"先渲染再执行效果"的顺序(渲染从上到下,效果从下到上)是 React Hooks 的一个重要特性。


示例 5: useEffect 清理函数和依赖变化

这个例子更侧重于 useEffect 的生命周期,但它在父子组件的上下文中仍然适用,展示了当依赖项变化时,清理函数如何执行以及新的效果如何运行。

jsx 复制代码
import React, { useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';

// 子组件
function EffectChild({ parentData }) {
  const [childCount, setChildCount] = useState(0);

  console.log(`  EffectChild: Rendered with parentData: ${parentData}`);

  useEffect(() => {
    console.log(`  EffectChild: useEffect [childCount] executed. Current childCount: ${childCount}`);
    return () => {
      console.log(`  EffectChild: useEffect [childCount] cleanup. Cleaning up childCount: ${childCount}`);
    };
  }, [childCount]); // 依赖 childCount

  useEffect(() => {
    console.log(`  EffectChild: useEffect [parentData] executed. Current parentData: ${parentData}`);
    return () => {
      console.log(`  EffectChild: useEffect [parentData] cleanup. Cleaning up parentData: ${parentData}`);
    };
  }, [parentData]); // 依赖 parentData

  const incrementChild = () => setChildCount(c => c + 1);

  return (
    <div style={{ border: '1px solid blue', margin: '10px', padding: '10px' }}>
      <h4>Effect Child</h4>
      <p>Child Count: {childCount}</p>
      <p>Parent Data received: {parentData}</p>
      <button onClick={incrementChild}>Increment Child Count</button>
    </div>
  );
}

// 父组件
function EffectParent() {
  const [parentData, setParentData] = useState(0);

  console.log('Parent: Rendered');

  useEffect(() => {
    console.log(`Parent: useEffect [parentData] executed. Current parentData: ${parentData}`);
    return () => {
      console.log(`Parent: useEffect [parentData] cleanup. Cleaning up parentData: ${parentData}`);
    };
  }, [parentData]); // 依赖 parentData

  const incrementParent = () => setParentData(d => d + 1);

  return (
    <div style={{ border: '2px solid green', padding: '20px' }}>
      <h2>Effect Parent</h2>
      <p>Parent Data: {parentData}</p>
      <button onClick={incrementParent}>Increment Parent Data</button>
      <EffectChild parentData={parentData} />
    </div>
  );
}

const App = () => <EffectParent />;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

观察控制台输出

控制台输出分析:

首次渲染:

yaml 复制代码
Parent: Rendered  
EffectChild: Rendered with parentData: 0  
EffectChild: useEffect [childCount] executed. Current childCount: 0  
EffectChild: useEffect [parentData] executed. Current parentData: 0  
Parent: useEffect [parentData] executed. Current parentData: 0

点击 "Increment Parent Data" 按钮(父组件状态更新):

yaml 复制代码
Parent: Rendered  
EffectChild: Rendered with parentData: 1 // 子组件重新渲染,接收新的 parentData  
Parent: useEffect [parentData] cleanup. Cleaning up parentData: 0 // 父组件旧 effect 清理  
EffectChild: useEffect [parentData] cleanup. Cleaning up parentData: 0 // 子组件依赖 parentData 的旧 effect 清理  
EffectChild: useEffect [childCount] executed. Current childCount: 0 // 子组件依赖 childCount 的 effect (childCount 未变,但父组件导致子组件重新渲染,所以这个 effect 也会重新运行,除非它没有依赖或依赖未变,这里因为它依赖 childCount 且 childCount 未变,所以它不会清理和重新执行,但如果父组件导致子组件重新挂载,则会重新执行。这里只是重新渲染,所以这个 effect 不会重新运行,除非它的依赖变化。我的 log 有点误导,应该说 `useEffect` 只有在依赖变化时才重新执行 effect 和清理。这里 `childCount` 没变,所以 `[childCount]` 的 effect 不会重新运行。)  
EffectChild: useEffect [parentData] executed. Current parentData: 1 // 子组件依赖 parentData 的新 effect 执行  
Parent: useEffect [parentData] executed. Current parentData: 1 // 父组件新 effect 执行
  • 清理和执行顺序 :当依赖项发生变化时,旧的 useEffect 清理函数会在新的渲染周期中,在新的 useEffect 回调函数执行之前运行。清理函数按照从子到父的顺序执行,新的 useEffect 回调函数也按照从子到父的顺序执行。

总结 Hooks 执行顺序:

  1. 渲染 (Render)
    • 父组件函数执行。
    • 子组件函数按 JSX 顺序依次执行。
    • useStateuseRefuseMemouseCallback 在此阶段处理。
  2. DOM 更新:React 更新浏览器 DOM。
  3. useLayoutEffect (同步)
    • 清理 :如果存在,前一次渲染的 useLayoutEffect 清理函数按子到父的顺序执行。
    • 执行 :当前渲染的 useLayoutEffect 回调函数按子到父的顺序执行。
  4. 浏览器绘制 (Paint):浏览器将更新后的 DOM 绘制到屏幕上。
  5. useEffect (异步)
    • 清理 :如果存在,前一次渲染的 useEffect 清理函数按子到父的顺序执行(在浏览器绘制之后,但新的 useEffect 之前)。
    • 执行 :当前渲染的 useEffect 回调函数按子到父的顺序执行(在浏览器绘制之后)。

理解这个顺序对于编写高效、无副作用的 React 组件至关重要,尤其是在处理 DOM 测量、订阅或清理资源时。

相关推荐
Nan_Shu_61419 分钟前
学习: Threejs (2)
前端·javascript·学习
G_G#27 分钟前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界42 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路1 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug1 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu121381 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中1 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路1 小时前
GDAL 实现矢量合并
前端
hxjhnct1 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子2 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端