前言
在react
没有新增 hooks
之前,我们可以通过PureComponent
、shouldComponentUpdate
等方法进行性能优化,那么在hooks
中有没有性能优化的方法呢?答案是肯定的。下面我们就来一一进行解答。
一、官方文档的解释
下面是官方文档的解释,感觉有些惜字如金,看完之后,依旧有一种找不到北的感觉...
参考链接 useCallback
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a,b)
},
[a, b]
)
返回一个memoized
回调函数。
把内联回调函数
及依赖项数组
作为参数传入useCallback
,它将返回该回调函数的memoized
版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
。
注意
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
我们推荐启用 eslint-plugin-react-hooks
中的exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
返回一个 memoized
(记忆) 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized
值。这种优化有助于避免在每次渲染时都进行高开销的计算
。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
你可以把 useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React
可能会选择“遗忘”以前的一些 memoized
值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo
,以达到优化性能的目的。
注意
依赖项数组不会作为参数传给“创建”函数。虽然从概念上来说它表现为:所有“创建”函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
我们推荐启用 eslint-plugin-react-hooks
中的 exhaustive-deps
规则。此规则会在添加错误依赖时发出警告并给出修复建议。
看完了关于useCallback
和useMemo
的官方解释,大概可以总结出如下几点:
useCallback
接收一个内联回调函数
和一个依赖项数组
useMemo
接收一个创建函数
和一个依赖项数组
useCallback
和useMemo
都是在某个依赖项改变时才会更新- 他们都是性能优化的方法
useMemo
用于计算量较大的地方
纸上得来终觉浅,绝知此事要躬行
,下面我们就来写个例子
二、基础使用场景与对比
useCallback
的使用
useCallback
接收一个内联回调函数
和一个依赖项数组
,依赖项改变时才会更新。
1. 未使用useCallback
这个例子由两个组件构成,分别为<App />
和子组件<Child />
function App () {
const [count, setCount] = useState(0)
const [name, setName] = useState('fruit')
const changeCount = () => {
setCount(count + 1)
}
const changeName = () => {
setName(name + 1)
}
return (
<div>
<div>Count is {count}</div>
<div>Name is {name}</div>
<br/>
<div>
<button onClick={changeName}>Change Name</button>
<Child changeCount={changeCount} />
</div>
</div>
)
}
function Child({changeCount}) {
console.log('Child render')
return <div>
<button onClick={changeCount}>Increment Count</button>
</div>
}
render(<App />, document.getElementById('root'))
stackblitz.com/edit/react-… 可以通过此在线编辑器进行尝试
如上图所示,当我们点击Change Name
和Increment Count
两个按钮时,都会导致,Child
组件的重新渲染执行。就会造成非常大的开销。当点击Change Name
的时候,并未更改Increment Count
,但依旧会造成子组件重新执行。
2. 通过memo
优化<Child />
组件
之前的做法是用componentWillReceiveProps
、shouldComponentUpdate
、pureComponent
等来做性能优化。在hooks
中我们用memo
来做优化。
改造后的<Child />
如下:
const Child = memo(({changeCount}) => {
console.log('Child render')
return <div>
<button onClick={changeCount}>Increment Count</button>
</div>
})
但是此时我们点击Change Name
和Increment Count
两个按钮时,发现好像并没有什么用。
原因是什么呢?当我们将changeCount
作为props
传递给其他组件时会导致像PureComponent、shouldComponentUpdate、React.memo
等相关优化失效(因为每次都是不同的函数)。
当我们传递changeCount
的时候,每次我们点击按钮,函数都会被重新执行,就会创建新的changeCount
,每次的指针地址都不一样
,因此就会导致每次的对比都不一样,因此我们还需要通过使用useCallback
来解决这个问题。
memo
类似于PureComponent
,函数的props
做一个浅比较,但useMemo
是用来缓存值的,因此他们是不同的。
3. useCallback
上场
function App () {
const [count, setCount] = useState(0)
const [name, setName] = useState('fruit')
// 这行是重点
const changeCount = useCallback(() => {
setCount(count + 1)
}, [count])
const changeName = () => {
setName(name + 1)
}
return (
<div>
<div>Count is {count}</div>
<div>Name is {name}</div>
<br/>
<div>
<button onClick={changeName}>Change Name</button>
<Child changeCount={changeCount} />
</div>
</div>
)
}
const Child = memo(({changeCount}) => {
console.log('Child render')
return <div>
<button onClick={changeCount}>Increment Count</button>
</div>
})
stackblitz.com/edit/react-… 可以在这里进行试验,当点击Increment Count
时Child
会正常执行,当点击Change Name
时则Child
不再执行。这样就达到了优化的目的。
我们可以尽量对function
包一层useCallback
。
因此,可以通过 memo + useCallback
来达到不被附带更新的情况。但也会占用一定的内存。
useMemo
的使用
useMemo
和useCallback
在使用上几乎是一样的,但useMemo
是用来缓存值的。
useMemo
一般用于密集型计算大的一些缓存。通过useMemo
的依赖我们就可以只在指定变量值更改时才执行计算,从而达到节约内存消耗。
// 在Hooks中获取上一次指定的props
const usePrevProps = value => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
});
return ref.current;
}
function App() {
const [count, setCount] = React.useState(0);
const [total, setTotal] = React.useState(0);
const calcValue = React.useMemo(() => {
return Array(100000).fill('').map(v => /*一些大量计算*/ v);
}, [count]);
const handleCount = () => setCount(count => count + 1);
const handleTotal = () => setTotal(total + 1);
const prevCalcValue = usePrevProps(calcValue);
console.log('两次计算结果是否相等:', prevCalcValue === calcValue);
return (
<div>
<div>Count is {count}</div>
<div>Total is {total}</div>
<br/>
<div>
<button onClick={handleCount}>Increment Count</button>
<button onClick={handleTotal}>Increment Total</button>
</div>
</div>
)
}
ReactDOM.render(<App />, document.body)
这次我们重点看这行,只有当count
变量值改变的时候才会执行useMemo
第一个入参的函数。
const calcValue = React.useMemo(() => {
return Array(100000).fill('').map(v => /*一些大量计算*/ v);
}, [count])
通过useMemo
的依赖我们就可以只在指定变量值更改时才执行计算,从而达到节约内存消耗。
useMemo
把创建函数作为依赖项数组传入useMemo
,和useEffect
有些类似,传入的函数改变只会依赖于传入依赖项数组中的值改变时会重新计算,重新执行函数,这个优化有助于避免在每次渲染的时候都进行高开销的计算。和useEffect
的第二个参数一样,只有当第二个参数的值改变的时候才会进行计算。
三、源码解析
// useCallback
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)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
// useMemo
function updateMemo(nextCreate, 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)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
从上面的代码可以看出useMemo
和useCallback
几乎是一样的,当我们理解了useCallback
后理解useMemo
就非常简单。
他们唯一的区别是:useCallback
是根据依赖项(deps
)缓存第一个参数callback
。useMemo
是根据依赖项(deps
)缓存第一个参数callback
执行后的值
。
useCallback
会重新返回一个函数体
,而useMemo
返回的是一个缓存计算数据的值
,当依赖项num
变化时,usecallback
会重新创建一个函数体,而useMemo
不会。
反思
使用useMemo
和useCallback
一定能达到优化的目的吗?
答案是否定的。
useMemo
和useCallback
不能盲目使用,因为他们都是基于闭包实现的,闭包会占用内存。- 当依赖项频繁改动时,要考虑
useMemo、useCallback
是否划算,因为useCallback
会频繁创建函数体。useMemo
会频繁创建回调。
总结
useMemo
和useCallback
可以用来解决性能问题useCallback
需要与memo
配合使用useMemo
与memo
是完全不同的useMemo
和useCallback
的源码几乎是一样的,useCallback
返回的是回调函数,useMemo
返回的是值useMemo
一般用于计算量比较大的情况useMemo
和useCallback
不一定能达到性能优化的目的,因此不能盲目使用
就写到这里吧,如果错误欢迎指正...