ahooks中usePrevious、useTimeout、useInterval的使用与实现

3,362 阅读4分钟

个人博客:github.com/BokFang/Blo…

觉得有收获的话,star支持一哈

ahooks是由阿里团队开发的一个React Hooks 库,里面有很多的常用且高质量的hooks,其中的设计实现也是值得我们学习的。今天我就挑几个比较常用的hooks,看看他们是如何使用与实现的。

ahooks的使用

上手还是非常简单的,只需要安装依赖,然后使用import导入就行了。

// 安装依赖
npm i ahooks --save

// 使用 Hooks
import { useRequest } from 'ahooks';

usePrevious

使用

这个hook的作用是可以保存上一次渲染时的状态。他传入一个需要记录变化的值,返回他的上一轮的状态,记录的值初始为undefined。

看看他具体怎么使用:

import React, {useState} from "react";
import ReactDOM from "react-dom";
import { usePrevious } from 'ahooks';

function App () {
  const [count, setCount] = useState(0);
  const previous = usePrevious(count);
  return (
    <>
      <div>当前值: {count}</div>
      <div style={{ marginBottom: 8 }}>上一轮的值: {previous}</div>
      <button type="button" onClick={() => setCount((c) => c + 1)}>
        增加
      </button>
      <button type="button" style={{ marginLeft: 8 }} onClick={() => setCount((c) => c - 1)}>
        减少  
      </button>
    </>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));

image.png

点击增加一下,可以看到当前值从0变为1,同时将上一轮的值(0)给记录了下来。

实现

思路

  • 要保存上一轮的状态,我们就需要一个存状态的容器。React官方提供的useRef就是一个很好的选择;
  • 使用useRef的current属性保存上一轮的值,并配合useEffect一起使用,当数据发生变化时,更新current的值;
  • 将current值返回;

实现如下:

function usePrevious(value) {
  const pre = useRef();
  const cur = useRef();
  
  useEffect(()=>{
    pre.current = cur.current;
    cur.current = value;
  }, [value]);
  return pre.current;
}

useTimeout

我们先来看一个关于setTimeout的例子:

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [state, setState] = useState(1);
  setTimeout(() => {
    setState(state + 1);
  }, 3000);

  return (
    <div>
      <p style={{ marginTop: 16 }}> {state} </p>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

我们原本的目的是在页面渲染完3s后修改一下state,但是你会发现当state+1后,触发了页面的重新渲染,就会重新有一个3s的定时器出现来给state+1,既而变成了每3秒+1。

我们再来看setTimeout的另一个例子:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [count, setCount] = useState(0)
  const [countInTimeout, setCountInTimeout] = useState(0)

  useEffect(() => {
      setTimeout(() => {
          setCountInTimeout(count)
      }, 3000)
      setCount(5)
  }, [])

  return (
      <div>
      Count: {count}
      <br />
      setTimeout Count: {countInTimeout}
      </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

执行结果如下:

image.png

可以看到,count发生了变化,但是3s后setTimout的count却还是0。这就是hooks的闭包陷阱。

既然setTimeout在react中存在着一些问题,那么整一个useTimeout的hook出来就很有必要了。ahooks也提供了对应的hook,使用如下。

使用

这个hook的作用是可以处理 setTimeout 计时器函数。使用例子如下:

例子1(对应setTimeout第一个例子)

import React, { useState } from 'react';
import { useTimeout } from 'ahooks';

function App() {
  const [state, setState] = useState(1);
  useTimeout(() => {
    setState(state + 1);
  }, 3000);

  return (
    <div>
      <p style={{ marginTop: 16 }}> {state} </p>
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

例子2(对应setTimeout第二个例子)

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [count, setCount] = React.useState(0);
  const [countInTimeout, setCountInTimeout] = React.useState(0);

  useTimeout(() => {
    setCountInTimeout(count);
  }, 3000);

  useEffect(() => {
    setCount(5);
  }, []);

  return (
    <div>
      Count: {count}
      <br />
      setTimeout Count: {countInTimeout}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("root"));

接下来我们就来实现这个钩子。

实现

  • 这个hook传入两个参数,一个为执行的回调,一个为延迟的时间。为了防止组件一刷新就生成一个新的定时器,我们用useEffect包着,把延迟时间作为依赖项,并使用clearTimeout清除副作用;
function useTimeout(callback, delay) {
  useEffect(() => {
    if (delay !== null) {
      const timer = setTimeout(() => {
        callback();
      }, delay);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [delay]);
}
  • 对于例子2的闭包陷阱,实际上是因为定时器中的回调函数被引用了,形成了闭包被一直保存着,当调用setState后虽然组件重新渲染,但是setTimeout还是上一轮的,所以拿到的state就还是上一轮的1,拿不到最新的值;
  • 我们可以使用useRef来保存setTimeout的回调函数,那么在setState后,组件重新渲染,定时器中的回调也会更新,就可以拿到最新的值了。我们把上面的版本修改一下:
function useTimeout(callback, delay) {
  const memorizeCallback = useRef();

  useEffect(() => {
    memorizeCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay !== null) {
      const timer = setTimeout(() => {
        memorizeCallback.current();
      }, delay);
      return () => {
        clearTimeout(timer);
      };
    }
  }, [delay]);
};

useInterval

setInterval在react中的问题和setTimeout是类似的,就如setTimeout的两个例子,setInterval也会出现同类的问题。所以useInterval的实现思路就和useTimeout类似,只需要把setTimeout换成setInterval就行了:

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

  useEffect(() => {
    memorizeCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay !== null) {
      const timer = setInterval(() => {
        memorizeCallback.current();
      }, delay);
      return () => {
        clearInterval(timer);
      };
    }
  }, [delay]);
};