likes
comments
collection

Hooks 技巧 | useEffect 依赖项数组的不同场景总结 & 一组导入操作的小技巧

作者站长头像
站长
· 阅读数 13

本文正在参加「金石计划 . 瓜分6万现金大奖」

文章阅读小提示,本文字数:3400左右,阅读完需:约 11 分钟

前情提要

我的第一篇关于「React Hooks」的文章,回答了第一个问题:

问:新特性出来就一定要用吗?

答:有些新出现的技术特性不一定适合我当前的开发,不适合的情况下我很少会用到它。但是当它比我现在使用到的更合适的时候,我会考虑或者直接采用新特性。

还有一个问题其实我现在就能回答:

用了这么久,有没有总结一些技巧做产出?

因为我习惯定期做总结,基本上有好的实现方案就会记录下来。为了一系列文章中会随机在文末进行分享。

此外,我会循序渐进的分享Hooks的技术点。

如果有精力,会将源码阅读的经验详细的记录并分享出来。

useEffect

基础知识

useEffect 是 React 提供的核心 Hooks 之一。

Hooks 已经存在一段时间了,所以对 useEffect 介绍的文章应该挺多的。按照惯例,还是要来上一段它的简介和特点的。

简介

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它是一个合并的 API 。跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途。

这句官网的介绍中,我对副作用感觉挺陌生的,所以就去查了一下资料,这里做个延伸。

副作用与纯函数

为什么这两个概念在一起,因为纯函数不产生副作用。

纯函数

一个基于入参返回结果且不额外修改入参或者作用域之外的变量的函数。纯函数在执行过程中不会产生副作用。

副作用

通常指和当前执行结果无关的代码。比如在函数内部修改函数内或外的变量、改变 DOM、添加订阅、设置定时器、记录日志、发起一个HTTP请求。

一般不会引起内存泄漏的副作用是不需要清除的,像定时器、事件监听之类的则需要清除。

用法

useEffect(callback, dependencies)

useEffect 接收两个入参

  • callback:要执行的函数;
  • dependencies:依赖项数组,可选的。

着重说说这个依赖项数组。它会影响 effect(副作用) 的执行。默认情况下,effect 会在每轮组件渲染完成后执行。一旦指定了依赖项数组,那么只有指定的值发生变化时,effect 才会执行。

重点来咯

四重使用体验

既然依赖项数组是可选的,那么不传值、传空值、传非空值,应该会有不同的影响吧。遇到非肯定的时候,用例子来的更有说服力。

依赖项不传值

依赖项不传值,每次 render 后会执行 effect 。

通常我不用做不传值的操作,因为不需要 effect 被执行的很频繁。虽然很少,但是也有,我们在用到 useKeepAliveEffect 做页面数据缓存的时候,每次打开页面的时候,都会初始化一下缓存状态,因为我们的项目,不同的入口进入是否需要进行页面缓存的需要不同,有的入口进入页面需要清除缓存。

useEffect(() => {
  // 设置initValue的值true,表示组件还未被缓存
  ref.current.initValue = true;
});

依赖项传空值

依赖项传空值时,仅第一次 render 后执行 effect 。

这种情况经常被用来在页面打开时请求页面数据,比如详情页等。

useEffect(() => {
  // 初始化页面数据的Htttp请求
  getData();
}, []);

依赖项传非空值

当依赖项的值非空的情况下,第一次以及依赖项的值发生变化后都会执行 effect 。

我日常开发中的常见场景是弹窗类组件间的通信,每次控制弹窗展示的布尔值发生变化时,都有重新请求数据进行内容渲染。

useEffect(() => {
  if (visible) {
    // 初始化数据的Htttp请求
    getData();
  }
}, [visible]);

清除 effect

第四重体验是callback给到的,也是上面简介中提到的,useEffect 是一个合并 API,同时具有与 componentWillUnmount 相同的用途。用来清除 effect 。

前面提到过有些副作用是需要清除的,避免内存泄露。而 useEffect 提供的清除 effect 方法也很简单,返回了一个清除函数,在函数中清除 effect 即可。

useEffect(() => {
  // 滚动监听
  window.addEventListener('scroll', scrollHandle, false);

  return () => {
    // 清除滚动
    window.removeEventListener('scroll', scrollHandle, false);
  };
}, []);

通过跳过 Effect 进行性能优化

官网对这个知识点写的很详细,我简单介绍一下:

上面讲到如果依赖项不传值时,每次 render 后会执行 effect。这就可能会导致一些问题,比如 effect 里是一个 Http请求,这个请求会被不停的刷新,所以我们习惯传入一个空数组作为依赖项:

useEffect(() => {
  // 初始化页面数据的Htttp请求
  getData();
}, []);

提到这个知识点,着重给大家分享官网中下面的一句话:

未来版本,可能会在构建时自动添加第二个参数。

这样一来,不用每次都重复写空数组,但是不知道官方对于想每次都执行 effect 的情况是如何兼容的,期待一下。

当函数被作为依赖项

我在学习 Hooks 的过程中,看了一些大佬的文章。其中王沛老师提的一个问题让我很感兴趣,进行了一番研究。作为依赖项的延伸。

已知可以用局部变量作为依赖项,那么函数是否可以被作为依赖项呢?

const [title, setTitle] = useState('');

  useEffect(() => {
    setTitle('编辑');
  }, [setTitle]);

useState 能够保证 setTitle 每次都是同一个函数,也就是 setTitle 不会变,依赖项不发生变化和传空数组是一样的效果,所以无需作为依赖项。

总结

文章半程了,来总结一下 useEffect 的知识点:

  1. useEffect 作为核心 Hooks,主要是为函数组件赋能处理副作用的能力。
  2. 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。
  3. useEffect 接收两个入参,其中依赖值数组会影响 effect(副作用) 的执行。
  4. useEffect 中依赖项指定的方式不同,会影响 effect 的执行,可以根据需要进行选择。
  5. 局部变量函数作为依赖项的意义不大。

今日小技巧

文章开头提到了我又做总结的习惯,正好最近在梳理增加做过的功能。

其实看自己曾经写的代码,还是需要点勇气的。有点像曾经的QQ空间,全是情感没有技巧。

沉思3秒之后,我决定做点什么。正好这个系列都是Hooks相关的,技术栈声明都省了。

文末可能不定期随机出现技巧分享。

万物皆有源

在我的日常工作中,会有很大一部分开发精力花在业务开发上。

我至今还记得我同事问过我的一个问题:

你觉得目前对你来说比较困难的是业务还是技术?

快节奏、强逻辑,这个时期的我是边迭代边捋业务。所以被问到这个问题的时候,我毫不犹疑回答了「业务」二字。

直到后来,我也成为了一个业务「老人」。我又开始思考新一轮问题,既然我工作离不开业务,那么我怎么从日复一日的开发中,搞点「新花样」,以此来保持对开发的热爱。

有点啰嗦了,第一次文末写小技巧,交代一下前因后果,后面就直接进入正题,不再有前情提要环节了。

今日分享一个上周写技术文档的时候,发现的一个老代码优化的小技巧。

一组导入按钮的整合

UI

先来看UI长什么样,如下图是一排的导入按钮

Hooks 技巧 |  useEffect 依赖项数组的不同场景总结 & 一组导入操作的小技巧

导入按钮的特点

  • 没有选择文件的时候,所有按钮置灰不可选;
  • 导入中,当前导入按钮是加载中(loading)状态,其他导入按钮是置灰不可点状态。换个说法,某个导入按钮,当它被点击时,则它是加载中状态,其他按钮点击时,则它是置灰不可点状态。

道理我都懂,但是这一排按钮挨个设置,是不是有点费劲,所以今日小技巧就讲讲怎么简化一系列相似按钮。

实现分几步?

1.定义一个加载中的对象变量

将所有导入按钮的加载中布尔值变量放到一个对象中集中管理,重置变量值方便,扩展也方便,更改的位置会比较集中。

let [loading, setLoading] = useState({
  provinceFlag: false,
  cityFlag: false,
  regionFlag: false,
  addressFlag: false,
});

2.定义一个导入按钮数组

数组元素包含:

  • 每个导入操作的展示名称
  • 唯一的key值
  • 加载中布尔值变量
/** @name 导入和导出数据项 */
const uploadAndDownList = [
  {
    title: '省份批量导入',
    key: 'provinceFlag',
    loadParam: loading.provinceFlag,
  },
  {
    title: '城市批量导入',
    key: 'cityFlag',
    loadParam: loading.cityFlag,
  },
  {
    title: '地区批量导入',
    key: 'regionFlag',
    loadParam: loading.regionFlag,
  },
  {
    title: '街道批量导入',
    key: 'addressFlag',
    loadParam: loading.addressFlag,
  },
];

3.View中渲染内容

antd的Button组件自带loading和disabled属性。loading取当前数组元素的loadParam字段。disabled则需要有点技巧了。

{ uploadAndDownList.map((item, index) => {
    // 当前按钮是否可以点击布尔值
    const itemDisabled = getImportLoadExcludeSelfOrExpression(uploadAndDownList, item.loadParam) || !file;
    return (
      <Button key={index} type="primary" onClick={() => upload(item)} loading={item.loadParam} disabled={itemDisabled}>
        {item.title}
      </Button>
    );
  });
}

4.获取导入的按钮组当前的load值

来单独介绍一下disabled的技巧。

四个按钮中的按钮A不能点击的逻辑条件是:没有导入文件 || 导入按钮B加载中 || 导入按钮C加载中 || 导入按钮D加载中。

没有导入文件这个条件跟另外三个不搭界,单独判断即可。

三个加载中的变量全部都在uploadAndDownList里面存在,所以换个思路,非当前判断中的按钮,其他按钮只要有一个按钮是加载中状态的,当前判断按钮就不可点击。只要用数组的some方法即可。

/**
 * 获取导入的按钮组当前的load值 排除当前操作按钮的
 * @param {Array} list 接口入参
 * @param {boolean} loadExclude 被排除的load值也就是当前操作按钮的load值
 * @return {boolean} 返回的load值
 */
const getImportLoadExcludeSelfOrExpression = (list, loadExclude) => {
  let flag = false;
  flag = list.some(item => {
    // 不是当前操作按钮的load值时 只要有一个load值为true 便是true
    if (item.loadParam !== loadExclude) {
      return item.loadParam;
    }
  });
  return flag;
};

到此功能就完成了。这是我目前想到的比较好的实现方案,但是我相信应该会有更好的实现方式,等我有时候在观察观察,也欢迎有创意的实现方案评论留言。💐

补充

代码果然常看常新,我再进行组件梳理的时候,意外的发现,我之前对导入按钮触发之后其他按钮不可点的处理,真的很简单直接,不过我喜欢。

当点击导入按钮A,进行上传操作的时候,更改数组的值,将所有按钮的不可点击布尔值设置为true,导入按钮A的加载中布尔值设置为true。

/**
 * 上传事件
 * @param {Object} item 当前操作的上传对象
 * @param {number} index 当前操作的上传索引
 * @return {void} 无
 */
const upload = (item, index) => {
  if (!file) {
    return message.error('请选择文件');
  }
  message.info('上传中,请稍后');
  let list = _.cloneDeep(uploadAndDownList);
  uploadAndDownList.map((itm, inx) => {
    // 上传操作项加载和不可点击的处理
    itm.loading = inx === index ? true : false;
    itm.disabled = true;
  });
  setUploadAndDownList(list);
  // 上传操作
};

后记

我现在心情无比复杂,就好像码了三千字,然后告诉我活动取消了,文章不用写了。不过每个时期能有新的想法,这点上,我还是感到欣慰的。