likes
comments
collection
share

React Effect 逃生舱口

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

前言

随着React的不断更新迭代,React Hooks就进入了前端开发者的视野,并大量使用Hooks,其中Effect是必然会用到。在平时的业务开发中,可能希望通过React状态去控制一些逻辑比如弹窗提醒、列表渲染、发送服务器请求,更或者是非React相关组件等。那么Effect就可以满足你,它可以在渲染之后再继续运行,便于您可以将组件与外部系统同步。

开始

本文主要会介绍以下内容:

  1. 什么是副作用(Effect)?副作用与事件处理程序(Event Handler)有什么区别?
  2. 如何声明一个副作用?
  3. 被滥用的 useEffect。
  4. 如果副作用中存在非反应逻辑,我们应该怎么做呢?

副作用与事件处理程序

什么是副作用?

在我们写代码的过程中,经常会为了获取到某个想要的结果,而通过操作某个对象,这里的操作可能包括使用 函数/表达式 又或者这个对象自带的一些属性方法等。但操作某个对象时,产生了附加的影响,这个影响称为副作用。比如:

const arr = [1,2,3,4];
const last = arr.pop(); // -> 4
console.log(arr); // -> [1,2,3]

上述例子 我们定义了一个数组 arr,为了去获取到数组的最后一个值,使用了数组内置的方法pop(), 虽然在这里我们获取到了这个值 last === 4,但是我们的arr也被修改成了 [1,2,3]。所以这里虽然达到了目的获取到了数组最后一个值,但是改变了原数组,这样可能会导致其他不可控的 bug 出现。 然后我们使用 React 会在很多场景用到响应式数据,如果因为类似这样的副作用的操作,可能就会影响全局,从而导致 bug 产生。

什么是事件处理程序?

事件处理程序是组件内部的嵌套函数,主要包含由用户发起的一些交互行为引起的副作用(可以修改状态),以及在渲染期间,执行的某些特定的行为交互。

副作用与事件处理程序(Event Handler)有什么区别?

副作用是由渲染本身而产生的效果。而事件处理程序则是在渲染期间,由用户发起的一些交互行为产生的效果。

如何声明一个副作用

声明 Effect 会用到 React useEffect Hook。useEffect 是用来捕获副作用的 Hook,代替类式组件中组件的生命周期函数。

useEffect(setup, dependencies);

useEffect的依赖项

当没有 dependencies 的时候,意味着无论页面有什么样的交互,副作用内部的反应逻辑代码都会执行一次。

useEffect(() => {});

当 dependencies 为一个空数组的时候,意味着副作用内部的反应逻辑代码只会在初始化时执行一次。

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

当 dependencies 里面有依赖时(reactive 值),意味着副作用内部的反应逻辑代码会根据reactive 值变化而执行。

useEffect(() => {}, [a, b]);

如何确认依赖项是否正确

React 官方提供了一个Eslint插件eslint-plugin-react-hooks,它将验证您的副作用代码中的依赖项是否正确,实际效果如下: ![image.png](cdn.nlark.com/yuque/0/202… 前言 :::warning 随着React的不断更新迭代,React Hooks就进入了前端开发者的视野,并大量使用Hooks,其中Effect是必然会用到。在平时的业务开发中,可能希望通过React状态去控制一些逻辑比如弹窗提醒、列表渲染、发送服务器请求,更或者是非React相关组件等。那么Effect就可以满足你,它可以在渲染之后再继续运行,便于您可以将组件与外部系统同步。 :::

开始

本文主要会介绍以下内容:

  1. 什么是副作用(Effect)? 副作用与事件处理程序(Event Handler)有什么区别?
  2. 如何声明一个副作用?
  3. 被滥用的useEffect。
  4. 如果副作用中存在非反应逻辑,我们应该怎么做呢?

副作用与事件处理程序

什么是副作用?

在我们写代码的过程中,经常会为了获取到某个想要的结果,而通过操作某个对象,这里的操作可能包括使用 函数/表达式 又或者这个对象自带的一些属性方法等。但操作某个对象时,产生了附加的影响,这个影响称为副作用。比如:

const arr = [1,2,3,4];
const last = arr.pop(); // -> 4
console.log(arr); // -> [1,2,3]

上述例子 我们定义了一个数组 arr,为了去获取到数组的最后一个值,使用了数组内置的方法pop(), 虽然在这里我们获取到了这个值 last === 4,但是我们的arr也被修改成了 [1,2,3]。所以这里虽然达到了目的获取到了数组最后一个值,但是改变了原数组,这样可能会导致其他不可控的 bug 出现。 然后我们使用 React 会在很多场景用到响应式数据,如果因为类似这样的副作用的操作,可能就会影响全局,从而导致 bug 产生。

什么是事件处理程序?

事件处理程序是组件内部的嵌套函数,主要包含由用户发起的一些交互行为引起的副作用(可以修改状态),以及在渲染期间,执行的某些特定的行为交互。

副作用与事件处理程序(Event Handler)有什么区别?

副作用是由渲染本身而产生的效果。而事件处理程序则是在渲染期间,由用户发起的一些交互行为产生的效果。

如何声明一个副作用

声明 Effect 会用到 React useEffect Hook。useEffect 是用来捕获副作用的 Hook,代替类式组件中组件的生命周期函数。

useEffect(setup, dependencies);

useEffect的依赖项

当没有 dependencies 的时候,意味着无论页面有什么样的交互,副作用内部的反应逻辑代码都会执行一次。

useEffect(() => {});

当 dependencies 为一个空数组的时候,意味着副作用内部的反应逻辑代码只会在初始化时执行一次。

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

当 dependencies 里面有依赖时(reactive 值),意味着副作用内部的反应逻辑代码会根据reactive 值变化而执行。

useEffect(() => {}, [a, b]);

如何确认依赖项是否正确

React 官方提供了一个Eslint插件eslint-plugin-react-hooks,它将验证您的副作用代码中的依赖项是否正确,实际效果如下: React Effect 逃生舱口 因为副作用代码依赖的空数组,实际上 cardId 和 pos 是反应性数据及 props 和 state,因此 linter 规则提醒您,应该将 cardId 和 pos 添加到依赖项中。 如果您想将 cardId 和 pos 移除依赖项,那么就需要满足它们的值不具备反应性即 not reactive,也就是说它们的值是稳定不变的。

// pass 不具备反应性
const pass = false
function AttendConcert({ cardId }) {
  // 是否通过。
  const cardId = useRef(1001)
  useEffect(() => {
    const isPass = validateUser(cardId.current);
    Toast.info(isPass ? '可以参加' : '不可以参加')
    // 不需要添加依赖项
  }, [])
  return <div>{cardId}</div>
}

如何定义一个事件处理程序

由上文“什么是事件处理程序”可知,事件处理程序是组件内部的嵌套函数,它主要包含由用户发起的一些交互行为引起的副作用等。比如:

function Concert() {
  const [cardId, setCardId] = useState(102);
  // 模拟场景
  const hanldeCountClick = () => {
    //
    setCardId(cardId => cardId + 1);
  }
  return (
    <div className="App">
      <AttendConcert cardId={cardId}/>
      <button type='button' onClick={hanldeCountClick}>点击+1</button>
      <div>当尾号为0,3,6,9的时候,可以参加演唱会</div>
    </div>
  );
}

被滥用的 useEffect

在开发的过程中,我们经常遇到的场景,就是在页面初始化渲染的时候,我们会通过请求接口获取数据并执行一系列的判断逻辑。而这个时候如果利用不合理,就会产生很多 dependencies。虽然我们会充分利用 React 的其他 Hooks 来优化性能,但也总会避免不了一些其他的浪费。比如:在模拟线上演唱会人员入场时,需要验票等。

function App() {
  const [cardId, setCardId] = useState(102)
  const [pos, setPos] = useState('top')
  const handlePosClick = () => {
    setPos((pos) => {
      if (pos === 'top') return 'bottom'
      return 'top'
    })
  }
  const hanldeCountClick = () => {
    setCardId(cardId => cardId + 1)
  }
  return (
    <div className="App">
      <AttendConcert cardId={cardId} pos={pos} />
      <div className='btns'>
        <Button type='primary' onClick={hanldeCountClick}>点击+1</Button>
        <Button onClick={handlePosClick}>修改Tips位置: {pos}</Button>
      </div>
    </div>
  );
}

// 参加演唱会
function AttendConcert({ cardId, pos }) {
  useEffect(() => {
    const validateRes = validateUser(cardId);
    validateRes.on('validated', () => {
      Toast.show({content: `验证成功`, position: pos})
    })
    validateRes.validated();
  }, [cardId, pos])
  return <div>{cardId}: {pos}</div>
}

这个时候会根据票号校验是否正确。当前副作用会依赖 cardId 和 pos 属性变化而执行副作用。 React Effect 逃生舱口

通过示例图发现,我们点击2个按钮都会触发 tips,但其实我们的需求是只有第一个按钮触发,第二个按钮只是修改一个 tips 的位置,而我们真正想要的应该是修改状态后,点击第一个按钮的时候就沿用上一个状态,显示其最终效果。 所以,我们需要满足需求的话,就应该提取出副作用中的非反应逻辑代码出来。那么我们应该怎么做呢?接着往下看。

如何在副作用中提取非反应逻辑

随着 React 的更新迭代,官方也发现了该问题,为了应对这种场景,React 官方文档逃生舱章节里面专门新增了一个试验性的 Hooks, 它就是 useEffectEvent

experimental_useEffectEvent

useEffectEvent是声明一个副作用事件,它的目的就是为了提取副作用中非反应逻辑代码。这也是 React 针对副作用做的一个极大的优化。但该 Hook 仅是实验性 Hook ,并未在正式版上发布。如果您想尝试该 Hook,可以安装 React 最新的实验性包:

  • react@experimental
  • react-dom@experimental
  • eslint-plugin-react-hooks@experimental

但是不能用在生产环境中哦。 结合上文,我们应该把副作用里面的非反应逻辑提取出来,这里要用到useEffectEvent

import { Button, Toast } from 'antd-mobile';
import './App.css';
// 引入实验性Hook experimental_useEffectEvent
import { useEffect, experimental_useEffectEvent as useEffectEvent, useState } from 'react'

function App() {
  const [cardId, setCardId] = useState(102)
  const [pos, setPos] = useState('top')
  const handlePosClick = () => {
    setPos((pos) => {
      if (pos === 'top') return 'bottom'
      return 'top'
    })
  }
  const hanldeCountClick = () => {
    setCardId(cardId => cardId + 1)
  }
  return (
    <div className="App">
      <AttendConcert cardId={cardId} pos={pos} />
      <div className='btns'>
        <Button type='primary' onClick={hanldeCountClick}>点击+1</Button>
        <Button onClick={handlePosClick}>修改Tips位置: {pos}</Button>
      </div>
    </div>
  );
}

// 参加演唱会
function AttendConcert({ cardId, pos }) {
  // 将副作用中,非反应逻辑提取出来
  const validate = useEffectEvent(() => {
     Toast.show({content: `验证成功`, position: pos})
  })
  useEffect(() => {
    const validateRes = validateUser(cardId);
    validateRes.on('validated', () => {
      validate();
    })
    validateRes.validated();
    // 可以删除pos依赖, 也不用依赖validate
  }, [cardId])
  return <div>{cardId}: {pos}</div>
}

这里我们使用useEffectEvent Hook提取出了副作用中非反应逻辑代码 Toast.show({content: `验证成功`, position: pos}),所以副作用依赖项也删除了pos。实际效果如下: React Effect 逃生舱口 从效果中能看出,当我们点击第二个按钮的时候,并没有触发 tips,而是将 tips 的位置存储了下来;只有当点击第一个按钮的时候,才会将最新状态的 tips 显示出来。

experimental_useEffectEvent使用限制

useEffectEvent使用相对比较受限:

  • 只能在副作用内部调用它们。
  • 不要将它们传递给其他组件或钩子。

比如:不要像以下方式去使用useEffectEvent

import { experimental_useEffectEvent as useEffectEvent, useCallback, useEffect } from 'react';
function Ani() {
	const cat = useEffectEvent(() => Toast.show({content: '我是动物'}));
  // 错误示范
  const onAni = useCallback(() => {
  	cat();
	}, [])
  // 正确示范
  useEffect(() => {
    cat();
  }, [])
  return <div/>
}

所以,副作用事件是副作用代码的非反应逻辑。 它们应该存在于副作用的逻辑内。

结尾

  • 事件处理程序运行是响应程序特定的交互。
  • 只要有状态同步,比如 props, state 及其他变量状态更新,副作用 useEffect 就会运行。
  • 事件处理程序中的逻辑不具备反应性。
  • 副作用内部的逻辑是反应式的。
  • 你可以将非反应性逻辑从副作用移动到副作用事件 useEffectEvent 内。
  • 副作用事件只能用于副作用逻辑内部。
  • 不能将副作用事件传递给其他组件或钩子。
  • 推荐使用 React linter 【eslint-plugin-react-hooks】规则校验副作用依赖项。
  • 推荐使用useTransition当组件状态频繁更新导致UI频繁更新时,可以使用该 Hook 在转换时更加顺畅。
  • 推荐使用useDeferredValue 当父组件依赖多个子组件的状态渲染页面时,子组件渲染顺序不确定可能导致页面闪烁等,可以使用该 Hook 做延迟渲染。

附言