日期:2024年5月19日

Effect

React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。

例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。

React.StrictMode

编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码,当然这个智能是加了引号的,我们来看看React官网的文档是如何说明的:

Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:

  • Class component constructorrender, and shouldComponentUpdate methods
  • Class component static getDerivedStateFromProps method
  • Function component bodies
  • State updater functions (the first argument to setState)
  • Functions passed to useStateuseMemo, or useReducer

上文的关键字叫做“double-invoking”即重复调用,这句话是什么意思呢?大概意思就是,React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:

类组件的的 constructorrender, 和 shouldComponentUpdate 方法
类组件的静态方法 getDerivedStateFromProps
函数组件的函数体
参数为函数的setState
参数为函数的useStateuseMemo, or useReducer

重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。

如果你无法通过浏览器正常安装React Developer Tools可以通过点击这里下载。

使用Effect

为了解决这个问题React专门为我们提供了钩子函数useEffect(),Effect的翻译过来就是副作用,专门用来处理那些不能直接写在组件内部的代码。

哪些代码不能直接写在组件内部呢?像是:获取数据、记录日志、检查登录、设置定时器等。简单来说,就是那些和组件渲染无关,但却有可能对组件产生副作用的代码。

useEffect语法:

useEffect(didUpdate);

useEffect()需要一个函数作为参数,你可以这样写:

useEffect(()=>{
/* 编写那些会产生副作用的代码 */
});

useEffect()中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。

通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。

清除effect

组件的每次重新渲染effect都会执行,有一些情况里,两次effect执行会互相影响。比如,在effect中设置了一个定时器,总不能每次effect执行都设置一个新的定时器,所以我们需要在一个effect执行前,清除掉前一个effect所带来的影响。要实现这个功能,可以在effect中将一个函数作为返回值返回,像是这样:

useEffect(()=>{
/* 编写那些会产生副作用的代码 */

return () => {
/* 这个函数会在下一次effect执行钱调用 */
};
});

effect返回的函数,会在下一次effect执行前调用,我们可以在这个函数中清除掉前一次effect执行所带来的影响。

限制effect

组件每次渲染effect都会执行,这似乎并不总那么必要。因此在useEffect()中我们可以限制effect的执行时机,在useEffect()中可以将一个数组作为第二个参数传递,像是这样:

useEffect(()=>{
    /* 编写那些会产生副作用的代码 */

    return () => {
        /* 这个函数会在下一次effect执行前调用 */
    };
}, [a, b]);

上例中,数组中有两个变量a和b,设置以后effect只有在变量a或b发生变化时才会执行。这样即可限制effect的执行次数,也可以直接传递一个空数组,如果是空数组,那么effect只会执行一次。

使用Effect修改练习

在《汉堡到家》的练习中,存在着一个bug。当我们在购物车或结账界面减少商品的数量全部为0时(购物车中没有商品时)。购物车或结账页面并不能自动关闭,这里我们就可以借用Effect来解决问题。可以直接修改Cart.js直接向组件中添加如下的代码:

useEffect(() => {
if(ctx.totalAmount === 0) {
setShowCheckout(false);
setShowDetails(false);
}
}, [ctx]);

这样一来,当购物车中的商品发生变化时,就会触发useEffect,从而检查商品的总数量,如果总数量为0的话就会将购物车详情页和结账也直接隐藏。

除了Cart.js以外,FilterMeals组件也存在一个问题,首先,该组件中的表单项我们并没有使用state,所以这个组件是一个非受控组件,虽然目前看来没什么太大的问题,但是我们还是应该处理一下,因为受控组件使用时会更加的灵活,可以适用于更多的场景。其次、该组件的主要作用是过滤汉堡的列表,当用户输入关键字时它可以根据关键字的内容对食物列表进行过滤。问题正在于此,由于每次用户输入都需要过滤,这就意味着它的过滤频率过高了。举个例子,用户要输入“汉堡”这个关键字,他需要一次输入h-a-n-g-b-a-o七个字母,由于每次输入都会触发一次过滤,所以在“汉堡”打出来之前,列表完全是一个空白的状态,同时无用的过滤也对应用的性能造成了一定的影响。怎么办呢?同样可以使用Effect来解决这个问题,修改FilterMeals中的代码如下:

const [keyword, setKeyword] = useState('');

useEffect(() => {
const timer = setTimeout(() => {
props.onFilter(keyword);
}, 1000);

return () => {
clearTimeout(timer);
};
}, [keyword]);

const inputChangeHandler = e => {
setKeyword(e.target.value.trim());
};
5 7 投票数
文章评分
订阅评论
提醒
guest

9 评论
最旧
最新 最多投票
内联反馈
查看所有评论
翦水双瞳
翦水双瞳
1 年 前

超哥yyds~请持续更新呀!

熊zzz
熊zzz
1 年 前

李老师 我有个疑问呀,可以帮我解答一下吗,usecallback和useeffect的区别和使用场景是怎么样的呢 感觉好容易混啊

Chin
1 年 前

打卡

学前端的小萌新
学前端的小萌新
1 年 前

打卡学习了

巨人张
巨人张
1 年 前

老师好,有个地方不太明白。react_74【什么是副作用】这一章节中第六分钟,if(ctx.totalAmount===0);我的理解是只会重新渲染一次【实际上是:Too many re-renders】,因为第一次重新渲染后,showDetail的值一直就是false呀,state中的值并没有发生改变呀,怎么还会触发多次重新渲染呢?

ruyu
ruyu
1 年 前
回复给  巨人张

值得地址改变了 就是一个新的值

ruyu
ruyu
1 年 前
回复给  巨人张

但是值的内容没改变 这次重新渲染不会渲染子组件

巨人张
巨人张
1 年 前

https://www.lilichao.com/index.php/2022/05/12/effect/#comment-351
哦,明白了。75节给了我答案【狗头】

9
0
希望看到您的想法,请您发表评论x