Published on
1541

谈谈react里的事件和副作用的那些事

Authors
  • avatar
    Name
    小辉辉
    Twitter

前言

自从react更新到新的官方文档(react.dev)后,除了网站风格和以前对比变化很多以外,也更新了很多有用的知识。

这部分知识争取每个都看一遍,因为官方文档里往往会给出一些开发的最佳实践,针对有些概念会从根源入手,讲的比较详细,不仅仅会给出问题出现的原因,还会给出相关的解决方案,这无疑会对每个react开发者提供很大的帮助。

在这篇文章里分析了下Escape Hatches系列下的两篇文章:

  1. Separating Events from Effects
  2. Removing Effect Dependencies

文章很长,这里我会对里面提到的几个关键问题进行分析,对于这些问题,有些我会先尝试给出自己的解决方案,后面再给出官方的解决方案。

事件和副作用的使用场景

对于刚刚接触react的开发者,这两个概念很容易混淆,这里大致讲下两者的特点和使用场景。

事件(event)

  1. 什么时候使用

在处理某类点击、值修改时触发,一般在其内部处理一些单一的业务逻辑。

  1. 特点

事件触发时可以获取到组件所属范围内的state最新值。

非响应式,state的触发不会导致不需要事件触发,事件一般都是由操作人员手动触发。

副作用(effect)

  1. 什么时候使用

使用useEffecthook来实现。

一般在组件内部针对某些state状态改变时调用,除此之外,还包括组件挂载时间。

  1. 特点

可以监听多个state的变化,任意一个依赖的变化都可以触发副作用的执行。

使用总结

  1. 事件处理方法一般针对于某类交互场景
  2. 副作用用于解决某些需要同步响应数据变化的场景
  3. 事件处理内的逻辑不是响应式的(不会随外部数据变化而执行)
  4. 副作用内部的逻辑是响应式的(会随着外部数据变化而执行)

副作用依赖

依赖为空

使用useEffecthook时,第一个传参会hook触发时调用的方法,第二个参数为依赖项,如果不传代表每次组件内部有变化或者重新渲染时都会调用回调方法,这类使用场景比较少。

依赖为空数组

传入[]时,代表组件首次加载的时候触发方法,之后的变化不会再调用,除非组件在之后被卸载又重新创建。

依赖缺失带来的问题

举例如下:

function Canvas({name}) {
    const [msg] = useState('');

    useEffect(()=>{
        sendMsg(msg, name);
    }, [msg]);
    //....
}

这里sendMsg方法里面没有声明name依赖项,最后导致的问题是外部组件对应的入参name发生变化而组件内部msg没有改变时,sendMsg并不会触发。

正确的做法:

function Canvas({name}) {
    const [msg] = useState('');

    useEffect(()=>{
        sendMsg(msg, name);
    }, [msg, name]);
    //....
}

如何正确设置依赖项

常规的做法是使用官方推荐的eslint-plugin-react-hooks插件,相关配置项如下:

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
    "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
  }
}

开启该插件后,会针对副作用回调函数里用到的响应式数据进行分析,如果没有声明依赖,会抛出eslint校验错误。

解决副作用依赖里非响应的逻辑

这一块的内容可能比较难以理解,实际开发过程中这类场景也比较少见,具体情况:

  1. 某个副作用函数里存在两个依赖ab
  2. a的值发生改变时,需要触发
  3. b的值发生改变时,不需要触发

先说下我这边的解决方案

考虑将数据b设置为ref类型,这样以来原有设值方法需要替换为b.current来实现,在副作用行数内部同样使用b.current来实时获取最新值。

这个方案的实现原理就是一个对象引用的概念。

这里说下官方的解决方案:

使用开发版本中的useEffectEventhook来解决,该hook在正式版的react中还未发布

相关实现代码如下

function Canvas({name}) {
    const [msg] = useState('');
    // 关注这个hook的调用
    const handleMsgChange = useEffectEvent((msg)=>{
        sendMsg(nsg, name); // 这里的name始终是最新的
    })

    useEffect(()=>{
        handleMsgChange(msg);
    }, [msg]); // 未加name依赖项,但是可以通过lint校验
    //....
}

关于useEffectEvent使用详情参考文档

可以看出官方的实现方案会更加简单优雅,期待早日发布。

移除非必要的副作用依赖

我们都知道依赖加多的情况下,很容易会导致回调函数频繁执行,最终有可能会带来相关性能问题或者bug。

官方文章里对此也给出了下面几个有效的解决方案:

  1. 副作用函数内部使用的变量为常量
  2. 考虑将部分副作用的方法放到事件里面去执行
  3. 尽可能单个副作用方法实现一个逻辑,每个逻辑最少固定的依赖项
  4. 如果副作用内部不需要获取响应数据,仅需求修改响应数据时,可以不声明依赖项,考虑通过function updater的形式来解决
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    connection.on('message', (receivedMessage) => {
      setMessages(msgs => [...msgs, receivedMessage]);
    });
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...
  1. 针对设置依赖项以后,相同数据会重复执行的问题,考虑不要使用对象和函数类型

总结

看完这两篇关于react副作用和事件的文章以后,自己还是多少学到了一点新的知识,希望大家看完文章以后也会有不一样的收获,当然有时间的话,还是推荐大家去看下原文。