UseInterval

所以在 class 里你要怎么用 setInterval 做到这一点呢?我会这么做:

class Counter extends React.Component {
  state = {
    count: 0,
    delay: 1000,
  };

  componentDidMount() {
    this.interval = setInterval(this.tick, this.state.delay);
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevState.delay !== this.state.delay) {
      clearInterval(this.interval);
      this.interval = setInterval(this.tick, this.state.delay);
    }
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  tick = () => {
    this.setState({
      count: this.state.count + 1
    });
  }

  handleDelayChange = (e) => {
    this.setState({ delay: Number(e.target.value) });
  }

  render() {
    return (
      <>
        <h1>{this.state.count}</h1>
        <input value={this.state.delay} onChange={this.handleDelayChange} />
      </>
    );
  }
}

Hook 版本

因为需要许多订阅 API 可以随时顺手移除老的监听者和加个新的。但是,setInterval 和它们不一样。当我们执行 clearInterval 和 setInterval 时,它们会进入时间队列里,如果我们频繁重渲染和重执行 effects,interval 有可能没有机会被执行!

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  });

  return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");

// Second interval to demonstrate the issue.
// Fast updates from it cause the Counter's
// interval to constantly reset and never fire.
setInterval(() => {
  ReactDOM.render(<Counter />, rootElement);
}, 100);

当我们 只 想在 mount 时执行 effect 和 unmount 时清理它,我们可以传空 [] 的依赖数组。

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;

但是,现在我们的计时器更新到 1 就不动了

问题在于,useEffect 在第一次渲染时获取值为 0 的 count,我们不再重执行 effect,所以 setInterval 一直引用第一次渲染时的闭包 count,以至于 count + 1 一直是 1。哎呀呀!

  • 修复它的一种方法是用像 setCount(c => c + 1) 这样的 「updater」替换 setCount(count + 1),这样可以读到新 state 变量。但这个无法帮助你获取到新的 props。
  • 另一个方法是用 useReducer()。这种方法为你提供了更大的灵活性。在 reducer 中,你可以访问到当前 state 和新的 props。dispatch 方法本身永远不会改变,所以你可以从任何闭包中将数据放入其中。useReducer() 有个约束是你不可以用它执行副作用。(但是,你可以返回新状态 —— 触发一些 effect。)
function Counter() {
  const [count, dispatch] = useReducer((state, action) => {
    if (action === 'inc') {
      return state + 1;
    }
  }, 0);

  useEffect(() => {
    let id = setInterval(() => {
      dispatch('inc');
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

阻抗不匹配

我们的「阻抗匹配」不在数据库和对象之间,它在 React 编程模型和命令式 setInterval API 之间。

一个 React 组件可能在 mounted 之前流经许多不同的 state,但它的渲染结果将一次性全部描述出来。

Hooks 使我们把相同的声明方法用在 effects 上:

  // 描述每个间隔状态
  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

我们不设置 interval,但指定它是否设置延迟或延迟多少,我们的 Hooks 做到了,用离散术语描述连续过程

相反,setInterval 没有及时地描述过程 —— 一旦设定了 interval,除了清除它,你无法对它做任何改变。

这就是 React 模型和 setInterval API 之间的不匹配。

Refs 出马

  • 我们在第一次渲染时执行带 callback1setInterval(callback1, delay)
  • 我们在下一次渲染时得到携带新的 propsstatecallbaxk2
  • 我们无法在不重置时间的情况下替换掉已经存在的 interval

那么如果我们根本不替换 interval,而是引入一个指向新 interval 回调的可变 savedCallback 会怎么样? 现在我们来看看这个方案:

  • 我们调用 setInterval(fn, delay),其中 fn 调用 savedCallback
  • 第一次渲染后将 savedCallback 设为 callback1
  • 下一次渲染后将 savedCallback 设为 callback2

这个可变的 savedCallback 需要在重新渲染时「可持续(persist)」,所以不可以是一个常规变量,我们想要一个类似实例的字段。

 const savedCallback = useRef();
  // { current: null }

useRef() 返回一个有带有 current 可变属性的普通对象在 renders 间共享,我们可以保存新的 interval 回掉给它:

  function callback() {
    // 可以读到新 props,state等。
    setCount(count + 1);
  }

  // 每次渲染后,保存新的回调到我们的 ref 里。
  useEffect(() => {
    savedCallback.current = callback;
  });

之后我们便可以从我们的 interval 中读取和调用它:

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);

感谢 [],不重执行我们的 effectinterval 就不会被重置。同时,感谢 savedCallback ref,让我们可以一直在新渲染之后读取到回调,并在 interval tick 里调用它。

提取一个 Hook

function useInterval(callback) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
}

当前,1000 delay 是写死的,我想把它变成一个参数:

function Counter() {
  const [count, setCount] = useState(0);

  useInterval(() => {
    setCount(count + 1);
  }, 1000);

  return <h1>{count}</h1>;
}

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

等等,我们不是要避免重置 interval effect,并专门通过 [] 来避免它吗?不完全是,我们只想在回调改变时避免重置它,但当 delay 改变时,我们想要重启 timer

有效!我们现在可以不用想太多 useInterval() 的实现过程,在任意组件中使用它。

暂停 Interval

假设我们希望能够通过传递 null 作为 delay 来暂停我们的 interval:

 const [delay, setDelay] = useState(1000);
  const [isRunning, setIsRunning] = useState(true);

  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

如何实现这个?答案时:不创建 interval

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);

Leave a Reply

Your email address will not be published. Required fields are marked *