# 1. hooks 与定时器

在 hooks 中使用了定时器对于新手来说往往会出错,本文将介绍并剖析。

# 1.1. 错误的定时器用法

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

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

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

页面上的 count 永远是 1,因为 useEffect 的依赖数组重没有包含 count。导致定时器中 count 永远是第一次渲染时的值,即 0 。页面上一直为 0+1 = 1

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

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

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

现在在依赖数组中加入了 count,页面上的数值不一直为 1 了, 但是过不了一会儿,页面上的数字就会开始闪烁。 这是因为每次 count 变化后,都重新去运行了一遍 useEffect,导致生成了非常多的定时器。页面上结果为:

  • 0 + 1
  • 1 + 1
  • 2 + 1
  • 3 + 1
  • 4 + 1
  • ...

所以数字在闪烁。

# 1.2. 正确的定时器设置

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

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

    return () => clearInterval(id);
  }, [count]);

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

每下一次渲染前都用 useEffect 的 return 销毁掉之前生成的 定时器。

不过这样是比较耗费性能的,每一次加一都要执行一遍创建和销毁定时器。可以使用箭头函数的方式来更新 count。

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

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

    return () => clearInterval(id);
  }, []);

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

# 1.3. 思考这样能不能实现定时器效果?

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

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

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

答案是:会。 看似没有把 count 放入依赖数组中,但不使用依赖数组的情况下,useEffect 会在第一次渲染之后和每次更新之后都会执行。这就使得每次渲染都能拿到最新的 count 值,这样就能实现定时器效果了。

不过,这会导致一个非常隐蔽的 BUG,参见 Dan 的博客 (opens new window)

原因: setInterval 和它们不一样。当我们执行 clearInterval 和 setInterval 时,它们会进入时间队列里,如果我们频繁重渲染和重执行 effects,interval 有可能没有机会被执行!