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 测量、订阅或清理资源时。

相关推荐
撰卢2 分钟前
Filter快速入门 Java web
java·前端·hive·spring boot
ai小鬼头12 分钟前
创业心态崩了?熊哥教你用缺德哲学活得更爽
前端·后端·算法
拾光拾趣录26 分钟前
算法 | 下一个更大的排列
前端·算法
小屁孩大帅-杨一凡1 小时前
如何使用Python将HTML格式的文本转换为Markdown格式?
开发语言·前端·python·html
于慨1 小时前
uniapp云打包安卓
前端·uni-app
米粒宝的爸爸1 小时前
【uniapp】使用uviewplus来实现图片上传和图片预览功能
java·前端·uni-app
LaoZhangAI1 小时前
2025年虚拟信用卡订阅ChatGPT Plus完整教程(含WildCard停运后最新方案)
前端·后端
雪碧聊技术1 小时前
Uniapp 纯前端台球计分器开发指南:能否上架微信小程序 & 打包成APP?
前端·微信小程序·uni-app·台球计分器
清风细雨_林木木1 小时前
Vuex 的语法“...mapActions([‘login‘]) ”是用于在组件中映射 Vuex 的 actions 方法
前端·javascript·vue.js
会功夫的李白1 小时前
Uniapp之自定义图片预览
前端·javascript·uni-app·图片预览