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
constructor
,render
, andshouldComponentUpdate
methods - Class component static
getDerivedStateFromProps
method - Function component bodies
- State updater functions (the first argument to
setState
) - Functions passed to
useState
,useMemo
, oruseReducer
上文的关键字叫做“double-invoking”即重复调用,这句话是什么意思呢?大概意思就是,React并不能自动替你发现副作用,但是它会想办法让它显现出来,从而让你发现它。那么它是怎么让你发现副作用的呢?React的严格模式,在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。所以在处于开发模式且开启了React严格模式时,这些函数会被调用两次:
类组件的的 constructor
, render
, 和 shouldComponentUpdate
方法
类组件的静态方法 getDerivedStateFromProps
函数组件的函数体
参数为函数的setState
参数为函数的useState
, useMemo
, 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());
};
超哥yyds~请持续更新呀!
李老师 我有个疑问呀,可以帮我解答一下吗,usecallback和useeffect的区别和使用场景是怎么样的呢 感觉好容易混啊
useCallback是用来创建回调函数的,useEffect用来执行代码
打卡
打卡学习了
老师好,有个地方不太明白。react_74【什么是副作用】这一章节中第六分钟,if(ctx.totalAmount===0);我的理解是只会重新渲染一次【实际上是:Too many re-renders】,因为第一次重新渲染后,showDetail的值一直就是false呀,state中的值并没有发生改变呀,怎么还会触发多次重新渲染呢?
值得地址改变了 就是一个新的值
但是值的内容没改变 这次重新渲染不会渲染子组件
https://www.lilichao.com/index.php/2022/05/12/effect/#comment-351
哦,明白了。75节给了我答案【狗头】