经由过程 React Hooks 声明式地运用 setInterval

本文由云+社区宣布

作者:Dan Abramov

打仗 React Hooks 肯定时刻的你,或许会遇到一个奇异的题目: setInterval 用起来没你想的简朴

Ryan Florence 在他的推文内里说到:

不少朋侪跟我提起,setInterval 和 hooks 一升引的时刻,有种蛋蛋的难过。

老实说,这些朋侪也不是胡扯。刚最先打仗 Hooks 的时刻,确切还挺让人迷惑的。

但我以为谈不上 Hooks 的缺点,而是 React 编程模子setInterval 之间的一种形式差别。比拟类(Class),Hooks 更切近 React 编程模子,使得这类差别越发凸起。

虽然有点绕,然则让二者调和相处的要领,照样有的。

本文就来探究一下,怎样让 setInterval 和 Hooks 调和地游玩,为什么是这类体式格局,以及这类体式格局给你带来了什么新才能

声明:本文采纳循规蹈矩的示例来诠释题目。所以有一些示例虽然看起来可以有捷径可走,然则我们照样一步步来。

假如你是 Hooks 新手,不太邃晓我在纠结啥,无妨读一下 React Hooks 的引见官方文档。本文假定读者已运用 Hooks 凌驾一个小时。

代码呢?

经由历程下面的体式格局,我们可以轻松地完成一个每秒自增的计数器:

import React, { useState, useEffect, useRef } from 'react';

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

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);

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

CodeSandbox 线上示例

上述 useInterval 并非内置的 React Hook,而是我完成的一个自定义 Hook

import React, { useState, useEffect, useRef } from 'react';

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

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  });

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

(假如你在错过了,这里也有一个一样的 CodeSandbox 线上示例

我完成的 useInterval Hook 设置了一个计时器,而且在组件 unmount 的时刻清算掉了。 这是经由历程组件生命周期上绑定 setIntervalclearInterval 的组合完成的。

这是一份可以在项目中随便复制粘贴的完成,你以至可以宣布到 NPM 上。

不关心为什么如许完成的读者,就不必继承浏览了。下面的内容是为愿望深切明白 React Hooks 的读者而预备的。

哈?! 🤔

我晓得你想什么:

Dan,这代码不对劲。说好的“地道 JavaScript”呢?React Hooks 打了 React 哲学的脸?

哈,我一最先也是这么想的,然则厥后我转变了,如今,我预备也转变你的主意。最先之前,我先引见下这份完成的才能。

为什么 useInterval() 是一个更合理的 API?

注意下,useInterval Hook 吸收一个函数和一个延时作为参数:

  useInterval(() => {
    // ...
  }, 1000);

这个跟原生的 setInterval 异常的相似:

  setInterval(() => {
    // ...
  }, 1000);

那为啥不痛快运用 setInterval 呢?

setIntervaluseInterval Hook 最大的区分在于,useInterval Hook 的参数是“动态的”。乍眼一看,可以不是那末显著。

我将经由历程一个现实的例子来申明这个题目:

假如我们愿望 interval 的距离是可调的:

《经由过程 React Hooks 声明式地运用 setInterval》一个延时可输入的计时器

此时无需手动掌握延时,直接动态调解 Hooks 参数就好了。比方说,我们可以在用户切换到另一个选项卡时,下降 AJAX 更新数据的频次。

假如根据类(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} />
      </>
    );
  }
}

(CodeSandbox 在线示例)

太熟习了!

那改成运用 Hooks 怎样完成呢?

🥁🥁🥁扮演最先了!

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

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, delay);

  function handleDelayChange(e) {
    setDelay(Number(e.target.value));
  }

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

(CodeSandbox 线上示例)

没了,就这么多!

不必于 class 完成的版本,useInterval Hook “升级到”支撑到支撑动态调解延时的版本,没有增添任何庞杂度。

运用 useInterval 新增动态延时才能,几乎没有增添任何庞杂度。这个上风是运用 class 没法比拟的。

// 牢固延时
useInterval(() => {
  setCount(count + 1);
}, 1000);

// 动态延时
useInterval(() => {
  setCount(count + 1);
}, delay);

useInterval 吸收到另一个 delay 的时刻,它就会从新设置计时器。

我们并没有经由历程实行代码来设置或许清算计时器,而是声清楚明了具有特定延时的计时器 – 这是我们完成的 useInterval 的基础缘由。

假如想暂时停息计时器呢?我可以如许来:

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

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

(线上示例)

这就是 Hooks 和 React 再一次让我高兴的缘由。我们可以把原有的挪用式 API,包装成声明式 API,从而越发贴切地表达我们的企图。就跟衬着一样,我们可以形貌当前时刻每一个点的状况,而无需战战兢兢地经由历程详细的敕令来操纵它们。

到这里,我愿望你已确信 useInterval Hook 是一个更好的 API – 至少在组件层面运用的时刻是如许。

但是为什么在 Hooks 里运用 setInterval 和 clearInterval 这么让人恼火? 回到刚最先的计时器例子,我们尝试手动去完成它。

第一次

最简朴的,衬着初始状况:

function Counter() {
  const [count, setCount] = useState(0);
  return <h1>{count}</h1>;
}

如今我愿望它每秒定时更新。我预备运用 useEffect() 而且返回一个清算要领,因为它是一个须要清算的 Side Effect

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

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

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

(检察 CodeSandbox 线上示例)

看起来很简朴?

但是,这段代码有个诡异的行动。

React 默许会在每次衬着时,都从新实行 effects。这是相符预期的,这机制规避了初期在 React Class 组件中存在的一系列题目

一般来讲,这是一个好特征,因为大部份的定阅 API 都许可移除旧的定阅并增加一个新的定阅来替代。然则,这不包含 setInterval。挪用了 clearInterval 后从新 setInterval 的时刻,计时会被重置。假如我们频仍从新衬着,致使 effects 频仍实行,计时器可以基础没有机会被触发!

经由历程运用在一个更小的时刻距离从新衬着我们的组件,可以重现这个 BUG:

setInterval(() => {
  // 从新衬着致使的 effect 从新实行会让计时器在挪用之前,
  // 就被 clearInterval() 清算掉,以后 setInterval()
  // 从新设置的计时器,会从新最先计时
  ReactDOM.render(<Counter />, rootElement);
}, 100);

(检察这个 BUG 的线上示例)

第二次

部份读者可以晓得,useEffect 许可我们掌握从新实行的现实。经由历程在第二个参数指定依靠数组,React 就会只在这个依靠数组变动的时刻从新实行 effect。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]);

假如我们愿望 effect 只在组件 mount 的时刻实行,而且在 unmount 的时刻清算,我们可以通报空数组 [] 作为依靠。

然则!不是迥殊熟习 JavaScript 闭包的读者,很可以会犯一个共性毛病。我来树模一下!(我们在设想 lint 划定规矩来协助定位此类毛病,不过如今还没有预备好。)

第一次的题目在于,effect 的从新实行致使计时器太早被清算掉了。假如不从新实行它们,或许可以处置惩罚这个题目:

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

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

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

假如如许完成,计时器更新到 1 以后,就住手不动了。(检察这个 BUG 的线上示例)

发生了啥?

题目在于,useEffect 运用的 count 是在第一次衬着的时刻猎取的。 猎取的时刻,它就是 0。因为一向没有从新实行 effect,所以 setInterval 在闭包中运用的 count 一直是从第一次衬着时来的,所以就有了 count + 1 一直是 1 的征象。呵呵哒!

我觉得你已最先怼天怼地了。Hooks 是什么鬼嘛!

处置惩罚这个题目的一个计划,是把 setCount(count + 1) 替代成“更新回调”的体式格局 setCount(c => c + 1)。从回调参数中,可以猎取到最新的状况。此非万全之策,新的 props 就没法读取到。

另一个处置惩罚计划是运用 useReducer()。此计划更加天真。在 reducer 内部,可以接见当前的状况,以及最新的 props。dispatch 要领自身不会转变,所以你可以在闭包里往内里灌任何数据。运用 useReducer() 的一个限定是,你不能在内部触发 effects。(不过,你是可以经由历程返回一个新 state 来触发一些 effect)。

为什么云云困难?

阻抗不婚配

这个术语(译者注:术语原文为 “Impedance Mismatch”)在许多处所被人人运用,Phil Haack 是如许诠释的:

有人说数据库来自火星,对象来自金星。数据库不能自然的和对象模子竖立映照关联。这就像尝试将两块磁铁的 N 极挤在一同一样。

我们此处的“阻抗不婚配”,说的不是数据库和对象。而是 React 编程模子,与敕令式的 setInterval API 之间的不婚配。

一个 React 组件可以会被 mount 一段时刻,而且阅历多个差别的状况,不过它的 render 效果一次性地形貌了所有这些状况

// 形貌了每一次衬着的状况
return <h1>{count}</h1>

同理,Hooks 让我们声明式地运用一些 effect:

// 形貌每一个计数器的状况
useInterval(() => {
  setCount(count + 1);
}, isRunning ? delay : null);

我们不须要去设置计时器,然则指清楚明了它是不是应该被设置,以及设置的距离是多少。我们事前的 Hook 就是这么做的。经由历程离散的声明,我们形貌了一个一连的历程。

相对应的,setInterval 却没有形貌到全部历程 – 一旦你设置了计时器,它就没法转变了,只能消灭它。

这就是 React 模子和 setInterval API 之间的“阻抗不婚配”。

React 组件的 props 和 state 会变化时,都会被从新衬着,而且把之前的衬着效果“忘记”的一尘不染。两次衬着之间,是互不相关的。

useEffect() Hook 同样会“忘记”之前的效果。它清算上一个 effect 而且设置新的 effect。新的 effect 猎取到了新的 props 和 state。所以我们第一次的事前在某些简朴的情况下,是可以实行的。

然则 setInterval() 不会 “忘记”。 它会一向引用着旧的 props 和 state,除非把它换了。然则只要把它换了,就没法不从新设置时刻了。

等会,真的不能吗?

Refs 是救星!

先把题目整顿下:

  • 第一次衬着的时刻,运用 callback1 举行 setInterval(callback1, delay)
  • 下一次衬着的时刻,运用 callback2 可以接见到新的 props 和 state
  • 我们没法用 callback2 替代掉 callback1 然则又不重设想时器

假如我们压根不替代计时器,而是传入一个 savedCallback 变量,一直指向最新的计时器回调呢??

如今我们的计划看起来是如许的:

  • 设置计时器 setInterval(fn, delay),个中 fn 挪用 savedCallback
  • 第一次衬着,设置 savedCallbackcallback1
  • 第二次衬着,设置 savedCallbackcallback2
  • ???
  • 行了

可变的 savedCallback 须要在屡次衬着之间“耐久化”,所以不能运用通例变量。我们须要像相似实例字段的手腕。

从 Hooks 的 FAQ 中,我们得知 useRef() 可以帮我们做到这点:

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

(你可以已对 React 的 DOM refs 比较熟习了。Hooks 引用了雷同的观点,用于持有恣意可变的值。一个 ref 就行一个“盒子”,可以放东西进去。)

useRef() 返回了一个字面量,持有一个可变的 current 属性,在每一次衬着之间同享。我们可以把最新的计时器回调保留进去。

function callback() {
  // 可以读取到最新的 state 和 props
  setCount(count + 1);
}

// 每次衬着,保留最新的回调到 ref 中
useEffect(() => {
  savedCallback.current = callback;
});

后续就可以在计时器回调中挪用它了:

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

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

因为传入了 [],我们的 effect 不会从新实行,所以计时器不会被重置。另一方面,因为设置了 savedCallback ref,我们可以猎取到最后一次衬着时设置的回调,然后在计时器触发时挪用。

再看一遍完全的完成:

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

  function callback() {
    setCount(count + 1);
  }

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

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

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

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

(检察 CodeSandbox 线上示例)

提取为自定义 Hook

不能不认可,上面的代码有点迷。种种花狸狐哨的操纵让人费解不说,另有可以让 state 和 refs 与别的逻辑里的搞混。

我以为,虽然 Hooks 比拟 Class 供应了更底层的才能 – 不过 Hooks 的牛逼在于许可我们重组、笼统后创造出声明语意更优的 Hooks

事实上,我就想如许来写:

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

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

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

因而我把我的完成中心拷贝到自定义 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 是硬编码的,把它参数化:

function useInterval(callback, delay) {

在设置计时器的时刻运用:

let id = setInterval(tick, delay);

如今 delay 可以在屡次衬着之间变动,我须要把它声明为计时器 effect 的依靠:

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

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

慢着,我们之前不是为了防止计时器重设,才传入了一个 [] 的吗?不完满是。我们只是愿望 Hooks 不要在 callback 变动的从新实行。假如 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]);
}

(读者可以在 CodeSandbox 上试一下)

棒棒的!如今,我们可以无需关注完成细节,在任何组件内里须要的时刻,直接运用 useInterval() 了。

Bonus: 停息计时器

我们愿望在给 delaynull 的时刻停息计时器:

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

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

怎样完成?简朴:不设置计时器就可以了。

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

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

(CodeSandbox 线上示例)

就如许了。这段代码可以处置惩罚种种可以的变动了:延时价转变、停息和继承。虽然 useEffect() API 须要我们前期花更多的精神举行设置和清算事情,增加新才能倒是轻松了。

Bonus: 风趣的 Demo

这个 useInterval() Hook 实在很好玩。如今 side effects 是声明式的,所以组合运用变得轻松多了。

比方说,我们可以运用一个计时器来掌握另一个计时器的 delay:

《经由过程 React Hooks 声明式地运用 setInterval》自动加快的计时器

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

  // Increment the counter.
  useInterval(() => {
    setCount(count + 1);
  }, delay);

  // Make it faster every second!
  useInterval(() => {
    if (delay > 10) {
      setDelay(delay / 2);
    }
  }, 1000);

  function handleReset() {
    setDelay(1000);
  }

  return (
    <>
      <h1>Counter: {count}</h1>
      <h4>Delay: {delay}</h4>
      <button onClick={handleReset}>
        Reset delay
      </button>
    </>
  );
}

(CodeSandbox 线上示例)

总结

Hooks 须要我们逐步顺应 – 尤其是在面临敕令式和声明式代码的区分时。你可以创造出像 React Spring 一样壮大的声明式笼统,然则他们庞杂的用法偶然会让你慌张。

Hooks 还很年青,另有许多我们可以研讨和对照的形式。假如你习惯于根据“最好实践”来的话,大可不必焦急运用 Hooks。社区还需时刻来尝试和发掘更多的内容。

运用 Hooks 的时刻,涉及到相似 setInterval() 的 API,会遇到一些题目。浏览本文后,愿望读者可以明白而且处置惩罚它们,同时,经由历程建立越发语义化的声明式 API,享用其带来的优点。

此文已由腾讯云+社区在各渠道宣布

猎取更多新颖手艺干货,可以关注我们腾讯云手艺社区-云加社区官方号及知乎机构号

    原文作者:腾讯云加社区
    原文地址: https://segmentfault.com/a/1190000018224631
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞