在看这篇文章前,推荐大家先仔细看一下React官网中对于useEffect的文章react.nodejs.cn/learn/you-m…
和这⬇️篇文章 ( 主要看评论)
zhuanlan.zhihu.com/p/450513902…
1、最佳实践?
1.1 状态同步
大伙一定遇到过这样的场景,假设有stateA和stateB,当stateA变化时,stateB也必须要变化,但反过来并不会。例如有一个日期类型(dateType)和日期值(dateKey),当日期类型切换时,你需要转换dateKey
- dateKey受到dateType控制,把他变为一个计算属性?显然不行,因为dateKey还需要维护自己的状态
//他们都必须是state
const [dateType,setDateType] = useState('day')
const [dateKey,setDateKey] = useState(new Date().toLocaleString())
- 引入一个新的计算属性,当有值更改时,去重新计算?
const date = useMemo(()=>{ transform(dateKey,dateType)},[dateKey,dateType])
为了解决一个小问题,引入了一个新的状态,这似乎不划算,反倒增加了负担
//这是最常见的方式,大伙一定见过这种做法,甚至写过,无论背景
useEffect(() => {
checkValue(dateType)
setDateKey((dateKey)=>transform(dateType,dateKey))
},[dateType]);
- 当一个值的改变的时候,你可以知晓并做些什么,这让你想到了Vue中的watch,你在React的文档中查找,发现了useEffect可以满足你要求,并没有在意什么 “
useEffect
是一个 React Hook,它允许你将组件与外部系统同步”,你隐约的知道useEffect是在渲染结束时调用,可能会导致重复渲染,但你并没有在意,因为除了这个也没有API可用了
import React, { useEffect, useState } from "react";
let onlyOne = true;
//分别执行下这两段逻辑,应该可以初见端倪
const Son = (props) => {
const [data, setData] = useState(1);
console.log("渲染中");
// if (props.show && onlyOne) {
// setData((c) => c + 1);
// onlyOne = false;
// }
// useEffect(() => {
// setData((c) => c + 1);
// }, [props.show]);
useEffect(() => {
console.log("渲染完毕"); //第一种方式只会打印一次,第二种会打印两次
});
return <div>son:{data}</div>;
};
const App = () => {
const [show, setShow] = useState(false);
return (
<div
onClick={() => {
console.log("start");
setShow(true);
}}
>
<Son show={show}></Son>
</div>
);
};
export default App;
1.2 官方例子
让我们看一看官方文档给的方案吧 当属性改变时调整一些状态
官网的最佳例子再次在组件中引入了新的状态,preItem,再次为了解决小问题而引入了一个新的值,这可能反倒增加了负担,显然称不上最佳实践,但他确实没有导致不必要的渲染
- 正如图上底部蓝字所说,我们需要在渲染过程中计算一切
- preItem的目的仅仅只是为了缓存上一次的值,检测是否需要执行变更
- 更新应该是同步的
2、昂贵的渲染
上述三点,我们可以想到什么?useMemo! 将上例中的useEffect换成useMemo,
很好,只有一次。
2.1 渲染次数
先暂时不管上面那个东西,抛出一个问题,这三个的大小排序是什么
- UI组件函数执行次数
- 单个useEffect组件的执行次数
- dom的渲染次数
前两个很好检测,最后一个的话,我们可以更新一个文本dom,利用 MutationObserver去检测
三个检测hooks
const useCheckDomRenderCount = (
domref: React.MutableRefObject<HTMLDivElement>
) => {
const domRenderCount = useRef(0);
useEffect(() => {
const observe = new MutationObserver((e) => {
domRenderCount.current = domRenderCount.current + 1;
console.log(e, "do再渲染次数:", domRenderCount);
});
observe.observe(domref.current, {
characterData: true,
subtree: true,
characterDataOldValue: true,
});
return () => observe.disconnect();
}, [domref]);
};
const useCheckUIFnExecCount = () => {
const uiFnExecCount = useRef(0);
++uiFnExecCount.current;
console.log("UI函数执行次数:", uiFnExecCount.current);
};
const useCheckUseEffectExecCount = () => {
let isLatest = true;
const useEffectExecCount = useRef(0);
useEffect(() => {
++useEffectExecCount.current;
console.log(
"useEffect(request API)函数执行次数:",
useEffectExecCount.current
);
fetch("http://localhost:8080")
.then((res) => res.json())
.then(() => {
if (isLatest) {
//do something
}
});
return () => {
isLatest = false;
};
});
};
我们引用ahooks中的这段代码,这在effect中仅为了setState,但稍微更(添)改(油)一(加)点(醋),尽管在实际上可能很少并且也不推荐有这么长的依赖链
const APP = () => {
const [count, setCount] = useState("a");
const [effectCount, setEffectCount] = useState("b");
const [updateEffectCount, setUpdateEffectCount] = useState("c");
const [afterUpdateEffect, setAfterUpdateEffect] = useState("d");
const domref = useRef<any>();
console.log(
count,
effectCount,
updateEffectCount,
afterUpdateEffect,
"________________"
);
useEffect(() => {
setEffectCount((b) => b + "b");
}, [count]);
useEffect(() => {
setUpdateEffectCount((c) => c + "c");
return () => {
// do something
};
}, [effectCount]); // you can include deps array if necessary
useEffect(() => {
setAfterUpdateEffect((d) => d + "d");
}, [updateEffectCount]);
useCheckDomRenderCount(domref);
useCheckUIFnExecCount();
useCheckUseEffectExecCount();
return (
<>
<span ref={domref}>
{count + effectCount + updateEffectCount + afterUpdateEffect}
</span>
<button
type="button"
onClick={() => {
setCount((a) => a + "a");
}}
>
reRender
</button>
</>
);
};
执行上面那段代码
将三个监听逻辑改为useMemo
你应该对为什么你点一下却有好几个请求调用有一些了解了
这个结果似乎有些不及预期,useEffect导致dom的渲染次数增加了,但为什么才一次?这个与useEffect的执行时机有关,更细致的可以见站内这篇和他里面的那篇 # useEffect 一定在页面渲染后才会执行吗?
2.2 性能差距
要让他符合预期,多次加载的加载很简单,只需要让渲染变得昂贵一点,这也更符合实际和预期
比如这样⬇️
<>
<span ref={domref}>
{count + effectCount + updateEffectCount + afterUpdateEffect}
</span>
<button
type="button"
onClick={() => {
setCount((a) => a + "a");
}}
>
reRender
</button>
{new Array(10000).fill(0).map(() => {
const key = Math.random().toString(36).slice(-8);
return <div key={key}>{key}</div>;
})}
</>
再进行一个简单的性能比较
<button
type="button"
onClick={() => {
console.time("start"); //button处开个定时
setCount((a) => a + "a");
}}
>
useEffect(() => {
console.timeEnd("start");
}, [afterUpdateEffect]); //最后一个变化完成时结束时定时
useEffect⬇️无用的中间态的渲染太多了,性能大致有3-4倍差距
将三个useEffect换成useMemo⬇️ 伟大!无须多言
3、UI组件函数执行不等于渲染
回到上面那个问题,应该可以明确
UI组件函数执行次数>单个useEffect组件的执行次数=dom的渲染次数(后两者的关系可能不是单纯的等于)
具体的机制就与React老生常谈的渲染流程机制有关了,站内的文章很多,本文就不赘述了(因为其实我也不是很了解😂),但细想也能明白,当你执行UI函数后发现还有更新项,你肯定就没有必要渲染这次中间态了
4、useWatch
用useMemo可能会由于lint规则,观念限制,没有Update等等原因限制你去更换,但很简单,我们用useMemo仅仅只是因为上面那三个条件他刚好都满足。所以一个useWatch很简单
//就这样,并不复杂
const useWatch = (callback: () => any, deps: any[]) => {
const [pre, SetPre] = useState<any[] | undefined>(undefined);
if (
deps.some((now, index) => {
if (!Array.isArray(pre)) return true;
return now !== pre[index];
})
) {
callback();
SetPre(deps);
}
};
useUpdateWatch
需要初始化不执行的watch,改一下初始值就行
const useWatch = (callback: () => any, deps: any[]) => {
const [pre, SetPre] = useState<any[] | undefined>(deps);
if (
deps.some((now, index) => {
if (!Array.isArray(pre)) return true;
return now !== pre[index];
})
) {
callback();
SetPre(deps);
}
};
useDeepCompareWatch useUpdateDeepCompareWatch
………………
比较方式换成深比较就行,懒得写了
5、结语
useEffect
是一个 React 钩子,可以让你 将组件与外部系统同步。(外部系统)- 推荐大伙看一下react官网上有关useEffect的说明和例子,挺有用的。并且我想说个暴论😆,如果你的函数实质上并不需要清除副作用,也不需要用到useEffect的伪生命周期机制,那么你就不应该用useEffect
- 上面知乎那篇评论有个说的好,许多人把useEffect当成了Vue中的watch在用。但在useEffect中同步更类似于这样的场景: 你没有发现Vue中的watch这个API,但任何变化都会走到onUpdated逻辑中,所以你在onUpdated中去检查是否是你的依赖项引起的变化,如果是,那再更新一次。这就是许多React开发者有意无意都在做的事
- 本文的作用最后应该是一种性能优化的问题和方案,把useEffect换成useMemo或者useWatch都不会解决你的数据依赖链过长并难以理解的问题,在代码开发中应该尽可能避免这种依赖链过长问题
- 以ahooks为例,乃至许多开源框架中都频繁的利用useEffect去当watch而仅仅为了同步状态,这是一种十分不妥的行为
Watch,在哪儿?
在实际开发中,我们必然面临需要有watch的情况,参数变更,需要同步变化
所以在函数组件中,watch在哪儿?
这不就是组件函数本身吗!只不过他的依赖类似于是: watch([props,allCustomState],Componnet)
而上面的useWatch只是帮你挑出是那个值变化了而已
去尝试一下把你项目仅仅只是为了同步state,而没有利用到他的伪生命周期或者同步外部系统而写的useEffect改成useMemo吧,应该会有些提升(应该,因为你下载的npm包中可能充斥着useEffect)