你真的需要useMemo和useCallback吗
本文深入探讨了React中的useMemo和useCallback的使用场景和性能影响。useMemo用于缓存计算结果以减少不必要的计算,适合在计算开销较大的情况下使用,而useCallback用于缓存函数定义以避免不必要的组件刷新。作者强调,过度使用这两个Hook可能导致性能下降,建议在实际需要时再使用,并提出了其他优化方案,如使用useReducer和合理组织组件逻辑。
在React中,useRef和useEffect常用于管理组件的可变值和副作用,但在响应DOM元素挂载时,使用useCallback作为ref的回调函数更为有效。useCallback确保在元素挂载和卸载时被调用,避免了因useRef导致的生命周期回调未触发的问题。文中还介绍了其他几种优化Node操作的方法,如使用useState、useStateRef和useRefWithCallback,以提高性能和管理复杂性。
原文:useCallback Might Be What You Meant By useRef & useEffect
在React中,useRef
和useEffect
是常用的钩子函数,用于不同的目的。
useRef
通常用于保存在组件渲染之间持久存在的可变值,而useEffect
用于执行诸如数据获取、订阅或DOM操作等副作用。
然而,当涉及对React元素挂载做出响应时,有一个更好的选择:useCallback
。
如果你想要对React元素在DOM中的挂载做出响应,你可能会尝试使用useRef来获取它的引用,并使用useEffect来响应其挂载和卸载。但这样做是无效的。
这是因为当组件被(卸载)挂载并通过useRef
连接到ref.current
时,ref.current
的更改并没有触发回调或重新渲染,自然也不会触发预期的生命周期回调。
甚至react-hooks 的ESLint规则也会对此发出警告。请注意,无论是ref还是ref.current作为useEffect的依赖项,都不会触发它的执行(这是useRef的特性决定的)。
下面的例子展示了上面的方法确实不可行的(在Code Sandbox中尝试)。
export default function App() {
const [count, setCount] = useState(1);
const shouldShowImageOfCat = count % 3 === 0;
const [catInfo, setCatInfo] = useState(false);
// notice how none of the deps of useEffect
// manages to trigger the hook in time
const catImageRef = useRef();
useEffect(() => {
console.log(catImageRef.current);
setCatInfo(catImageRef.current?.getBoundingClientRect());
// notice the warning below
}, [catImageRef, catImageRef.current]);
return (
<div className="App">
<h1>useEffect & useRef vs useCallback</h1>
<p>
An image of a cat would appear on every 3rd render.
<br />
<br />
Would our hook be able to make the emoji see it?
<br />
<br />
{catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
{catInfo ? `, it's height is ${catInfo.height}` : ""}!
</p>
<input disabled value={`render #${count}`} />
<button onClick={() => setCount((c) => c + 1)}>next render</button>
<br />
{shouldShowImageOfCat ? (
<img
ref={catImageRef}
src={catImageUrl}
alt="cat"
width="50%"
style={{ padding: 10 }}
/>
) : (
""
)}
</div>
);
}
那我们应该怎么做呢?使用useCallback。(参考react官方文档)
我们可以依赖于将一个普通函数通过useCallback包装后传递给ref,并对它返回的最新DOM节点引用做出反应。
下面的代码是一个例子(在Code Sandbox中尝试):
export default function App() {
const [count, setCount] = useState(1);
const shouldShowImageOfCat = count % 3 === 0;
const [catInfo, setCatInfo] = useState(false);
// notice how this is a useCallback
// that's used as the "ref" of the image below
const catImageRef = useCallback((catImageNode) => {
console.log(catImageNode);
setCatInfo(catImageNode?.getBoundingClientRect());
}, []);
return (
<div className="App">
<h1>useEffect & useRef vs useCallback</h1>
<p>
An image of a cat would appear on every 3rd render.
<br />
<br />
Would our hook be able to make the emoji see it?
<br />
<br />
{catInfo ? "😂" : "😩"} - I {catInfo ? "" : "don't"} see the cat 🐈
{catInfo ? `, it's height is ${catInfo.height}` : ""}!
</p>
<input disabled value={`render #${count}`} />
<button onClick={() => setCount((c) => c + 1)}>next render</button>
<br />
{shouldShowImageOfCat ? (
<img
ref={catImageRef}
src={catImageUrl}
alt="cat"
width="50%"
style={{ padding: 10 }}
/>
) : (
""
)}
</div>
);
}
在上面的例子中,我们可以看到useCallback
被传入了ref
参数中,这个过程中我们需要注意以下两点:
useCallback
*来包装ref回调函数。因为如果没有使用useCallback
,在ref回调函数中引发重新渲染,会导致ref回调函数再次以null触发,可能导致无限循环,这是由于React内部机制所致。实际上这种模式可以以多种方式使用,以下三种方法渐进地对Node操作进行了优化:
由于useState
是在渲染之间保持一致的函数,它也可以用作ref。在这种情况下,整个节点将保存在state中。
作为一个状态,当它发生变化时,它会触发重新渲染,并且可以安全地在渲染结果和useEffect的依赖项中使用该状态:
const [node, setRef] = useState(null);
useEffect(() => {
if (!node) {
console.log('unmounted!');
return null;
}
console.log('mounted');
const fn = e => console.log(e);
node.addEventListener('mousedown', fn);
return () => node.removeEventListener('mousedown', fn);
}, [node])
访问DOM是一项昂贵的操作,因此我们希望尽可能少地进行这样的操作。
如果你不需要像之前的钩子函数中那样保存整个节点,最好只在一个状态中保存其中的一部分:
// the hook
function useStateRef(processNode) {
const [node, setNode] = useState(null);
const setRef = useCallback(newNode => {
setNode(processNode(newNode));
}, [processNode]);
return [node, setRef];
}
// how it's used
const [clientHeight, setRef] = useStateRef(node => (node?.clientHeight || 0));
useEffect(() => {
console.log(`the new clientHeight is: ${clientHeight}`);
}, [clientHeight])
// <div ref={setRef}....
// <div>the current height is: {clientHeight}</div>
然而,有时为了性能考虑,你可以在使用ref的元素挂载和卸载时避免触发重新渲染。
下面的钩子函数不会将节点保存在状态中。它直接响应挂载和卸载,因此不会触发任何重新渲染。
// the hook
function useRefWithCallback(onMount, onUnmount) {
const nodeRef = useRef(null);
const setRef = useCallback(node => {
if (nodeRef.current) {
onUnmount(nodeRef.current);
}
nodeRef.current = node;
if (nodeRef.current) {
onMount(nodeRef.current);
}
}, [onMount, onUnmount]);
return setRef;
}
const onMouseDown = useCallback(e => console.log('hi!', e.target.clientHeight), []);
const setDivRef = useRefWithCallback(
node => node.addEventListener("mousedown", onMouseDown),
node => node.removeEventListener("mousedown", onMouseDown)
);
// <div ref={setDivRef}
在上面这个例子中,setRef
函数被传入了ref
中,只有在DOM节点发生变化时才会调用它来更新ref
,借此实现了生命周期函数。
unmount
周期,再更新ref,调用其mount
周期null
,则直接更新ref并调用其mount
周期即可最终,如果你理解了将useCallback用作元素ref的原理,你可能会根据自己的特定需求提出自己的想法。
官方文档:使用ref回调 - react.dev
众所周知,ref
并非普通的props,直观地看就是我们无法从props中直接解构出ref
。甚至在React.createElement
中,ref
参数早早地就被抽离了出来。
虽然useRef
被我们当作存储视图无关变量的一般方法,但ref
本身作为props传递时却有些许不同,它可以接收三种类型的参数:
(ref) ⇒ void
ref
对象这三种类型的ref
具有统一的更新时机:在组件的layout
阶段(或者说加载完成)时调用或赋值自己的ref参数。
作者: ChlorineC
创建于: 2023-09-04 00:00:00
更新于: 2025-01-19 04:57:00
版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
本文深入探讨了React中的useMemo和useCallback的使用场景和性能影响。useMemo用于缓存计算结果以减少不必要的计算,适合在计算开销较大的情况下使用,而useCallback用于缓存函数定义以避免不必要的组件刷新。作者强调,过度使用这两个Hook可能导致性能下降,建议在实际需要时再使用,并提出了其他优化方案,如使用useReducer和合理组织组件逻辑。
深入探讨React Hooks的设计原则、实现机制及其在函数式组件中的应用,强调声明式渲染的优势,并提供自定义Hooks的实现示例,如useAsync,用于处理异步请求和状态管理。
作者分享了个人博客框架迭代历程:从Hexo迁移到Astro的尝试,到发现Elog平台带来的启发,最终思考将Notion作为内容管理系统的新方案。文章重点描述了作者对博客系统的核心需求——解决创作同步问题,以及在探索NotionNext等解决方案过程中遇到的技术限制和思考。
本文探讨了前端开发的复杂性,提倡回归以后端为中心的架构,强调使用简单的工具和技术,如模板引擎、jQuery、alpine.js和htmx,来简化开发流程。作者认为,现代前端工具链的复杂性逐渐偏离了简化开发的初衷,建议在不引入过多复杂性的情况下,利用Web Components等标准技术来实现可复用组件,从而提高开发效率。