如何构建自己的 react hooks
侧边栏壁纸
  • 累计撰写 1,061 篇文章
  • 累计收到 0 条评论

如何构建自己的 react hooks

加速器之家
2024-10-20 / 0 评论 / 2 阅读 / 正在检测是否收录...

我们组的前端妹子在组内分享时谈到了 react 的钩子,趁此机会我也对我所理解的内容进行下总结,方便更多的同学了解。在 React 的 v16.8.0 版本里添加了 hooks 的这种新的 API,我们非常有必要了解下他的使用方法,并能够结合我们的业务编写几个自定义的 hooks。

1. 常用的一个 hooks #

官方中提供了几个内置的钩子,我们简单了解下他们的用法。

1.1 useState: 状态钩子 #

需要更新页面状态的数据,我们可以把他放到 useState 的钩子里。例如点击按钮一下,数据加 1 的操作:

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

return (
  <>
<p>{count}p>

<button onClick={() => setCount(count + 1)}> add 1 button>
);

在 typescript 的体系中,count 的类型,默认就是当前初始值的类型,例如上面例子中的变量就是 number 类型。如果我们想自定义这个变量的类型,可以在 useState 后面进行定义:

const [count, setCount] = (useState < number) | (null > null); // 变量count为number类型或者null类型

同时,使用 useState 改变状态时,是整个把 state 替换掉的,因此,若状态变量是个 object 类型的数据,我只想修改其中的某个字段,在之前 class 组件内调用 setState 时,他内部会自动合并数据。

class Home extends React.Component {
  state = {
    name: "wenzi",
    age: 20,
    score: 89,
  };

  update() {
    this.setState({
      score: 98,
    }); // 内部自动合并
  }
}

但在 function 组件内使用 useState 时,需要自己先合并数据,然后再调用方法,否则会造成字段的丢失。

const [person, setPerson] = useState({
    name: 'wenzi',
    age: 20,
    score: 89
});

setPerson({
    ...person,
    {
        score: 98
    }
}); // 先合并数据 { name: 'wenzi', age: 20, score: 98 }
setPerson({
    score: 98
}); // 仅传入要修改的字段,后name和age字段丢失

1.2 useEffect: 副作用钩子 #

useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect 钩子在组件初始化完毕时,一定会执行一次,在组件重新渲染的过程中,是否还要 update,还要看传入的第 2 个参数。

  1. 当只有回调函数这一个参数时,组件的每次更新,回调都会执行;
  2. 当有 2 个参数时,只有第 2 参数里的数据发生变化时,回调才执行;
  3. 只想在组件初始化完毕时只执行一次,第 2 个参数可以传入一个空的数组;

我们可以看下这个例子,无论点击 add按钮 还是 settime按钮 ,useEffect 的回调都会执行:

const Home = () => {
  const [count, setCount] = useState(0);
  const [nowtime, setNowtime] = useState(0);

  useEffect(() => {
    console.log("count", count);
    console.log("nowtime", nowtime);
  });

  return (
    <>
<p>count: {count} p>

<p>nowtime: {nowtime} p>
<button onClick={() => setCount(count + 1)}> add 1 button>
<button onClick={() => setNowtime(Date.now())}> set now time button>
); };

若改成下面的这样,回调仅会在 count 发生变化时才会在控制台输出,仅修改 nowtime 的值时没有输出:

useEffect(() => {
  console.log("count", count);
  console.log("nowtime", nowtime);
}, [count]);

useEffect 的回调函数还可以返回一个函数,这个函数在 effect 生命周期结束之前调用。为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除

基于上面的代码,我们稍微修改一下:

useEffect(() => {
  console.log("count", count);
  console.log("nowtime", nowtime);

  return () => console.log("effect callback will be cleared");
}, [count]);

蚊子的前端博客

基于这个机制,在一些存在添加绑定和取消绑定的案例上特别合适,例如监听页面的窗口大小变化、设置定时器、与后端的 websocket 接口建立连接和断开连接等,都可以预计 useEffect 进行二次的封装,形成自定义的 hook。关于自定义 hook,下面我们会讲到。

1.3 useMemo 和 useCallback #

function 组件中定义的变量和方法,在组件重新渲染时,都会重新重新进行计算,例如下面的这个例子:

const Home = () => {
  const [count, setCount] = useState(0);
  const [nowtime, setNowtime] = useState(0);

  const getSum = () => {
    const sum = ((1 + count) * count) / 2;
    return sum + " , " + Math.random(); // 这个random是为了看到区别
  };

  return (
    <>
<p> count: {count}p>

<p> sum: {getSum()}p>
<p> nowtime: {nowtime}p>
<button onClick={() => setCount(count + 1)}> add 1 button>
<button onClick={() => setNowtime(Date.now())}> set now time button>
); };

这里有 2 个按钮,一个是 count+1,一个设置当前的时间戳, getSun() 方法是计算从 1 到 count 的和,我们每次点击 add 按钮后,sum 方法都会重新计算和。可是当我们点击 settime 按钮时,getSum 方法也会重新计算,这是没有必要的。

这里我们可以使用 useMemo 来修改下:

const sum = useMemo(
  () => ((1 + count) * count) / 2 + " , " + Math.random(),
  [count]
);

<p> {sum} p>;

点击查看样例,修改后就可以看到,sum 的值只有在 count 发生变化的时候才重新计算,当点击 settime 按钮的时候,sum 并没有重新计算。这要得益于 useMemo 钩子的特性:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 返回回调里 return 的值,而且 memoizedValue 它仅会在某个依赖项改变时才重新计算。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

在上面的例子里,只有 count 变量发生变化时,才重新计算 sum,否则 sum 的值保持不变。

useCallback 与 useMemo 类型,只不过 useCallback 返回的是一个函数,例如:

const fn = useCallback(() => {
  return ((1 + count) * count) / 2 + " , " + nowtime;
}, [count]);

2. 实现几个自定义的 hook #

在官方文档里,实现了好友的在线与离线功能。这里我们自己也学着实现几个 hook。

2.1 获取窗口变化的宽高 #

我们通过监听resize事件来获取实时获取 window 窗口的宽高,对这个方法进行封装后可以在生命周期结束前能自动解绑 resize 事件:

const useWinResize = () => {
  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight,
  });
  const resize = useCallback(() => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
    });
  }, []);
  useEffect(() => {
    window.addEventListener("resize", resize);
    return () => window.removeEventListener("resize", resize);
  }, []);
  return size;
};

使用起来也非常方便:

const Home = () => {
  const { width, height } = useWinResize();

  return (
    <div>
<p>width: {width}p>

<p>height: {height}p>
div> ); };

点击链接useWinResize 的使用可以查看 demo 演示。

2.2 定时器 useInterval #

在前端中使用定时器时,通常要在组件生命周期结束前清除定时器,如果定时器的周期发生变化了,还要先清除定时器再重新按照新的周期来启动。这种最常用的场景就是九宫格抽奖,用户点击开始抽奖后,先缓慢启动,然后逐渐变快,接口返回中奖结果后,再开始减速,最后停止。

我们很容易想到用 useEffect 来实现这样的一个 hook(举一个错误的例子):

const useInterval = (callback, delay) => {
  useEffect(() => {
    if (delay !== null) {
      let id = setInterval(callback, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};

我们把这段代码用到项目中试试:

const Home = () => {
  const [count, setCount] = useState(0);

  useInterval(() => {
    console.log(count);
    setCount(count + 1);
  }, 500);

  return <div> {count} div>;
};

可是这段运行后很奇怪,页面从 0 到 1 后,就再也不变了, console.log(count) 的输出表明代码并没有卡死,那么问题出在哪儿了?

React 组件中的 props 和 state 是可以改变的, React 会重渲染它们且「丢弃」任何关于上一次渲染的结果,它们之间不再有相关性。

useEffect() Hook 也「丢弃」上一次渲染结果,它会清除上一次 effect 再建立下一个 effect,下一个 effect 锁住新的 props 和 state,这也是我们第一次尝试简单示例可以正确工作的原因。

但 setInterval 不会「丢弃」。 它会一直引用老的 props 和 state 直到你把它换掉 —— 不重置时间你是无法做到的。

这里就要用到useRef这个 hook 了,我们把 callback 存储到 ref 中,当 callback 更新时去更新 ref.current 的值:

const useInterval = (callback, delay) => {
  const saveCallback = useRef();

  useEffect(() => {
    // 每次渲染后,保存新的回调到我们的 ref 里
    saveCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      saveCallback.current();
    }
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};

当我们使用新的 useInterval 时,发现就可以自增了,点击查看样例useInterval 的简单使用

这里我们使用一个变量来控制增加的速度:

const [count, setCount] = useState(0);
const [diff, setDiff] = useState(500);

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

return (
  <div>
<p> count: {count} p>

<p> diff: {diff}ms p>
<p>
<button onClick={() => setDiff(diff - 50)}> 加快50ms button>
<button onClick={() => setDiff(diff + 50)}> 减慢50ms button>
p>
div> );

分别点击两个按钮,可以调整 count 增加的速度。当想要停止定时器时,将 diff 设置为 null 即可(setDiff(null))。重新设置为数字时,定时器则重新启动。

3. 总结 #

使用 react hook 可以做很多有意思的事情,这里我们也仅仅是举几个简单的例子,后续我们也会更加深入了解 hook 的原理。

0

评论

博主关闭了当前页面的评论