前言
大家好,我是寻找光的sxy,又是充实的一天开发日啊!!!!在本周的开发中,我遇到了一个关于子组件传参的小坑......
现象
先看一段核心代码:父组件 Demo2 引用了子组件 Com1,但没有给 Com1 传递 obj 参数;子组件内部用解构赋值给 obj 设了默认值 {},并通过 useEffect 监听 obj 的变化、打印日志:
js
import { useEffect, useState } from "react";
/** 父组件 */
const Demo2 = () => {
return ( <div>
<h1>Demo2</h1>
{/* 父组件未给子组件传 obj 参数 */}
<Com1 /> </div>
);
};
export default Demo2;
/** 子组件 */
const Com1 = (props: any) => {
// 解构赋值,给 obj 设默认值 {}
const { obj = {} } = props;
const [nn, setNn] = useState(3);
// 监听 obj 变化,打印日志
useEffect(() => {
console.log(`useEffect触发,obj是: ${JSON.stringify(obj)}`);
}, [obj]);
// 点击按钮修改 nn 的值
const handleClick = () => { setNn(Math.random()); };
return (
<div>
<button onClick={handleClick}>Com1, nn is {nn}</button>
</div>
);
};
此时大家猜猜,当点击按钮时,控制台会不会执行console打印?
我的预期: useEffect 只监听 obj,而 obj 既没从父组件接收参数,也没在子组件内部修改,理应只在初始化时打印一次日志;点击按钮修改 nn,和 obj 无关,不该触发打印。
实际情况: 初始化打印一次后,每次点击按钮都会触发 useEffect 打印!
这就埋下了隐患:如果后续在这个 useEffect 里加业务逻辑(比如接口请求、DOM 操作),会导致这些逻辑被频繁执行,不仅浪费性能,还可能引发意外 Bug。
解决
发现了以上的问题,我先尝试了两种简单直接的方案,用于快速"止血":
1、给子组件显式传默认值
给子组件显式传一个空对象
js
<Com1 /> // 改动前
<Com1 obj={{}}/> // 改动后
2、去掉子组件传参的初始值
直接去掉子组件解构的默认值
js
const { obj = {} } = props; // 改动前
const { obj } = props; // 改动后
以两种方法都能解决且实现预期效果;
原因
那么为什么会多次打印呢?
相信聪明的你一下子就能关注到问题的关键点:useEffect。
我们在useEffect中添加了依赖项,只有依赖项发生变化的时候,才会执行打印逻辑;
回顾一下 useEffect 监听依赖项的逻辑:
- 依赖项为基本类型时,值发生变化,useEffect就会监听到,且触发;
- 当依赖项为引用类型时,引用地址发生变化,useEffect也会监听到,且触发;
这里的监听项是一个引用类型,那基本就可以确定了,应该是每次点击引用地址都发生了变化了。
那么问题来了,引用地址是在哪一步发生了变化?
相信聪明的你很快就能注意到以下这段代码:
js
const { obj = {} } = props;
当点击按钮执行 setNn(Math.random()) 时,会触发子组件 Com1 的 重新渲染------ 此时子组件内部的代码会重新执行一遍,包括这句解构赋值。
- 因为父组件没传 obj,所以每次重新渲染时,都会执行 obj = {},创建一个 新的空对象;
- 新对象的引用地址和上一次渲染的 obj 地址不同;
- useEffect 检测到依赖 obj 的地址变了,就会触发日志打印。
再看解决方案的本质
两种方案之所以有效,核心都是 阻止 "重新渲染时创建新 obj" :
- 方案 1:父组件传递 obj={{}} 后,子组件解构时会直接用父组件传的对象(地址固定),不再创建新对象;
- 方案 2:移除默认值后,obj 为 undefined(基本类型,值固定),地址自然不会变。
延伸思考
上面的小问题已经完美解决了,再解决后,我又想到了一种别的常用的场景,代码如下:
js
const Com1 = () => {
const [nn2, setNn2] = useState({ n: 1 });
const handleClick2 = () => {
setNn2({ n: 1 });
};
useEffect(() => {
console.log(`useEffect触发,nn2是: ${nn2}`);
}, [nn2]);
return (
<div>
<button onClick={handleClick2}>改引用类型 {nn2.n}</button>
</div>
);
};
现象:点击按钮, useEffect 每次都触发
虽然 setNn2 的值都是 {n:1},但 每次创建的都是新对象,引用地址不同------useEffect 检测到地址变化,就会触发打印。
这和之前的子组件传参问题本质一致:都是 "引用类型的地址被意外修改" 导致的 useEffect 误触发。
总结
使用useEffect监听引用类型需谨慎:
-
- 警惕 "隐式创建新引用": 比如解构赋值设默认值(const { obj = {} } = props)、函数内定义对象(setNn2({ n: 1 })),这些操作在重新渲染时会生成新地址;
- 优先 "固定引用地址": 如果引用类型的值不变,尽量让它的地址固定(比如父组件传默认值、用 useMemo 缓存对象);
- 复杂场景用 useMemo/useCallback: 如果需要监听的引用类型是计算得出的,用 useMemo 缓存(对象 / 数组);如果是函数,用 useCallback 缓存,避免地址频繁变化。