在 React 函数式组件中,当父组件包含多个子组件时,Hooks 的执行顺序遵循特定的规则。理解这个顺序对于调试和优化组件性能至关重要。
核心原则是:
- 渲染阶段 (Render Phase) :组件函数(包括父组件和所有子组件)会从上到下(父到子)依次执行。在这个阶段,
useState
、useRef
、useMemo
、useCallback
等 Hooks 会被处理。 - DOM 更新阶段 (DOM Mutation Phase) :React 会根据渲染阶段的输出更新 DOM。
useLayoutEffect
阶段 :所有useLayoutEffect
回调函数会同步执行,发生在 DOM 更新之后,浏览器绘制之前。它的执行顺序也是从父到子。- 浏览器绘制阶段 (Paint Phase) :浏览器完成屏幕绘制。
useEffect
阶段 (Commit Phase) :所有useEffect
的清理函数(如果存在)会先执行(从子到父),然后useEffect
回调函数会异步执行(从子到父)。
下面通过 5 个详细的代码示例来讲解各种情况。
示例 1: 基本父子组件的 Hooks 执行顺序
这个例子展示了在首次渲染和状态更新时,useState
、useLayoutEffect
和 useEffect
在父子组件中的基本执行顺序。
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
- 清理函数执行 :在新的渲染周期开始前,上一次渲染的
useLayoutEffect
和useEffect
的清理函数会执行。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 执行顺序:
- 渲染 (Render) :
- 父组件函数执行。
- 子组件函数按 JSX 顺序依次执行。
useState
、useRef
、useMemo
、useCallback
在此阶段处理。
- DOM 更新:React 更新浏览器 DOM。
useLayoutEffect
(同步) :- 清理 :如果存在,前一次渲染的
useLayoutEffect
清理函数按子到父的顺序执行。 - 执行 :当前渲染的
useLayoutEffect
回调函数按子到父的顺序执行。
- 清理 :如果存在,前一次渲染的
- 浏览器绘制 (Paint):浏览器将更新后的 DOM 绘制到屏幕上。
useEffect
(异步) :- 清理 :如果存在,前一次渲染的
useEffect
清理函数按子到父的顺序执行(在浏览器绘制之后,但新的useEffect
之前)。 - 执行 :当前渲染的
useEffect
回调函数按子到父的顺序执行(在浏览器绘制之后)。
- 清理 :如果存在,前一次渲染的
理解这个顺序对于编写高效、无副作用的 React 组件至关重要,尤其是在处理 DOM 测量、订阅或清理资源时。