react生命周期及hooks等效实现
目录
- 一、React生命周期图例
- 1. 挂载/创建阶段(组件首次渲染)4步
- 2. 更新阶段(props/state变更)5步
- 3. 卸载阶段(组件从DOM移除)
- 4. 错误处理(React 16+新增)
- 二、Hooks 对生命周期的替代
- 1. 状态管理(替代 this.state)
- 2. 副作用管理(替代生命周期钩子)
- 3. 性能优化(替代 shouldComponentUpdate)
- 4. 其他常用数据保存 Hooks
- 三、hooks其他问题
- 1. useEffect 与 useLayoutEffect 的区别?
- 2. 如何避免 useEffect 无限循环?
- 3. constructor 和 getDerivedStateFromProps 的区别
- 4. useState和useReducer
- 5. 多个 useEffect 的执行顺序
- 6. 如何避免 useEffect 无限循环
- 7. useMemo和 React.memo
- 8. 自定义React hooks
- 9. setState和useState是同步还是异步?区别是什么?
一、React生命周期图例
函数 | 何时调用 | 其他 | 典型场景 | hooks等效实现 |
---|---|---|---|---|
constructor | 仅在组件实例化时执行一次 | 唯一可以直接修改state,很少使用 | 初始化内部状态 | 相当于useState/useReducer 初始化状态 |
getDerivedStateFromProps | 外部传入属性发生变化,例如:setState(),forceUpdate() | state需要从props初始化时使用,每次render都会调用。维护两者状态一致会增加复杂度,尽量不要使用 | 表单控件获取默认值 | useMemo、useCallback 、useEffect+useState |
shouldComponentUpdate | 属性变化时,除了forceUpdate() | 决定Virtual DOM是否要重绘 | 性能优化 | React.memo,用于浅比较 props 来决定是否重新渲染组件 |
render | 渲染UI | 描述UI,必须写的方法 | 函数组件本身就是渲染逻辑 | |
getSnapshotBeforeUpdate | 页面render之前调用,state已更新 | 获取render之前DOM状态 | 使用 useLayoutEffect 在 DOM 更新前读取值,并通过 useRef 存储快照 | |
componentDIdMount | UI渲染完成后调用 | 只执行一次 | 发起ajax等外部请求 | useEffect(…, []) |
componentDitUpdate | 每次UI更新时调用 | 页面需要根据props变化重新获取数据 | useEffect(…, [dependency]) | |
componentWillUnmount | 组件被移除时调用 | 资源释放 | useEffect 返回的清理函数return |
1. 挂载/创建阶段(组件首次渲染)4步
- 执行顺序:constructor → getDerivedStateFromProps → render → componentDidMount
2. 更新阶段(props/state变更)5步
- 触发条件:props 变化、state 变化、forceUpdate()
- 执行顺序:getDerivedStateFromProps → shouldComponentUpdate → render →
getSnapshotBeforeUpdate → componentDidUpdate
3. 卸载阶段(组件从DOM移除)
- componentWillUnmount:用于清理副作用(如定时器、订阅)
4. 错误处理(React 16+新增)
- componentDidCatch
二、Hooks 对生命周期的替代
1. 状态管理(替代 this.state)
- useState:基础状态管理。
const [count, setCount] = useState(0);
- useReducer:复杂状态逻辑(类似 Redux 的 reducer)
const [state, dispatch] = useReducer(reducer, initialState);
2. 副作用管理(替代生命周期钩子)
- useEffect:相当于 componentDidMount + componentDidUpdate + componentWillUnmount 的组合
// 依赖项为空数组:仅挂载时执行(类似 componentDidMount)
useEffect(() => {// 初始化逻辑return () => {// 组件卸载时执行:清理逻辑(类似 componentWillUnmount)};
}, []);// 无依赖项:每次渲染后执行(类似 componentDidMount + componentDidUpdate)
useEffect(() => {// 副作用逻辑
});// 特定依赖变化时执行
useEffect(() => {// 仅当 count 变化时执行(初始也执行一次?)
}, [count]);
3. 性能优化(替代 shouldComponentUpdate)
- React.memo:浅比较 props,阻止重复渲染(函数组件版 PureComponent)。
const MyComponent = React.memo((props) => { ... });
- useMemo:缓存计算结果,避免重复计算。
const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- useCallback:缓存函数引用,避免子组件不必要的更新。
const handleClick = useCallback(() => {doSomething(a);
}, [a]);
4. 其他常用数据保存 Hooks
- useRef:保存可变值(类似类组件的实例属性);获取 DOM 节点(替代 createRef)。
- useContext:跨层级传递数据(替代 Context.Consumer)。
三、hooks其他问题
1. useEffect 与 useLayoutEffect 的区别?
对比 | useEffect | useLayoutEffect |
---|---|---|
执行时机 | 异步执行,在浏览器渲染后触发(不阻塞页面绘制)。 | 同步执行,在 DOM 更新后、浏览器绘制前触发(可能阻塞渲染)。 |
适用场景 | 数据获取、订阅等不影响渲染的操作 | 需要读取 DOM 布局并立即更新的场景(如测量元素尺寸、滚动位置恢复)。 |
2. 如何避免 useEffect 无限循环?
- 检查依赖项:确保依赖项数组中不包含引用类型(如对象、函数),或使用 useCallback/useMemo 缓存。
- 过滤不必要的更新:在 useEffect 内部通过条件判断过滤重复操作。
- 使用 useRef 存储可变值:避免依赖项变化触发无限循环。
3. constructor 和 getDerivedStateFromProps 的区别
方法 | constructor | getDerivedStateFromProps |
---|---|---|
调用时机 | 组件实例化时调用(初始化阶段) | 每次渲染前调用(初始化 + 更新阶段) |
是否可以更新 state | 可以(通过 this.state = { … }) | 必须返回新 state 或 null(纯函数) |
是否可以访问 this | 可以(需先调用 super(props)) | 不可以(静态方法,无 this) |
典型用途 | 初始化 state、绑定事件处理函数 | 根据 props 动态更新 state(如受控组件) |
注意:不能在constructor里调用 setState(此时组件尚未挂载)。用this.state = { … }赋值
4. useState和useReducer
特性 | useState | useReducer |
---|---|---|
状态结构 | 基础类型(number、string、boolean)或对象 | 复杂对象或数组 |
更新逻辑 | 直接修改(setState(newValue)) | 通过 reducer 函数处理(纯函数) |
适用场景 | 状态逻辑简单,状态更新相互独立(如计数器、表单值) | 复杂状态逻辑,更新逻辑分散,需要撤销 / 重做、时间旅行调试(如多值联动、状态历史记录) |
组件间共享 | 需逐层传递 state 和 setState | 可结合 Context 全局共享 dispatch |
调试与测试 | 较简单 | 更易预测(纯函数),便于测试和时间旅行调试 |
多次调用 | 多次调用 setState 可能触发多次渲染(React 18 中自动批处理已优化)。 | 单个 dispatch 触发一次更新,更适合复杂状态变更。 |
useReducer使用
const initialState = {loading: false,data: null,error: null
};const reducer = (state, action) => {switch (action.type) {case 'FETCH_START':return { ...state, loading: true }; // 仅更新 loadingcase 'FETCH_SUCCESS':return { ...state, loading: false, data: action.payload }; // 更新 loading 和 datacase 'FETCH_ERROR':return { ...state, loading: false, error: action.error }; // 更新 loading 和 errordefault:return state;}
};const DataFetcher = () => {const [state, dispatch] = useReducer(reducer, initialState);return (<div><button onClick={() => dispatch({ type: 'FETCH_START', payload: { id: 1 } })}>start</button>{/* 其他操作... */}</div>);
};
5. 多个 useEffect 的执行顺序
useEffect(() => {console.log(1)return () => {console.log(2)};
}, []);useEffect(() => {console.log(3);return () => {console.log(4)}
});useEffect(() => {console.log(5)return () => {console.log(6)}
}, [count]);
以上代码挂载时、卸载时、count变化时的执行顺序是什么?
-
挂载时(首次渲染)
输出:1 → 3 → 5- 第一个 useEffect(依赖项为空数组 []):仅在挂载时执行。
- 第二个useEffect(无依赖项):在每次渲染后执行,包括首次渲染。
- 第三个 useEffect(依赖项为 count):在首次渲染时,由于count 存在初始值(假设为 0),也会执行。
-
卸载时
输出 6 → 4 → 2- 第三个 useEffect的return(依赖 count):最先注册,最后清理(6)
- 第二个 useEffect的return(无依赖):其次注册,其次清理(4)。
- 第一个 useEffect的return(空依赖):最后注册,最先清理(2)。
-
count 变化时
输出:6 → 5 → 4 → 3- 第三个 useEffect:
- 清理上一次的副作用(6)。
- 执行新的副作用(5)。
- 第二个 useEffect:
- 清理上一次的副作用(4)。
- 执行新的副作用(3)。
- 第三个 useEffect:
useEffect 的清理函数(return 返回的函数)遵循 “后进先出”(LIFO) 的栈结构规则,useEffect顺序执行,遇到return后压入栈中,所以第一个useEffect的return先入栈,第三个useEffect的return最后入栈;卸载时,第三个useEffect的return先出栈执行
6. 如何避免 useEffect 无限循环
- 无限循环的常见原因
当 useEffect 的依赖项包含引用类型(如对象、函数)时,如果每次渲染都生成新的引用,会导致 useEffect 不断触发 - 解决
-
① useCallback 缓存函数引用,仅在依赖项变化时才重新创建函数
const [count, setCount] = useState(0);// 仅当依赖项 [] 变化时才重新创建函数(这里永远不变) const fetchData = useCallback(() => {console.log('Fetching data...'); }, []); // ✅ 只创建一次函数useEffect(() => {fetchData(); }, [fetchData]); // ✅ 仅触发一次
-
② useMemo 缓存计算结果,避免重复计算复杂值
const [count, setCount] = useState(0);// 仅当 count 变化时才重新计算 expensiveValue const expensiveValue = useMemo(() => {return performExpensiveCalculation(count); }, [count]);useEffect(() => {console.log(expensiveValue); }, [expensiveValue]); // ✅ 仅在 count 变化时触发
-
③ useRef 存储可变值,修改 ref 不会导致组件重新渲染
const [count, setCount] = useState(0); const prevCountRef = useRef(0);useEffect(() => {prevCountRef.current = count; // 保存当前值 }, [count]);const prevCount = prevCountRef.current; // 获取上一次的值
-
7. useMemo和 React.memo
关于 useMemo 和 React.memo 的区别,这是 React 性能优化中的常见问题。虽然名字相似,但它们的功能和应用场景完全不同
特性 | useMemo | React.memo |
---|---|---|
类型 | Hook(函数组件内部使用) | 高阶组件(包裹函数组件) |
作用对象 | 值(计算结果、函数等) | 组件本身 |
触发条件 | 依赖项变化时重新计算 | props 浅比较不相等时重新渲染 |
应用场景 | 避免重复计算复杂值、缓存函数引用 | 避免组件因相同 props 重复渲染 |
默认行为 | 不自动执行,需手动包裹 自动执行 props 浅比较 |
- React.memo 的浅比较限制
- 仅浅比较 props:如果 props 包含引用类型(如对象、数组),需确保引用不变。
- 函数 props:需配合 useCallback 或 useMemo 使用,避免父组件每次渲染时创建新函数。
8. 自定义React hooks
- 定义:自定义 Hooks 是一个函数,其名称以 use 开头,内部可以调用其他 Hooks。
- 核心价值:复用有状态的逻辑(如订阅、动画、表单验证),同时不污染组件结构。
- 与组件的区别:组件返回 UI,而自定义 Hooks 返回数据或副作用。
自定义 Hook 示例:
import { useState, useEffect } from 'react';
// 使用 localStorage 持久化状态
function useLocalStorage(key, initialValue) {// 1. 内部使用 useState 管理状态const [value, setValue] = useState(() => {try {const storedValue = localStorage.getItem(key);return storedValue ? JSON.parse(storedValue) : initialValue;} catch (error) {return initialValue;}});// 2. 使用 useEffect 添加副作用useEffect(() => {localStorage.setItem(key, JSON.stringify(value));}, [key, value]); // 3. 指定依赖项,控制副作用触发时机// 4. 返回状态和修改状态的函数(类似 useState),也可以返回对象或函数return [value, setValue];
}
9. setState和useState是同步还是异步?区别是什么?
特性 | class 组件的 setState | 函数组件的 useState |
---|---|---|
执行时机 | 批量异步(多数情况下) | 批量异步(多数情况下) |
强制同步场景 | 1. 原生事件回调 2. setTimeout/setInterval | 1. 原生事件回调 2. setTimeout/setInterval |
状态更新方式 | 合并对象(this.state 是累积的) | 完全替换(需手动合并 setState(prev => ({…prev, key}))) |
多次调用行为 | 合并为一次更新(对象浅合并) | 按顺序执行(函数式更新) |
-
异步机制:React 为了优化性能,会批量处理多个状态更新,因此在同一个事件循环中多次调用 setState 或 useState 可能不会立即生效。
// Class 组件 handleClick() {this.setState({ count: this.state.count + 1 });console.log(this.state.count); // 输出旧值(异步更新) }// 函数组件 const handleClick = () => {setCount(count + 1);console.log(count); // 输出旧值(异步更新) };
-
同步场景
-
原生事件回调
// Class 组件 componentDidMount() {document.getElementById('btn').addEventListener('click', () => {this.setState({ count: this.state.count + 1 });console.log(this.state.count); // 输出新值(同步更新)}); }// 函数组件 useEffect(() => {document.getElementById('btn').addEventListener('click', () => {setCount(count + 1);console.log(count); // 输出新值(同步更新)}); }, []);
-
setTimeout/setInterval 回调
场景 Class 组件的 setState 函数组件的 useState 状态更新时机 立即更新(同步) 不立即更新(异步) 闭包捕获问题 无(通过 this.state 获取最新值) 有(闭包捕获初始值)可通过 useRef 存储可变值,使用最新状态 多次调用合并 合并为一次更新(对象浅合并) 按顺序执行(函数式更新) 渲染触发次数 一次(批量处理) 一次(React 18 自动批处理) Class 组件的setState在setTimeout中:
class Counter extends React.Component {state = { count: 0 };handleClick = () => {setTimeout(() => {this.setState({ count: this.state.count + 1 });console.log(this.state.count); // 输出 1(状态立即更新)this.setState({ count: this.state.count + 1 });console.log(this.state.count); // 输出 2(状态立即更新)}, 1000);};render() {console.log('Rendering...'); // 仅触发一次渲染return <button onClick={this.handleClick}>{this.state.count}</button>;} }
函数组件的useState在setTimeout中:
const Counter = () => {const [count, setCount] = useState(0);const handleClick = () => {setTimeout(() => {setCount(prev => prev + 1); // 使用函数式更新,获取最新状态console.log(count); // 输出 0(状态未立即更新,但更新逻辑正确)setCount(prev => prev + 1); // 基于最新状态更新,最终 count 变为 2console.log(count); // 输出 0}, 1000);};return <button onClick={handleClick}>{count}</button>; };
-
-
批量更新机制的差异
- setState:多次调用会合并对象,仅触发一次渲染。
- useState:多次调用函数式更新会按顺序执行,触发多次渲染(React 18 中自动批处理优化为一次)。
// Class 组件 handleClick() {this.setState({ count: this.state.count + 1 });this.setState({ count: this.state.count + 1 }); // 合并为一次更新,最终 count 只加 1 } // 函数组件(React 18 之前) const handleClick = () => {setCount(prev => prev + 1); // 第一次更新setCount(prev => prev + 1); // 第二次更新,最终 count 加 2 };
-
获取更新后的状态
-
Class 组件:setState使用回调函数
-
函数组件:useState使用useEffect
this.setState({ count: this.state.count + 1 },() => {console.log(this.state.count); // 输出新值} ); const [count, setCount] = useState(0);useEffect(() => {console.log(count); // 每次 count 变化时执行 }, [count]);const handleClick = () => {setCount(count + 1); };
-