使用 React Hooks 声明 setInterval

来自Dan 写的一篇文章:
https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/
建议读原文

关键的一段代码:

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

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

  // 保存新回调
  useEffect(() => {
    savedCallback.current = callback;
  });

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

useInterval Hook 内置了一个 interval 并在 unmounting 的时候清除,它是一个作用在组件生命周期里的 setInterval 和 clearInterval 的组合。

以下为读完后我的一点记录,需要解决以下几个问题:
回答这些问题之前,需要确定下, setInterval 的逻辑是应该写在 useEffect 中的,而不是写在函数组件的第一层作用域内,这是因为?这是第一个问题

1.为什么最简单的写法有问题
2.为什么useInterval 的写法可以
写在useEffect 后,可以确认的是 setInterval 内部使用的 state 和props 一直是旧的,不会更新,像是下面这段代码:

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

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

  return 

{count}

; }

出来的count 一直是1 ,不会再更新,这是因为最初渲染的时候 count 是 0, setInterval 一直用的是 0 的 count .查看例子
修复它的一种方法是用像 setCount(c => c + 1) 这样的 「updater」替换 setCount(count + 1),这样可以读到新 state 变量。但这个无法帮助你获取到新的 props。

另一个方法是用 useReducer()。这种方法为你提供了更大的灵活性。在 reducer 中,你可以访问到当前 state 和新的 props。dispatch 方法本身永远不会改变,所以你可以从任何闭包中将数据放入其中。useReducer() 有个约束是你不可以用它执行副作用。(但是,你可以返回新状态 —— 触发一些 effect。)
setInterval 没有及时地描述过程 —— 一旦设定了 interval,除了清除它,你无法对它做任何改变

3.setInterval 和 useInterval 的区别:
useInterval 中使用 refs 来解决这个问题的,首先要想想要解决什么问题,上面这个问题,想要的其实是能够在 state 或者 props 变化的时候,interval 的回调也能够看到,但是 interval 呢,声明了它之后呢,是无法在不改变 delay 时间的情况下替换原来的 interval 的, 但是可以引入指向新 interval 回调的可变savedCallback,让 interval 的回调变化。

  • 我们调用 setInterval(fn, delay),其中 fn 调用 savedCallback。
  • 第一次渲染后将 savedCallback 设为 callback1。
  • 下一次渲染后将 savedCallback 设为 callback2
    这个可变的 savedCallback 需要在重新渲染时「可持续(persist)」,所以不可以是一个常规变量,我们想要一个类似实例的字段。
    就是说 在一个生命周期中 savedCallback 这个变量是不变的,
    useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以

形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

然而,useRef()ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

换一种表达方式 useRef 在 react hook 中的作用, 正如官网说的, 它像一个变量, 它就像一个盒子, 你可以存放任何东西. createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。

这个 createRef 又是何物?它和 useRef 又有什么区别呢?https://stackoverflow.com/questions/54620698/whats-the-difference-between-useref-and-createref

这个 ref 对象类似一个 class 的实例属性,为什么这里需要一个实例属性,这个实例属性与 直接声明一个变量的区别在哪里?(类似于this)
为的是 在组件的生命周期中 这个变量是不变的,变量的变化也不会引起组件的重新渲染。

下面描述是创建实例对象的时候
使用new命令时,它后面的函数依次执行下面的步骤。

1.创建一个空对象,作为将要返回的对象实例。
2.将这个空对象的原型,指向构造函数的prototype属性。
3.将这个空对象赋值给函数内部的this关键字。
4.开始执行构造函数内部的代码。

实例对象与创建普通对象的区别?
实例对象有上面的 4个步骤, 一般的通过字面量声明的对象 没有

为什么使用 ref 来解决这个问题呢?
因为 ref 返回的对象在组件的生命周期内是不变化的,这个对象的属性的变化也不会引起组件的重新渲染,这很重要,是为了保存变化的回调函数,并且在回调函数变化的时候,组件不要重新渲染。

要解决的这个问题的关键点在于:

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

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

  return 

{count}

; }

实例在这里 https://codesandbox.io/s/jj0mk6y683?file=/src/index.js

现在的这种写法,的执行结果 会是 0, 1,1,1,1 并且 id 始终是一个
为什么会这样呢?

// 第一次执行
count = 0;

0

useEffect (() => { // count 为 0 })

useEffect 中执行的东西是 声明一个 interval 这个interval 在1 s之后 设置 count 为 1,于是时间很快滴 到 1s 了,当前 count 为 0, 设置 count = 0 + 1, ok 完成了, 很开心吧,现在 count 变成 1 啦,这个interval 还要继续执行的,时间很快又 1s 了,因为 useEffect 只能执行1 次呀, 它记住的只是它那次渲染的 count, interval 的回调中看到的 count 还是 0 呀, 它又要完成 将 count = 0 +1 的工作,于是 count 又变成了 1。

所以,这个问题的点在于,count 实际上已经更新了,但是 useEffect 中的 interval 中的回调中是捕获不到这个更新的,那就想着可以在每次有更新的时候,引用能看到更新的那个回调吧, 但是在回调发生变化的时候,这个 interval id 它是不可以变的, 因为这样也更合理,回调变化时为了拿到更新的值,但是 delay 时间如果变化,那就是另外一个 interval 了,这两还是不一样的。

于是 自然地,因为 ref 返回的对象,可以由一个变化的 current, 让这个curret 属性指向新变化的可以拿到 更新过的 state/ props 的回调函数,然后将 interval 的回调设置为 这个 ref 对象的 current 属性,由于 ref 对象的特殊性,他的current 属性变化的时候,不会引起组件的重新渲染。完美地解决了这个问题。

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 

{count}

; }

大神还提取个useHook

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

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

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

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

简直完美,最后 delay 以参数形式表示,

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

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

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

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

简直完美,在 delay 变化的时候,重置 interval, 本来就应该这样。

这个代码:

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

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

  return (
    

You clicked {count} times

); }

补充下, useEffect hooks 函数组件的执行过程
React: 给我状态为 0时候的UI。
你的组件:

  • 给你需要渲染的内容:

    You clicked 0 times

  • 记得在渲染完了之后调用这个effect: () => { document.title = 'You clicked 0 times' }。
  • React: 没问题。开始更新UI,喂浏览器,我要给DOM添加一些东西。
  • 浏览器: 酷,我已经把它绘制到屏幕上了。
  • React: 好的, 我现在开始运行给我的effect

运行 () => { document.title = 'You clicked 0 times' }。

现在我们回顾一下我们点击之后发生了什么:

  • 你的组件: 喂 React, 把我的状态设置为1。

  • React: 给我状态为 1时候的UI。
    你的组件:

  • 给你需要渲染的内容:

    You clicked 1 times

  • 记得在渲染完了之后调用这个effect: () => { document.title = 'You clicked 1 times' }。

  • React: 没问题。开始更新UI,喂浏览器,我修改了DOM。

  • Browser: 酷,我已经将更改绘制到屏幕上了。

  • React: 好的, 我现在开始运行属于这次渲染的effect

  • 运行 () => { document.title = 'You clicked 1 times' }。

先根据 initial state 渲染出 DOM 节点, return 的部分
执行 useEffect 的部分 这里面可能要 发生更新
更新 DOM 就是 return 的地方
useEffect 中的return 的地方 清理旧的
执行新的 useEffect 

React 渲染{id: 20}的UI。
浏览器绘制。我们在屏幕上看到{id: 20}的UI。
React 清除{id: 10}的effect。
React 运行{id: 20}的effect。

4.Dan的写法还可以带来哪些新特性
暂停interval
加速interval
5.总结一下对自己写代码有什么启发

react 的模型
react hooks 执行过程是怎样
useEffect 中通常执行哪些代码(请求数据)
useEffect 中执行 setInteval 清除 interval
编写自定义 hooks

文章中一开始就提到了 React 编程模型,用听得懂的话描述就是

你可能感兴趣的:(使用 React Hooks 声明 setInterval)