React Hooks 常见问题及解决方案
React Hooks 常见问题及解决方案常见问题???? useState 和 setState 有什么明显的区别????? useState 和 useReducer 的初始值如果是个执行函数返回值,执行函数是否会多次执行????? 还原 useReducer 的初始值,为什么还原不回去了????? useEffect 如何模拟 componentDidMount、componentUpdat
React Hooks 常见问题及解决方案
常见问题
-
🐤 useState 和 setState 有什么明显的区别?
-
🐤 useState 和 useReducer 的初始值如果是个执行函数返回值,执行函数是否会多次执行?
-
🐤 还原 useReducer 的初始值,为什么还原不回去了?
-
🐤 useEffect 如何模拟 componentDidMount、componentUpdate、componentWillUnmount 生命周期?
-
🐤 如何在 useEffect 中正确的为 DOM 设置事件监听?
-
🐤 useEffect、useCallback、useMemo 中取到的 state、props 中为什么会是旧值?
-
🐤 useEffect 为什么会出现无限执行的问题?
-
🐤 useEffect 中出现竞态如何解决?
-
🐤 如何在函数组件中保存一些属性,跟随组件进行创建和销毁?
-
🐤 当 useCallback 会频繁触发时,应该如何进行优化?
-
🐤 useCallback 和 useMemo 的使用场景有何区别?
一、函数组件渲染过程
先来看一下函数组件的运作方式:
Counter.js
function Counter() {
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>count: {count}</p>;
}
每次点击 p 标签,count 都会 + 1,setCount 会触发函数组件的渲染。函数组件的重新渲染其实是当前函数的重新执行。
在函数组件的每一次渲染中,内部的 state、函数以及传入的 props 都是独立的。
比如:
// 第一次渲染
function Counter() {
// 第一次渲染,count = 0
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
// 点击 p 标签触发第二次渲染
function Counter() {
// 第二次渲染,count = 1
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
// 点击 p 标签触发第三次渲染
function Counter() {
// 第三次渲染,count = 2
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
// ...
在函数组件中声明的方法也是类似。因此,在函数组件渲染的每一帧对应这自己独立的
state、function、props。
二、useState / useReducer
useState VS setState
-
useState只能作用在函数组件,setState只能作用在类组件 -
useState可以在函数组件中声明多个,而类组件中的状态值都必须声明在this的state对象中 -
一般的情况下,
state改变时:-
useState修改state时,同一个useState声明的值会被 覆盖处理,多个useState声明的值会触发 多次渲染 -
setState修改state时,多次setState的对象会被 合并处理
-
-
useState修改state时,设置相同的值,函数组件不会重新渲染,而继承Component的类组件,即便setState相同的值,也会触发渲染
useState VS useReducer
初始值
useState设置初始值时,如果初始值是个值,可以直接设置,如果是个函数返回值,建议使用回调函数的方式设置
const initCount = c => {
console.log('initCount 执行');
return c * 2;
};
function Counter() {
const [count, setCount] = useState(initCount(0));
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
会发现即便 Counter 组件重新渲染时没有再给 count 重新赋初始值,但是 initCount 函数却会重复执行
修改成回调函数的方式:
const initCount = c => {
console.log('initCount 执行');
return c * 2;
};
function Counter() {
const [count, setCount] = useState(() => initCount(0));
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
这个时候,initCount 函数只会在 Counter 组件初始化的时候执行,之后无论组件如何渲染,initCount 函数都不会再执行
useReducer设置初始值时,初始值只能是个值,不能使用回调函数的方式- 如果是个执行函数返回值,那么在组件重新渲染时,这个执行函数依然会执行
修改状态
useState修改状态时,同一个useState声明的状态会被覆盖处理
function Counter() {
const [count, setCount] = useState(0);
return (
<p
onClick={() => {
setCount(count + 1);
setCount(count + 2);
}}
>
clicked {count} times
</p>
);
}
当前界面中
count的step是 2

useReducer修改状态时,多次dispatch会按顺序执行,依次对组件进行渲染
function Counter() {
const [count, dispatch] = useReducer((x, payload) => x + payload, 0);
return (
<p
onClick={() => {
dispatch(1);
dispatch(2);
}}
>
clicked {count} times
</p>
);
}
当前界面中
count的step是 3

还原 useReducer 的初始值,为什么还原不了
比如下面这个例子:
const initPerson = { name: '小明' };
const reducer = function (state, action) {
switch (action.type) {
case 'CHANGE':
state.name = action.payload;
return { ...state };
case 'RESET':
return initPerson;
default:
return state;
}
};
function Counter() {
const [person, dispatch] = useReducer(reducer, initPerson);
const [value, setValue] = useState('小红');
const handleChange = useCallback(e => setValue(e.target.value), []);
const handleChangeClick = useCallback(() => dispatch({ type: 'CHANGE', payload: value }), [value]);
const handleResetClick = useCallback(() => dispatch({ type: 'RESET' }), []);
return (
<>
<p>name: {person.name}</p>
<input type="text" value={value} onChange={handleChange} />
<br />
<br />
<button onClick={handleChangeClick}>修改</button> |{' '}
<button onClick={handleResetClick}>重置</button>
</>
);
}
点击修改按钮,将对象的 name 改为 小红,点击重置按钮,还原为原始对象。但是我们看看效果:

可以看到 name 修改小红后,无论如何点击重置按钮,都无法还原。
这是因为在 initPerson 的时候,我们改变了 state 的属性,导致初始值 initPerson 发生了变化,所以之后 RESET,即使返回了 initPerson``,但是name 值依然是小红。
所以我们在修改数据时,要注意,不要在原有数据上进行属性操作,重新创建新的对象进行操作即可。比如进行如下的修改:
// ...
const reducer = function (state, action) {
switch (action.type) {
case 'CHANGE':
// !修改后的代码
const newState = { ...state, name: action.payload }
return newState;
case 'RESET':
return initPerson;
default:
return state;
}
};
// ...
看看修改后的效果,可以正常的进行重置了:

三、useEffect
useEffect 基本用法:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count: ', count);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
每次点击 p 标签,Counter 组件都会重新渲染,都可以在控制台看到有 log 打印。
使用 useEffect 模拟 componentDidMount
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count: ', count);
// 设置依赖为一个空数组
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
将 useEffect 的依赖设置为空数组,可以看到,只有在组件初次渲染时,控制台会打印输出。之后无论 count 如何更新,都不会再打印。
使用 useEffect 模拟 componentDidUpdate
- 使用条件判断依赖项是否是初始值,不是的话走更新逻辑
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count !== 0) {
console.log('count: ', count);
}
}, [count]);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
但是这样处理有个弊端,当有多个依赖项时,需要多次比较,因此可以选择使用下面这种方式。
- 使用
useRef设置一个初始值,进行比较
function Counter() {
const [count, setCount] = useState(0);
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
console.log('count: ', count);
}
}, [count]);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
使用 useEffect 模拟 componentWillUnmount
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count: ', count);
return () => {
console.log('component will unmount')
}
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
useEffect 中包裹函数中返回的函数,会在函数组件重新渲染时,清理上一帧数据时触发执行。因此这个函数可以做一些清理的工作。
如果 useEffect 给定的依赖项是一个空数组,那么返回函数被执行时,代表着组件真正被卸载了。
给
useEffect设置 依赖项为空数组,并且 返回一个函数,那么这个返回的函数就相当于是componentWillUnmount请注意,必须要设置依赖项为空数组。如果不是空数组,那么这个函数并不是在组件被卸载时触发,而是会在组件重新渲染,清理上一帧的数据时触发。
在 useEffect 正确的为 DOM 设置事件监听
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
return () => {
window.removeEventListener('click', handleClick, false)
};
}, [count]);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
在 useEffect 中设置事件监听,在 return 的函数中对副作用进行清理,取消监听事件
在 useEffect、useCallback、useMemo 中获取到的 state、props 为什么是旧值
正如我们刚才所说,函数组件的每一帧会有自己独立的 state、function、props。而 useEffect、useCallback、useMemo 具有缓存功能。
因此,我们取的是当前对应函数作用域下的变量。如果没有正确的设置依赖项,那么 useEffect、useCallback、useMemo 就不会重新执行,其中使用的变量还是之前的值。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
return () => {
window.removeEventListener('click', handleClick, false)
};
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
还是上一个例子,如果此时给
useEffect设置空数组为依赖项,那么无论count改变了多少次,点击window,打印出来的count依然是 0
useEffect 中为什么会出现无限执行的情况
- 没有为
useEffect设置依赖项,并且在useEffect中更新state,会导致界面无限重复渲染
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
这种情况会导致界面无限重复渲染,因为没有设置依赖项,如果我们想在界面初次渲染时,给 count 设置新值,给依赖项设置空数组即可。
修改后:只会在初始化时设置 count 值
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
上面这个例子是依赖项缺失的时候,会出现问题,那么在依赖项正常设置的情况下,也会出现问题。
- 此时有一个需求:每次
count增加的时候,我们需要进行翻页(page+ 1),看看如何写:
由于此时我们依赖 count,依赖项中要包含 count,而修改 page 时又需要依赖 page,所以依赖项中也要包含 page
function Counter() {
const [count, setCount] = useState(0);
const [page, setPage] = useState(0);
useEffect(() => {
setPage(page + 1);
}, [count, page]);
return (
<>
<p onClick={() => setCount(count + 1)}>clicked {count} times</p>
<p>page: {page}</p>
</>
);
}
此时也会导致界面无限重复渲染的情况,那么此时修改 page 时改成函数的方式,并从依赖性中移除 page 即可
修改后:既能实现效果,又避免了重复渲染
function Counter() {
const [count, setCount] = useState(0);
const [page, setPage] = useState(0);
useEffect(() => {
setPage(p => p + 1);
}, [count]);
return (
<>
<p onClick={() => setCount(count + 1)}>clicked {count} times</p>
<p>page: {page}</p>
</>
);
}
四、竞态
执行更早但返回更晚的情况会错误的对状态值进行覆盖
在 useEffect 中,可能会有进行网络请求的场景,我们会根据父组件传入的 id,去发起网络请求,id 变化时,会重新进行请求。
function App() {
const [id, setId] = useState(0);
useEffect(() => {
setId(10);
}, []);
// 传递 id 属性
return <Counter id={id} />;
}
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
setTimeout(() => {
const result = `id 为${id} 的请求结果`;
resolve(result);
}, Math.random() * 1000 + 1000);
});
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
// 发送网络请求,修改界面展示信息
const getData = async () => {
const result = await fetchData(id);
setData(result);
};
getData();
}, [id]);
return <p>result: {data}</p>;
}
展示结果:

上面的实例,多次刷新页面,可以看到最终结果有时展示的是 id 为 0 的请求结果,有时是 id 为 10 的结果。
正确的结果应该是 ‘id 为 10 的请求结果’。这个就是竞态带来的问题。
解决办法:
- 取消异步操作
// 存储网络请求的 Map
const fetchMap = new Map();
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
const timer = setTimeout(() => {
const result = `id 为${id} 的请求结果`;
// 请求结束移除对应的 id
fetchMap.delete(id);
resolve(result);
}, Math.random() * 1000 + 1000);
// 设置 id 到 fetchMap
fetchMap.set(id, timer);
});
// 取消 id 对应网络请求
const removeFetch = (id) => {
clearTimeout(fetchMap.get(id));
}
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
const getData = async () => {
const result = await fetchData(id);
setData(result);
};
getData();
return () => {
// 取消对应网络请求
removeFetch(id)
}
}, [id]);
return <p>result: {data}</p>;
}
展示结果:

此时无论如何刷新页面,都只展示 id 为 10 的请求结果。
- 设置布尔值变量进行追踪
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
setTimeout(() => {
const result = `id 为${id} 的请求结果`;
resolve(result);
}, Math.random() * 1000 + 1000);
});
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
let didCancel = false;
const getData = async () => {
const result = await fetchData(id);
if (didCancel) {
setData(result);
}
};
getData();
return () => {
didCancel = true;
};
}, [id]);
return <p>result: {data}</p>;
}
可以发现,此时无论如何刷新页面,也都只展示 id 为 10 的请求结果。
五、如何在函数组件中保存住非 state、props 的值
函数组件是没有 this 指向的,所以为了可以保存住组件实例的属性,可以使用 useRef 来进行操作
函数组件的 ref 具有可以 穿透闭包 的能力。通过将普通类型的值转换为一个带有 current 属性的对象引用,来保证每次访问到的属性值是最新的。
保证在函数组件的每一帧里访问到的 state 值是相同的
- 先看看不使用
useRef的情况下,每一帧里的state值是如何打印的
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
先点击 p 标签 5 次,之后点击 window 对象,可以看到打印结果:

- 使用
useRef之后,每一帧里的ref值是如何打印的
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
// 将最新 state 设置给 countRef.current
countRef.current = count;
const handleClick = function () {
console.log('count: ', countRef.current);
};
window.addEventListener('click', handleClick, false);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
和之前一样的操作,先点击 p 标签 5 次,之后点击 window 界面,可以看到打印结果

使用
useRef即可以保证函数组件的每一帧里访问到的state值是相同的。
如何保存住函数组件实例的属性
函数组件是没有实例的,因此属性也无法挂载到 this 上。那如果我们想创建一个非 state、props 变量,能够跟随函数组件进行创建销毁,该如何操作呢?
同样的,还是可以通过 useRef,useRef 不仅可以作用在 DOM 上,还可以将普通变量转化成带有 current 属性的对象
比如,我们希望设置一个 Model 的实例,在组件创建时,生成 model 实例,组件销毁后,重新创建,会自动生成新的 model 实例
class Model {
constructor() {
console.log('创建 Model');
this.data = [];
}
}
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(new Model());
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
按照这种写法,可以实现在函数组件创建时,生成 Model 的实例,挂载到 countRef 的 current 属性上。重新渲染时,不会再给 countRef 重新赋值。
也就意味着在组件卸载之前使用的都是同一个 Model 实例,在卸载之后,当前 model 实例也会随之销毁。
仔细观察控制台的输出,会发现虽然
countRef没有被重新赋值,但是在组件在重新渲染时,Model的构造函数却依然会多次执行
所以此时我们可以借用 useState 的特性,改写一下。
class Model {
constructor() {
console.log('创建 Model');
this.data = [];
}
}
function Counter() {
const [count, setCount] = useState(0);
const [model] = useState(() => new Model());
const countRef = useRef(model);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
这样使用,可以在不修改 state 的情况下,使用 model 实例中的一些属性,可以使 flag,可以是数据源,甚至可以作为 Mobx 的 store 进行使用。
六、useCallback
如题,当依赖频繁变更时,如何避免 useCallback 频繁执行呢?
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return <p onClick={handleClick}>clicked {count} times</p>;
}
这里,我们把 click 事件提取出来,使用 useCallback 包裹,但其实并没有起到很好的效果。
因为 Counter 组件重新渲染目前只依赖 count 的变化,所以这里的 useCallback 用与不用没什么区别。
使用 useReducer 替代 useState
可以使用 useReducer 进行替代。
function Counter() {
const [count, dispatch] = useReducer(x => x + 1, 0);
const handleClick = useCallback(() => {
dispatch();
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
useReducer 返回的 dispatch 函数是自带了 memoize 的,不会在多次渲染时改变。因此在 useCallback 中不需要将 dispatch 作为依赖项。
向 setState 中传递函数
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
在 setCount 中使用函数作为参数时,接收到的值是最新的 state 值,因此可以通过这个值执行操作。
通过 useRef 进行闭包穿透
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleClick = useCallback(() => {
setCount(countRef.current + 1);
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
这种方式也可以实现同样的效果。但是不推荐使用,不仅要编写更多的代码,而且可能会产生出乎预料的问题。
七、useMemo
上面讲述了 useCallback 的一些问题和解决办法。下面看一看 useMemo。
useMemo 和 React.memo 不同:
useMemo是对组件内部的一些数据进行优化和缓存,惰性处理。React.memo是对函数组件进行包裹,对组件内部的state、props进行浅比较,判断是否需要进行渲染。
useMemo 和 useCallback 的区别
useMemo的返回值是一个值,可以是属性,可以是函数(包括组件)useCallback的返回值只能是函数
因此,useMemo 一定程度上可以替代 useCallback,等价条件:useCallback(fn, deps) => useMemo(() => fn, deps)
所以,上述关于 useCallback 一些优化点同样适用于 useMemo。
八、参考文档
更多推荐


所有评论(0)