useEffect 中的一个问题

57 阅读2分钟

问题

步骤

一:点击 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)
*/