在编写 React Hook 代码时,useCallback
和useMemo
时常使人感到困惑。尽管咱们知道他们的功能都是作缓存并优化性能,可是又会担忧由于使用方法不正确致使负优化。本文将阐述useCallback
和useMemo
在开发中常见的使用方式和误区,并结合源码剖析缘由,知其然并知其因此然。javascript
useCallback
考察以下示例:html
import React from 'react'; function Comp() { const onClick = () => { console.log('打印'); } return <div onClick={onClick}>Comp组件</div> } 复制代码
当Comp
组件自身触发刷新或做为子组件跟随父组件刷新时,咱们注意到onClick
会被从新赋值。为了"提高性能",使用useCallback
包裹onClick
以达到缓存的目的:java
import React, { useCallback } from 'react'; function Comp() { const onClick = useCallback(() => { console.log('打印'); }, []); return <div onClick={onClick}>Comp组件</div> } 复制代码
那么问题来了,性能到底有没有得到提高?答案是非但没有,反而不如之前了;咱们改写代码的逻辑结构以后,缘由就会很是清晰:react
import React, { useCallback } from 'react'; function Comp() { const onClick = () => { console.log('打印'); }; const memoOnClick = useCallback(onClick, []); return <div onClick={memoOnClick}>Comp组件</div> } 复制代码
每一行多余代码的执行都产生消耗,哪怕这消耗只是 CPU 的一丁点热量。官方文档指出,无需担忧建立函数会致使性能问题,因此使用useCallback
来改造该场景下的组件,咱们并未得到任何收益(函数仍是会被建立),反而其带来的成本让组件负重(须要对比依赖是否发生变化),useCallback
用的越多,负重越多。站在 javascript 的角度,当组件刷新时,未被useCallback
包裹的方法将被垃圾回收并从新定义,但被useCallback
所制造的闭包将保持对回调函数和依赖项的引用。缓存
useCallback
的正确使用方法产生误区的缘由是useCallback
的设计初衷并不是解决组件内部函数屡次建立的问题,而是减小子组件的没必要要重复渲染。实际上在 React 体系下,优化思路主要有两种:markdown
因此考察以下场景:闭包
import React, { useState } from 'react'; function Comp() { const [dataA, setDataA] = useState(0); const [dataB, setDataB] = useState(0); const onClickA = () => { setDataA(o => o + 1); }; const onClickB = () => { setDataB(o => o + 1); } return <div> <Cheap onClick={onClickA}>组件Cheap:{dataA}</div> <Expensive onClick={onClickB}>组件Expensive:{dataB}</Expensive> </div> } 复制代码
Expensive
是一个渲染成本很是高的组件,但点击Cheap
组件也会致使Expensive
从新渲染,即便dataB
并未发生改变。缘由就是onClickB
被从新定义,致使 React 在 diff 新旧组件时,断定组件发生了变化。这时候useCabllback
和memo
就发挥了做用:函数
import React, { useState, memo, useCallback } from 'react'; function Expensive({ onClick, name }) { console.log('Expensive渲染'); return <div onClick={onClick}>{name}</div> } const MemoExpensive = memo(Expensive); function Cheap({ onClick, name }) { console.log('cheap渲染'); return <div onClick={onClick}>{name}</div> } export default function Comp() { const [dataA, setDataA] = useState(0); const [dataB, setDataB] = useState(0); const onClickA = () => { setDataA(o => o + 1); }; const onClickB = useCallback(() => { setDataB(o => o + 1); }, []); return <div> <Cheap onClick={onClickA} name={`组件Cheap:${dataA}`}/> <MemoExpensive onClick={onClickB} name={`组件Expensive:${dataB}`} /> </div> } 复制代码
memo
是 React v16.6.0 新增的方法,与 PureComponent 相似,前者负责 Function Component 的优化,后者负责 Class Component。它们都会对传入组件的新旧数据进行浅比较,若是相同则不会触发渲染。工具
因此useCallback
保证了onClickB
不发生变化,此时点击Cheap
组件不会触发Expensive
组件的刷新,只有点击Expensive
组件才会触发。在实现减小没必要要渲染的优化过程当中,useCallback
和memo
是一对利器。运行示例代码oop
useCallback
源码以下:
// 初始化阶段 function mountCallback(callback, deps) { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; } // 更新阶段 function updateCallback(callback, deps) { const hook = updateWorkInProgressHook();, const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { if (nextDeps !== null) { const prevDeps = prevState[1]; // 比较是否相等 if (areHookInputsEqual(nextDeps, prevDeps)) { // 若是相等,返回旧的 callback return prevState[0]; } } } hook.memoizedState = [callback, nextDeps]; return callback; } 复制代码
核心逻辑就是比较deps
是否发生变化,若是有变化则返回新的callback
函数,不然返回原函数。其中比较方法areHookInputsEqual
内部实际调用了 React 的is
工具方法:
// 排除如下两种特殊状况: // +0 === -0 // true // NaN === NaN // false function is(x: any, y: any) { return ( (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y); ); } 复制代码
useMemo
import React, { useMemo } from 'react'; function Comp() { const v = 0; const memoV = useMemo(() => v, []); return <div>{memoV}</div>; } 复制代码
建立memoV
的开销是没有必要的,缘由与第一节提到的相同。只有当建立行为自己会产生高昂的开销(好比计算上千次才会生成变量值),才有必要使用useMemo
,固然这种场景少之又少。
useMemo
的正确使用方法前文咱们提到,优化 React 组件性能的两个主要思路之一是减小计算量,这也是useMemo
的用武之地:
import React, { useMemo } from 'react'; function Comp({ a, b }) { const v = 0; const calculate = (a, b) => { // ... complex calculation return c; } const memoV = useMemo((a, b) => v, [a, b]); return <div>{memoV}</div>; } 复制代码
React Hook 对团队的协做一致性要求很是高,useCallback
和useMemo
这一对方法就是很好的示例,更复杂的场景还有对useRef
、自定义 Hook 的使用等等。从经验上来看,团队在进行 Hook 编码时须要特别增强 code review,不然很容易出现难以定位的 bug 或性能问题。当前 Hook 的各种方法还不完善,推特上争论也不少,期待 React 后续版本提供出更成熟易用的方案。