Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

一文搞定常用的自定义 React Hooks #26

Open
fengshi123 opened this issue Jan 24, 2021 · 3 comments
Open

一文搞定常用的自定义 React Hooks #26

fengshi123 opened this issue Jan 24, 2021 · 3 comments

Comments

@fengshi123
Copy link
Owner

fengshi123 commented Jan 24, 2021

前言

通过上一篇文章《一文归纳 React Hooks 常用场景》,我们根据使用场景分别进行举例说明,帮助你认识理解并可以熟练运用 React Hooks 大部分特性了。本文则对 hooks 进一步加深,让我们通过自定义一些 hooks,解决我们在平时项目中非常常用的需求场景,做到代码高复用低耦合,从而加深对 hooks 的理解和运用。

辛苦整理良久,还望手动点赞鼓励~
博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

1、实现自定义的 useMount

首先我们自定义一个 useMount hook,其功能为在 Dom 渲染之后执行相关函数,即类似于 class 组件写法中的 componentDidMount 生命周期钩子的功能。
我么基于以下原理实现:如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。如果在函数组件中实现该功能,即代码如下所示

useEffect(() => {
  console.log('mount');
}, []);

现在我们将这个功能进行抽取,封装成为 useMount hook,则可以如下实现,其中该钩子支持传入一个回调执行函数 fn 作为参数。

import { useEffect } from 'react';

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn();
  }, []);
};

export default useMount;

现在我们就可以在相关业务场景中使用这个 useMount hook 了,如下所示,只会在 MyPage 初次渲染时执行一次 fun,即使我们多次点击 button,使 count 不断增加,页面不断更新,也不会再执行 fun。

import React, { useCallback, useState } from 'react';
import useMount from './useMount';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const fun = useCallback(() => {
    console.log('mount');
  }, []);

  useMount(fun);

  return (
    <div >
      <button type="button" onClick={() => { setCount(count + 1); }}>
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

2、实现自定义的 useUnmount

本节我们自定义一个 useUnmount hook,其功能为在 Dom 卸载之前执行相关函数,即类似于 class 组件写法中的 componentWillUnmount 生命周期钩子的功能。
我么基于以下原理实现:如果 effect 有返回一个函数,React 将会在执行清除操作时调用它。如果在函数组件中实现该功能,即代码如下所示

useEffect(() => () => {
  console.log('unmount');
});

现在我们将这个功能进行抽取,封装成为 useUnmount hook,则可以如下实现,其中该钩子支持传入一个回调执行函数 fn 作为参数。

import { useEffect } from 'react';

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    fn();
  }, []);
};

export default useUnmount;

现在我们就可以在相关业务场景中使用这个 useUnmount hook 了,如下所示,只会在 MyComponet 卸载时执行一次 fun。

import React, { useCallback, useState } from 'react';
import useUnmount from './useUnmount';

const MyComponent = () => {
  const fun = useCallback(() => {
    console.log('unmount');
  }, []);

  useUnmount(fun);

  return <div>Hello World</div>;
};


const MyPage = () => {
  const [state, setState] = useState(true);

  return (
    <div >
      {state && <MyComponent />}
      <button type="button" onClick={() => { setState(!state); }}>
        切换
      </button>
    </div>
  );
};

export default MyPage;

3、实现自定义的 useUpdate

我们都知道如果想让 function 组件重新渲染,我们不得不更新 state,但是有时候业务需要的 state 是没必要更新的,我们不能仅仅为了让组件会重新渲染而强制让一个 state 做无意义的更新,所以这个时候我们就可以自定义一个更新的 hook 来优雅的实现组件的强制更新,类似于 class 组件的 forceUpdate 的功能,实现代码如下

import { useCallback, useState } from 'react';

const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

export default useUpdate;

useUpdate 的使用实例如下所示,点击按钮时,调用 update,会看到 Time 的值在变化,说明组件已经强制更新了。

import React from 'react';
import useUpdate from './useUpdate';

const MyPage = () => {
  const update = useUpdate();

  return (
    <div >
      <button type="button" onClick={update}>
      Time: {Date.now()}
      </button>
    </div>
  );
};

export default MyPage;

4、实现自定义的 usePrevious

平时在实现需求时,经常需要保存上一次渲染时 state 的值,so 这个 hook 就是用来保存上一次渲染状态的。如下所示为实现逻辑,主要用到 useRef.current 来存放变量。

import { useRef } from 'react';

function usePrevious<T> (state: T): T|undefined {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  prevRef.current = curRef.current;
  curRef.current = state;

  return prevRef.current;
}

export default usePrevious;

usePrevious 的使用实例如下所示,当点击按钮使 count 增加时,previous 会保留 count 的上一个值。

import React, { useState } from 'react';
import usePrevious from './usePrevious';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const previous = usePrevious(count);

  return (
    <div >
      <div>新值:{count}</div>
      <div>旧值:{previous}</div>
      <button type="button" onClick={() => { setCount(count + 1); }}>
        增加
      </button>
    </div>
  );
};

export default MyPage;

5、实现自定义的 useTimeout

在 hook 中,我们使用 setTimeout 之后,需要在 dom 卸载时,手动进行 clearTimeout 将定时器移除,否则可能造成内存泄漏。假设我们在项目中多次用到,那我们则需要多次重复写移除代码,并且有时候可能由于疏忽,将其遗忘。so,为什么不能将它封装成 hook,在需要的时候调用即可。

import { useEffect } from 'react';

function useTimeout (fn: () => void, delay: number) {
  useEffect(() => {
    const timer = setTimeout(() => {
      fn();
    }, delay);
    return () => {
      clearTimeout(timer); // 移除定时器
    };
  }, [delay]);
}

export default useTimeout;

如下所示,我们只需要告诉 useTimeout 多少毫秒去调用哪个方法,不需要再去考虑移除定时器的事情了。

import React, { useState } from 'react';
import useTimeout from './useTimeout';

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

  useTimeout(() => {
    setCount(count => count + 1);
  }, 3000);

  return (
    <div >
      <button type="button">
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

6、实现自定义的 useInterval

useInterval 封装 setInterval 功能,其原因和用法跟 useTimeout 一样,这里不再赘述。

import { useEffect } from 'react';

function useInterval (fn: () => void, delay: number) {
  useEffect(() => {
    const timer = setInterval(() => {
      fn();
    }, delay);
    return () => {
      clearInterval(timer); // 移除定时器
    };
  }, [delay]);
}

export default useInterval;

7、实现自定义的 useDebounce

防抖在我们日常开发中是非常常见的,比如:按钮点击、文本编辑保存等,为防止用户过于频繁操作,需要进行防抖处理。**防抖的定义:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时间,才执行代码一次。**类比于生活中的场景就例如坐公交,在一定时间内,如果有乘客陆续刷卡上车,司机就不会开车,当乘客没有刷卡了,司机才开车。
防抖功能的基本实现和相关注释如下所示

function debounce(fn,wait){
    let timeout1;
    return function(){
        clearTimeout(timeout1);  // 重新清零
        let context = this;  // 保存上下文
        let args = arguments; // 获取传入的参数
        timeout1 = setTimeout(()=> {
            fn.apply(context, args);
        },wait)
    }
}

我们将以上的实现用 hooks 自定义的方式来写,useDebounce hook 相关代码如下,其中传入的两个参数为:fn(要执行的回调方法)和 delay(防抖时间),然后该 hook 返回一个执行方法

import { useCallback, useRef } from 'react';

const useDebounce = (fn: Function, delay = 100) => {
  const time1 = useRef<any>();

  return useCallback((...args) => {
    if (time1.current) {
      clearTimeout(time1.current);
    }
    time1.current = setTimeout(() => {
      fn(...args);
    }, delay);
  }, [delay]);
};

export default useDebounce;

现在我们就可以在相关业务场景中使用这个 useDebounce hook 了,如下所示,我们不断点击 button,count 也不会增加,只有点击间隔超过 3000ms,count 数才会增加。

import React, { useCallback, useState } from 'react';
import useDebounce from './useDebounce';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const fun = useCallback(() => {
    setCount(count => count + 1);
  }, []);

  const run = useDebounce(fun, 3000);

  return (
    <div >
      <button type="button" onClick={() => { run(); }}>
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

8、实现自定义的 useThrottle

节流在我们日常开发中是非常常见的,比如:滚动条监听、图片放大镜效果功能等,我们不必每次鼠标滚动都触发,这样可以降低计算的频率,而不必去浪费资源。节流的定义:函数节流是指一定时间内 js 方法只跑一次。类比于生活中的场景就例如人眨眼睛,就是一定时间内眨一次。
节流功能的基本实现和相关注释如下所示,跟防抖很类似

function throttle(fn, wait){
  let timeout;
  return function(){
      if(timeout) return; // 如果已经触发,则不再触发
      let args = arguments;
      let context = this;
      timeout = setTimeout(()=>{
        fn.apply(context,args); // 执行
        timeout = null; // 执行后,将标志设置为未触发
      },wait)
  }
}

我们将以上的实现用 hooks 自定义的方式来写,useThrottle hook 相关代码如下,其中传入的两个参数为:fn(要执行的回调方法)和 delay(节流时间),然后该 hook 返回一个执行方法

import { useCallback, useRef } from 'react';

const useThrottle = (fn: Function, delay = 100) => {
  const time1 = useRef<any>();

  return useCallback((...args) => {
    if (time1.current) {
      return;
    }
    time1.current = setTimeout(() => {
      fn(...args);
      time1.current = null;
    }, delay);
  }, [delay]);
};

export default useThrottle;

现在我们就可以在相关业务场景中使用这个 useThrottle hook 了,如下所示,我们不断点击 button,count 只会在连续间隔 3000ms 增加一次,不会每次点击都会增加一次。

import React, { useCallback, useState } from 'react';
import useThrottle from './useThrottle';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const fun = useCallback(() => {
    setCount(count => count + 1);
  }, []);

  const run = useThrottle(fun, 3000);

  return (
    <div >
      <button type="button" onClick={() => { run(); }}>
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

总结

本文是 react hooks 三部曲中的第二篇,按照预期,后续我们会写 react hooks 三部曲中的第三篇,敬请期待。

辛苦整理良久,还望手动点赞鼓励~
博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

@alexischiang
Copy link

useUnmount 处的代码为啥不是这样写呢

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return fn();
  }, []);
};

@iamjoel
Copy link

iamjoel commented Mar 8, 2021

useUnmount 处的代码为啥不是这样写呢

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return fn();
  }, []);
};

作者写错了~

@LiuZhenyan-Wuzhong
Copy link

useUnmount 处的代码为啥不是这样写呢

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return fn();
  }, []);
};

这样写也戳啦,应该是酱紫啦

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return ()=>fn(); // 或者 return fn
  }, []);
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants