问题
步骤
一:点击 start 开始。
二:不点 stop,而是直接点击 clear 按钮,终止执行并将值改为 100。
但最终的显示结果却不是 100
为什么?
/*
React 16.8
fn组件 + hook (useEffect)
*/
// 时间间隔,哪怕设置为0ms,但是「定时器」本身有最小间隔,大约 4ms
/*
时间间隔 :100ms
没有问题 ✅
间隔 :10ms
出现问题 ❌
发现:定时器时间的间隔越小,越会出现这个问题
*/
const TimeInterval = 0;
function App() {
const [MS, setMS] = React.useState(0);
const [running, setRunning] = React.useState(false);
function handleClearClick() {
console.log('click clear')
setRunning(false);
setMS(100);
}
React.useEffect(() => {
console.log("1----" + running);
if (running) {
console.log("2----" + running);
const startTime = Date.now() - MS;
const intervalId = setInterval(() => {
let n = Date.now() - startTime;
console.log('interval', n)
setMS(n);
}, TimeInterval);
return () => {
console.log("0---" + running);
clearInterval(intervalId);
};
}
}, [running]);
function handleRunClick() {
setRunning(r => !r);
}
console.log('is running: ', running);
return (
<div>
<label style={labelStyles}>{MS}ms</label>
<button onClick={handleRunClick} >
{running ? "Stop" : "Start"}
</button>
<button onClick={handleClearClick} >
Clear
</button>
</div>
);
}
控制台:
click clear
is running false
interval 832
0----true
1----false
is running false
原来,点击 clear 按钮之后,页面其实已经显现出 100 了。
但后面又执行了定时器(控制台在 click clear 之后又输出 interval ),把页面又改了一次。
问:在 useEffect 的 return 中清理了定时器,为什么还会执行 ?
答:useEffect 中的 return 执行时机晚了。
为什么时机晚了?这就要看源码了
解决方式一 useLayoutEffect
/*
本例
react 16.8
fn组件 + hook (useLayoutEffect)
*/
时间间隔 :100ms
没有问题 ✅
间隔 :10ms
没有问题 ✅
间隔 < 10
没有问题 ✅
控制台:
click clear
is running false
0----true
1----false
问:为什么用 useLayoutEffect 就能解决?本质是什么?
解决方式二: 加上 useReducer
/*
本例
react 16.8
用 fn组件 + hook (useEffect、 useReducer)
*/
const TimeInterval = 10;
const TICK = 'TICK'
const CLEAR = 'CLEAR'
const TOGGLE = 'TOGGLE'
/*
时间间隔 :100ms
没有问题 ✅
间隔 :10ms
没有问题 ✅
间隔 < 10
没有问题 ✅
*/
function stateReducer(state = {}, action) {
switch (action.type) {
case TOGGLE:
return Object.assign({},state,{running: !state.running});
case TICK:
if (state.running) {
return Object.assign({},state,{ms: action.ms});
}
return state
case CLEAR:
return {running: false, MS: 100}
default:
return state
}
}
function App() {
const [state, dispatch] = React.useReducer(stateReducer, {
ms: 0,
running: false,
})
React.useEffect(() => {
console.log("1----" + state.running);
if (state.running) {
console.log("2----" + state.running);
const startTime = Date.now() - state.ms
const intervalId = setInterval(() => {
console.log('interval');
dispatch({
type: TICK,
ms: Date.now() - startTime,
})
}, TimeInterval)
return () => {
console.log("0---" + state.running);
clearInterval(intervalId);
};
}
},[state.running])
function handleRunClick() {
dispatch({
type: TOGGLE,
})
}
function handleClearClick() {
// debugger;
console.log('click clear')
dispatch({
type: CLEAR,
})
}
console.log('is running: ', state.running);
console.dir(state)
return (
<div>
<label style={labelStyles}>{state.ms}ms</label>
<button onClick={handleRunClick} style={buttonStyles}>
{state.running ? 'Stop' : 'Start'}
</button>
<button onClick={handleClearClick} style={buttonStyles}>
Clear
</button>
</div>
)
}
用了 useReducer 后,问题解决了
控制台:
click clear
is running false
interval
0----true
1----false
is running false
看控制台输出,发现:
点击 clear 后,定时器同样又执行了一次。
那为什么没出错?
因为 useReducer 中,在 case Tick 内,又做了一次限制
if (state.running) {
}
所以,最后的那次 定时器 re-render 对数据的修改被拦截了。
如果把这个 useReducer 内的拦截去掉,就变得和 a_1 中一样了
解决方式三: 升级到 React 17
/*
本例
react 17.0.0
fn组件 + hook (useEffect)
*/