前言
随着React的不断更新迭代,React Hooks就进入了前端开发者的视野,并大量使用Hooks,其中Effect
是必然会用到。在平时的业务开发中,可能希望通过React状态去控制一些逻辑比如弹窗提醒、列表渲染、发送服务器请求,更或者是非React相关组件等。那么Effect
就可以满足你,它可以在渲染之后再继续运行,便于您可以将组件与外部系统同步。
开始
本文主要会介绍以下内容:
- 什么是副作用(Effect)?副作用与事件处理程序(Event Handler)有什么区别?
- 如何声明一个副作用?
- 被滥用的 useEffect。
- 如果副作用中存在非反应逻辑,我们应该怎么做呢?
副作用与事件处理程序
什么是副作用?
在我们写代码的过程中,经常会为了获取到某个想要的结果,而通过操作某个对象,这里的操作可能包括使用 函数/表达式 又或者这个对象自带的一些属性方法等。但操作某个对象时,产生了附加的影响,这个影响称为副作用。比如:
javascript
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,代替类式组件中组件的生命周期函数。
javascript
useEffect(setup, dependencies);
useEffect的依赖项
当没有 dependencies 的时候,意味着无论页面有什么样的交互,副作用内部的反应逻辑代码都会执行一次。
javascript
useEffect(() => {});
当 dependencies 为一个空数组的时候,意味着副作用内部的反应逻辑代码只会在初始化时执行一次。
javascript
useEffect(() => {}, []);
当 dependencies 里面有依赖时(reactive 值),意味着副作用内部的反应逻辑代码会根据reactive 值变化而执行。
javascript
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
就可以满足你,它可以在渲染之后再继续运行,便于您可以将组件与外部系统同步。 :::
开始
本文主要会介绍以下内容:
- 什么是副作用(Effect)? 副作用与事件处理程序(Event Handler)有什么区别?
- 如何声明一个副作用?
- 被滥用的useEffect。
- 如果副作用中存在非反应逻辑,我们应该怎么做呢?
副作用与事件处理程序
什么是副作用?
在我们写代码的过程中,经常会为了获取到某个想要的结果,而通过操作某个对象,这里的操作可能包括使用 函数/表达式 又或者这个对象自带的一些属性方法等。但操作某个对象时,产生了附加的影响,这个影响称为副作用。比如:
javascript
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,代替类式组件中组件的生命周期函数。
javascript
useEffect(setup, dependencies);
useEffect的依赖项
当没有 dependencies 的时候,意味着无论页面有什么样的交互,副作用内部的反应逻辑代码都会执行一次。
javascript
useEffect(() => {});
当 dependencies 为一个空数组的时候,意味着副作用内部的反应逻辑代码只会在初始化时执行一次。
javascript
useEffect(() => {}, []);
当 dependencies 里面有依赖时(reactive 值),意味着副作用内部的反应逻辑代码会根据reactive 值变化而执行。
javascript
useEffect(() => {}, [a, b]);
如何确认依赖项是否正确
React 官方提供了一个Eslint插件eslint-plugin-react-hooks
,它将验证您的副作用代码中的依赖项是否正确,实际效果如下:
因为副作用代码依赖的空数组,实际上 cardId 和 pos 是反应性数据及 props 和 state,因此 linter 规则提醒您,应该将 cardId 和 pos 添加到依赖项中。 如果您想将 cardId 和 pos 移除依赖项,那么就需要满足它们的值不具备反应性即 not reactive,也就是说它们的值是稳定不变的。
javascript
// pass 不具备反应性
const pass = false
function AttendConcert({ cardId }) {
// 是否通过。
const cardId = useRef(1001)
useEffect(() => {
const isPass = validateUser(cardId.current);
Toast.info(isPass ? '可以参加' : '不可以参加')
// 不需要添加依赖项
}, [])
return <div>{cardId}</div>
}
如何定义一个事件处理程序
由上文"什么是事件处理程序"可知,事件处理程序是组件内部的嵌套函数,它主要包含由用户发起的一些交互行为引起的副作用等。比如:
javascript
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 来优化性能,但也总会避免不了一些其他的浪费。比如:在模拟线上演唱会人员入场时,需要验票等。
javascript
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 属性变化而执行副作用。
通过示例图发现,我们点击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
。
javascript
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
。实际效果如下:
从效果中能看出,当我们点击第二个按钮的时候,并没有触发 tips,而是将 tips 的位置存储了下来;只有当点击第一个按钮的时候,才会将最新状态的 tips 显示出来。
experimental_useEffectEvent使用限制
useEffectEvent
使用相对比较受限:
- 只能在副作用内部调用它们。
- 不要将它们传递给其他组件或钩子。
比如:不要像以下方式去使用useEffectEvent
javascript
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 做延迟渲染。