文章目录
前言
随笔记录react面试题
1. 在 useEffect 中,空数组 [] 作为依赖数组的作用
在 useEffect 中,空数组 [] 作为依赖数组的作用是告诉 React 该副作用函数(useEffect)只会在组件的 首次渲染 时执行一次,且 不再执行 后续的渲染。简单来说,空数组表示 “没有依赖”,所以这个副作用函数不会响应任何变化,只会在组件加载时执行。
具体解释:
useEffect的基本工作原理:
useEffect接受两个参数:- 第一个参数是一个副作用函数,它将在组件渲染后执行。
- 第二个参数是一个依赖数组,
useEffect会监视数组中的变量,一旦其中的任何一个变量发生变化,副作用函数就会重新执行。
空数组
[]的作用:
当你传递一个空数组[]作为useEffect的第二个参数时,React 会将其视为没有任何依赖项。这意味着副作用函数只会在 组件首次挂载时执行一次,并且 不再执行。也就是说,只有组件加载时才会执行setTimeout设置定时器,之后组件的任何重新渲染都不会触发setTimeout的重新执行。常见用途:
使用空数组通常用于以下几种情况:- 组件挂载时执行一次副作用(比如设置定时器、发送请求、订阅数据等)。
- 组件卸载时清理副作用(如清除定时器、取消订阅、移除事件监听器等)。
例子:
import { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 组件挂载时设置定时器
const timer = setTimeout(() => {
setLoading(true);
setCount(prevCount => prevCount + 1);
}, 2000);
// 组件卸载时清除定时器
return () => clearTimeout(timer);
}, []); // 依赖数组为空,意味着只会在组件首次渲染时执行
return (
<div>
{loading ? <p>Loaded!</p> : <p>Loading...</p>}
<p>Count: {count}</p>
</div>
);
}
代码解析:
useEffect(() => {...}, []): 这个useEffect在组件首次渲染时执行,且只执行一次。- 设置一个 2 秒的定时器,2 秒后更新
count和loading状态。 - 在组件卸载时(如页面跳转、组件被移除),会清除定时器,避免内存泄漏。
- 设置一个 2 秒的定时器,2 秒后更新
总结:
- 空数组
[]表示useEffect的副作用只会在组件首次挂载时执行一次,后续的更新不会重新执行这个副作用函数。 - 这种方式通常用于需要在组件挂载时执行一次的操作,比如设置定时器、发起 API 请求、订阅事件等。
在 React 中,setState 是一个异步操作,它会触发组件的重新渲染。当你在 setTimeout 或任何异步函数中使用 setState 时,可能会遇到一些意想不到的行为,尤其是在组件已经卸载的情况下。这是因为 React 可能会在 setTimeout 完成后,仍然尝试执行 setState,而组件可能已经被卸载或没有完全渲染。
2. 为什么不推荐在 setTimeout 中直接使用 setState ?
异步行为导致的不确定性:
React 的setState是异步的,它不会立即更新状态,而是将更新排入队列。setTimeout本身也是异步的,因此在setTimeout中直接调用setState可能导致不可预测的行为,尤其是在多个异步操作之间。组件卸载后调用
setState会产生警告:
如果组件在setTimeout调用的setState之前已经卸载,React 会给出警告:Can't perform a React state update on an unmounted component。如果你试图在卸载后的组件上调用setState,会导致内存泄漏。
解决方法
清除定时器:
在setTimeout中使用setState时,最好的实践是确保组件在setState执行时仍然挂载。你可以通过在组件卸载时清除定时器来避免这种问题。import { useState, useEffect } from 'react'; function MyComponent() { const [count, setCount] = useState(0); const [loading, setLoading] = useState(false); useEffect(() => { // 组件挂载时设置定时器 const timer = setTimeout(() => { setLoading(true); setCount(prevCount => prevCount + 1); }, 2000); // 组件卸载时清除定时器 return () => clearTimeout(timer); }, []); return ( <div> {loading ? <p>Loaded!</p> : <p>Loading...</p>} <p>Count: {count}</p> </div> ); }- 说明:在
useEffect中创建定时器,并且在返回的清理函数中清除它,以防组件在定时器执行之前卸载。
- 说明:在
在异步函数中检查组件是否仍然挂载:
如果你需要在异步操作(如setTimeout、fetch请求等)中执行setState,可以使用一个标志来检查组件是否仍然挂载。import { useState, useEffect } from 'react'; function MyComponent() { const [count, setCount] = useState(0); const [loading, setLoading] = useState(false); useEffect(() => { let isMounted = true; // 标志位,记录组件是否挂载 const timer = setTimeout(() => { if (isMounted) { setLoading(true); setCount(prevCount => prevCount + 1); } }, 2000); // 组件卸载时清除定时器并设置标志为 false return () => { clearTimeout(timer); isMounted = false; }; }, []); return ( <div> {loading ? <p>Loaded!</p> : <p>Loading...</p>} <p>Count: {count}</p> </div> ); }- 说明:通过
isMounted标志位,确保只有在组件挂载时才调用setState。
- 说明:通过
使用
useRef来保存组件挂载状态:
使用useRef可以更简洁地保存组件是否挂载的状态,并且它不会触发组件重新渲染。import { useState, useEffect, useRef } from 'react'; function MyComponent() { const [count, setCount] = useState(0); const [loading, setLoading] = useState(false); const isMounted = useRef(false); // 使用 useRef 保存挂载状态 useEffect(() => { isMounted.current = true; // 组件挂载时设置为 true const timer = setTimeout(() => { if (isMounted.current) { setLoading(true); setCount(prevCount => prevCount + 1); } }, 2000); return () => { clearTimeout(timer); isMounted.current = false; // 组件卸载时设置为 false }; }, []); return ( <div> {loading ? <p>Loaded!</p> : <p>Loading...</p>} <p>Count: {count}</p> </div> ); }- 说明:使用
useRef可以避免每次组件重新渲染时都检查挂载状态,因此相对更高效。
- 说明:使用
总结
- 问题:
setTimeout中直接调用setState可能导致在组件卸载后调用setState,引发警告或错误。 - 解决方法:可以通过清除定时器、使用
isMounted标志位,或使用useRef来确保组件仍然挂载,避免内存泄漏和警告。
这些做法确保了异步操作不会影响已卸载的组件,避免了 React 的警告信息。
3. useReducer 和 useMemo 的基本概念
useReducer:- 用于管理复杂的组件状态,尤其是状态逻辑比较复杂或涉及多个子状态的场景。
- 类似于 Redux 的 reducer,
useReducer接受一个 reducer 函数和初始状态,并返回当前状态和一个dispatch函数,用于更新状态。 useReducer是基于 “action” 和 “state” 的逻辑来更新状态的,它提供了一种更加结构化的方式来管理状态,适用于大型或复杂组件。
useMemo:- 用于 缓存 计算结果,避免重复执行昂贵的计算。
- 它会记住传递给它的函数的结果,只有当依赖项发生变化时,才会重新计算。这样可以减少不必要的计算,提升性能。
useMemo不会改变组件的状态,只会优化组件的渲染过程,特别是当计算值比较昂贵时。
2. useReducer 和 useMemo 结合使用
useReducer 和 useMemo 可以结合使用,但它们各自的职责不同:useReducer 用于状态管理,而 useMemo 用于性能优化。
使用场景:
useReducer通常用于复杂的状态管理,比如多个相关的状态、需要特定的更新逻辑等。useMemo用于缓存某些值,避免重复的昂贵计算,或者当状态变化时只重新计算某些特定的部分。
4.结合 useReducer 和 useMemo 使用
假设你有一个复杂的表单组件,其中有多个状态和一些计算依赖于当前的状态。在这种情况下,useReducer 用于管理复杂的表单状态,而 useMemo 用于缓存一些昂贵的计算结果,减少不必要的渲染。
代码示例:
import React, { useReducer, useMemo } from 'react';
// 初始化状态
const initialState = {
firstName: '',
lastName: '',
age: 0,
};
// 定义 reducer 函数
function reducer(state, action) {
switch (action.type) {
case 'SET_FIRST_NAME':
return { ...state, firstName: action.payload };
case 'SET_LAST_NAME':
return { ...state, lastName: action.payload };
case 'SET_AGE':
return { ...state, age: action.payload };
default:
return state;
}
}
function ComplexForm() {
const [state, dispatch] = useReducer(reducer, initialState);
// 使用 useMemo 缓存计算结果,避免每次重新计算
const fullName = useMemo(() => {
console.log('Calculating full name...');
return `${state.firstName} ${state.lastName}`;
}, [state.firstName, state.lastName]);
const isAdult = useMemo(() => {
console.log('Checking adult status...');
return state.age >= 18;
}, [state.age]);
return (
<div>
<input
type="text"
value={state.firstName}
onChange={(e) => dispatch({ type: 'SET_FIRST_NAME', payload: e.target.value })}
placeholder="First Name"
/>
<input
type="text"
value={state.lastName}
onChange={(e) => dispatch({ type: 'SET_LAST_NAME', payload: e.target.value })}
placeholder="Last Name"
/>
<input
type="number"
value={state.age}
onChange={(e) => dispatch({ type: 'SET_AGE', payload: Number(e.target.value) })}
placeholder="Age"
/>
<div>
<p>Full Name: {fullName}</p>
<p>Adult Status: {isAdult ? 'Adult' : 'Not an Adult'}</p>
</div>
</div>
);
}
export default ComplexForm;
为什么要这样结合使用?
性能优化:通过
useMemo,我们避免了每次状态更新都重新计算fullName和isAdult。例如,在用户输入名字时,如果名字没有变化,那么fullName就不需要重新计算,从而提高性能。简化状态管理:
useReducer提供了一种集中式的方式来管理组件状态,使得复杂的状态更新逻辑更加清晰和易于维护。避免重复计算:
useMemo会缓存计算结果,只有当依赖项(比如firstName、lastName或age)发生变化时才重新计算。这意味着即使组件重新渲染,fullName和isAdult也不会重复计算,除非相关状态发生变化。
什么时候结合使用 useReducer 和 useMemo?
- 当组件有复杂的状态管理,并且你需要优化某些计算过程时,结合
useReducer和useMemo会非常有效。 - 特别是当状态更新频繁,但某些计算过程非常昂贵时,
useMemo可以减少不必要的重新计算,而useReducer提供了更强的状态管理能力。
总结
useReducer用于管理复杂的状态,特别是需要依赖复杂逻辑或多个状态项时。useMemo用于缓存计算结果,减少昂贵计算的频繁执行,提升性能。- 这两个 Hook 可以结合使用,通过
useReducer管理状态和通过useMemo缓存计算结果,优化性能,减少不必要的重新渲染和计算。