如何实现一个Interval Hook

本文深入探讨React中自定义IntervalHook的实现原理,解决传统setInterval在React环境下的问题,如动态延迟和暂停功能,以及如何利用Refs保持状态最新,避免闭包问题。通过对比class component和function component,展示Hooks的灵活性和优势。

【React】如何实现一个Interval Hook

useInterval(() => {
  // do something...
}, 1000);
复制代码

可能你看过也写过一些 react hook ,不过你对 hook 的种种行为真的了解吗?这篇文章为你剖析 hook 对比 class component “反常” 的那些事儿。

有了自带的 setInterval 为何还要再实现一个

its arguments are “dynamic”

可以注意到我们的 setInterval 是接受一个 dealy 值的, 并且这个值是可以由我们的代码控制的, 这意味着我们可以随时调整这个值来做动态的改变.

可以做到这样: 用一个 interval 控制另一个 interval 的速度

class component 实现

第一次尝试

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  });
  return <h1>{count}</h1>;
}
复制代码

我们一开始一般会写出这样的实现, useEffect 设置 interval, return cleanup. 然而这样写会有个奇怪的表现...

react 在默认在每次 render 之后会重新执行 effects, 这其实也是 react 所预期的, 因为这样能避免 a whole class of bugs.

我们通常会使用 effect 来订阅, 退订一些 api, 但是在 setInterval 上使用的时候就会有问题, 因为执行 clearIntervalsetInterval 是有时间差的, 当 react 渲染过于频繁的时候, 就会出现 interval 压根没机会执行的情况!

我们以 100ms 的频率去渲染 counter 组件,我们会发现 count 值一直没有更新

第二次尝试

在上一个阶段中, 我们的问题是重复执行 effects 导致了 interval 被清理的太早.

我们知道 useEffect 可以传入一个参数来决定是否重复执行 effects, 试一下

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

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

  return <h1>{count}</h1>;
}
复制代码

好的, 现在我们 counter 更新到 1 就停止了

发生了什么?!

这其实是个很常见的闭包问题, 也有了对应的 lint.

我们的 effects 现在只会运行一次, 所以 effects 每次捕获的 count 值都是第一次 render 的 count 值(0), 所以 count + 1 一直是 1

有一种 fix 的方式是, 用 setState 的函数参数, setCount(count => count + 1), 这样我们就可以读取最新的 state, 但是这种方式不是万能的, 比如不能读取最新的 props, 那么假如我们需要根据最新的 props 来 setState 就无法实现了

使用 Refs

我们回到上个问题, count 无法被正确读取的原因是 count 的值一直引用的是第一次 render 的.

那如果我们在每次 render 的时候动态地改变 setInterval(fn, delay) 中 fn 函数, 使这个函数带上最新的 props 和 state, 并且这个 fn 函数要能在多次 render 之间可持续(persist), 这样 setInterval 执行的时候, 就可以实时的读取这个函数拿到最新的值了

第一版实现:

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

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

  useEffect(() => {
    const tick = () => savedCallback.current();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
}
复制代码

支持动态 delay暂停 的最终版:

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

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

  useEffect(() => {
    const tick = () => savedCallback.current();
    if (delay !== undefined) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}
复制代码

我们可以用这个 hook 做一些更加好玩的事 -- 用一个 interval 控制另一个 interval 的速度,就是一开始我们看到的那个动图的样子。

练习

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      console.log(`Clicked ${count} times`);
    }, 3000);
  });

  return [
    <h1>{count}</h1>,
    <button onClick={() => setCount(count + 1)}>
      click me
    </button>
  ];
}
复制代码

猜猜打印结果?

看看 class component 的表现如何?

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

  componentDidUpdate() {
    setTimeout(() => {
      console.log(`Clicked ${this.state.count} times`);
    }, 3000);
  }

  render() {
    const { count } = this.state;
    return [
      <h1>{count}</h1>,
      <button onClick={() => this.setState({ count: count + 1 })}>
        click me
      </button>
    ];
  }
}
复制代码

如何改造上面的 class component 让它跟使用 hook 的组件一样打印不同值?

hook 版本的怎么改能变得跟之前的 class component 一样打印相同值呢?

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

  useEffect(() => {
    saved.current = count;
    setTimeout(() => {
      console.log(`Clicked ${saved.current} times`);
    }, 3000);
  });

  return [
    <h1>{count}</h1>,
    <button onClick={() => setCount(count + 1)}>
      click me
    </button>
  ];
}
复制代码

专栏其他文章

FE One
关注我们的公众号FE One,会不定期分享JS函数式编程、深入Reaction、Rxjs、工程化、WebGL、中后台构建等前端知识

参考: overreacted.io/making-seti…

转载于:https://juejin.im/post/5cad6809f265da039955c112

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值