学习React最新的官方文档,读到《自定义Hook》这一章,发现了一个好玩的东西。 挑战题4的示例代码如下:
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { useCounter } from './useCounter.js';
export default function Counter() {
const count = useCounter(1000);
const changeBgcolor = useInterval(() => {
const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
document.body.style.backgroundColor = randomColor;
}, 2000);
return <h1>Seconds passed: {count}</h1>;
}
export function useCounter(delay) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, delay);
return count;
}
export function useInterval(onTick, delay) {
useEffect(() => {
const id = setInterval(onTick, delay);
return () => {
clearInterval(id);
};
}, [onTick, delay]);
}
这个示例有 两个 独立的计时器。
App 组件调用 useCounter,这个 Hook 调用 useInterval 来每秒更新一次计数器。但是 App 组件 也 调用 useInterval 每两秒随机更新一次页面背景色。
问题1:更新页面背景色的回调函数因为一些原因从未执行过。原因是什么呢?
答:在这段代码中,useCounter和changeBgcolor都使用了useInterval自定义Hook。useCounter的延迟时间是1000ms,而changeBgcolor的延迟时间是2000ms。这意味着useCounter的回调函数会比changeBgcolor的回调函数更早地执行。
当useCounter的回调函数执行setCount时,会引发App组件的重新渲染。在React中,组件的重新渲染会导致所有的Hook重新运行。因此,useInterval也会重新运行。
在useInterval的实现中,useEffect的依赖数组包含了onTick和delay。这意味着只有当这两个值改变时,useEffect才会重新运行,设置新的定时器。在这段代码中,由于setCount引发的重新渲染,onTick函数每次都是新的,所以useEffect会重新运行。
当useEffect重新运行时,它首先会执行清理函数clearInterval,清除之前的定时器。然后,它会设置新的定时器。由于changeBgcolor的定时器被清除,而新的定时器还没有到达2000ms,所以changeBgcolor的回调函数就不会执行。
问题2:如何修改才能解决这个问题?
答:如果想让changeBgcolor的回调函数执行,需要确保onTick函数在组件的重新渲染之间保持不变,或者在useEffect的依赖数组中移除onTick。
官方文档里给出的解决方案是用useEffectEvent。
import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { useCounter } from './useCounter.js';
export default function Counter() {
const count = useCounter(1000);
const changeBgcolor = useInterval(() => {
const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
document.body.style.backgroundColor = randomColor;
}, 2000);
return <h1>Seconds passed: {count}</h1>;
}
export function useCounter(delay) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, delay);
return count;
}
export function useInterval(callback, delay) {
const onTick = useEffectEvent(callback);
useEffect(() => {
const id = setInterval(onTick, delay);
return () => clearInterval(id);
}, [delay]);
}
思考一下:你有其他的解决方案吗?
问题3:如果把changeBgcolor的delay时间缩短到500ms,会发生什么呢?
答:背景色和count都会变化。
问题4:为什么count delay时间短时,背景色不变。但是背景色时间短时,count还是会变呢?
答:当背景色的delay时间短时,changeBgcolor的onTick函数会频繁地被调用,但是这并不会引发React的重新渲染,因为它并没有改变任何状态。因此,count的定时器不会被清除,count的onTick函数还会按照原定的delay时间执行,所以count还是会变。
如果你在useInterval的代码中添加日志,你会发现,当背景色改变时,日志并没有被调用,这是因为changeBgcolor的onTick函数并没有引发重新渲染。而当count改变时,日志被调用了,这是因为setCount引发了重新渲染,导致useInterval的useEffect重新运行。
useEffect(() => {
console.log('✅ Setting up an interval with delay ', delay)
const id = setInterval(onTick, delay);
return () => {
console.log('❌ Clearing an interval with delay ', delay)
clearInterval(id);
};
}, [onTick, delay]);
问题5:怎样才能让count不变,而changBgcolor变呢?
这个问题就留给你自己思考一下吧😁