@React Hook ---- useCallback详解
React Hooks 学习之 useCallback实践
前言:
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
本文主要介绍React中内置的Hook API — useCallback。
在开始阅读之前,本文默认读者已对React Hook有基础了解,如果你刚开始接触 Hook,那么可能需要先查阅 Hook 概览,再阅读本文。
useCallback语法
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
该方法返回一个 memoized(记忆化)回调函数 ,什么是记忆化函数呢?后文会就JavaScript中的Memoization做简单的介绍。
useCallback 的第一个参数是一个函数用来执行一些操作和计算。第二个参数是一个数组,当这个数组里面的值改变时 useCallback回调函数会重新执行,更新这个匿名函数里面引用到的值,当数组里面的值没有改变时,会返回该回调函数的 memoized 版本。
这样描述可能有点不太好理解,下面看一个例子:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: [1, 2, 3],
name: '123'
};
}
handleChangeNum = () => {
this.setState({
value: [4, 5, 6]
});
}
render() {
const {value, name} = this.state;
return (
<div className="App">
<button onClick={this.handleChangeNum}>修改传入的value值</button>
<TestUseCallback value={value} name={name} />
</div>
);
}
}
我们写一个简单的demo, 当点击按钮时,改变组件的value值,观察当传入子组件的value值发生改变时,以下三种情况下,useCallback函数的返回值。
第一种情况:
当依赖项数组为空时
function TestUseCallback({value, name}) {
const memoizedCallback = useCallback(
() => {
return value;
},
// 此处为空
[],
);
console.log('记忆中 value > ', memoizedCallback(), name);
console.log('修改后 value > ', value, name);
return (
<div>
<p>TestUseCallback</p>
</div>
);
}
可以看到,当依赖项数组为空时,即使value值发生了改变, memoized函数依然返回缓存中的value值。
第二种情况:
当依赖项数组有值,但是这个值没有发生改变
function TestUseCallback({value, name}) {
const memoizedCallback = useCallback(
() => {
return value;
},
// 此处添加依赖数组,但是name值没有发生变化
[name],
);
console.log('记忆中 value > ', memoizedCallback(), name);
console.log('修改后 value > ', value, name);
return (
<div>
<p>TestUseCallback</p>
</div>
);
}
可以看到,当依赖数组中的值没有发生改变时,memoized函数依然返回缓存中的value值。
第三种情况:
当依赖项数组有值,且这个值发生了变化
function TestUseCallback({value, name}) {
const memoizedCallback = useCallback(
() => {
return value;
},
// 此处添加依赖数组,且value值发生改变
[value],
);
console.log('记忆中 value > ', memoizedCallback(), name);
console.log('修改后 value > ', value, name);
return (
<div>
<p>TestUseCallback</p>
</div>
);
}
可以看到,当依赖项组织中的值发生改变之后,callback函数返回的不再是缓存中的数据,而是经过重新计算的最新value值。
通过上面简单的demo用例,相信大家已经对useCallback函数的语法及使用有了基础的认识。
那么这个函数是怎么实现的,以及我们应该什么时候使用这个方法呢?下面我们先介绍一下什么是Memoization,也就是javascript缓存。
Memoization
为什么会出现Memoization这个概念,首先我们渴望可以提高应用程序的性能,当一个复杂计算需要占用大量的CPU时间的时候,我们希望可以把这个结果缓存下来,当下一次调用这个计算方法的时候可以直接从缓存中读取数据,而不是重新进行一次耗时计算。
Memoization是JavaScript中的一种技术,通过缓存结果并在下一个操作中重新使用缓存来加速查找费时的操作,简单来说就是将纯函数的运算结果记录下来,下一次调用时可以通过缓存来加速,是一种用空间换时间的性能优化技术。
技术实现:
function memoize(fn) {
return function () {
var args = Array.prototype.slice.call(arguments)
fn.cache = fn.cache || {};
return fn.cache[args] ? fn.cache[args] : (fn.cache[args] = fn.apply(this,args))
}
}
我们可以看到这段代码接收另外一个函数作为参数并返回。
要使用此函数,我们调用memoize将要缓存的函数作为参数传递。
使用方法:
比如著名的斐波那契系列(Fibonacci)
function fibonacci(num) {
if (num == 1 || num == 2) {
return 1
}
return fibonacci(num-1) + fibonacci(num-2)
}
const memFib = memoize(fibonacci)
console.log('profiling tests for fibonacci')
console.time("non-memoized call")
console.log(memFib(6))
console.timeEnd("non-memoized call")
console.time("memoized call")
console.log(memFib(6))
console.timeEnd("memoized call")
可以看到,一个很小的数字,通过缓存方法,运行效率就已经有了不小的提高。
参考文献:https://juejin.im/post/5bf7c563e51d452d705fe8d1#heading-5
源码参考:
export function useCallback<T>(
callback: T,
inputs: Array<mixed> | void | null,
): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
// 需要保存下来的依赖数组,用作下次取用的key
const nextInputs =
inputs !== undefined && inputs !== null ? inputs : [callback];
const prevState = workInProgressHook.memoizedState; // 获取之前缓存的值
if (prevState !== null) {
const prevInputs = prevState[1];
if (areHookInputsEqual(nextInputs, prevInputs)) {
// 如果依赖数组中的值不变,直接返回缓存中的值
return prevState[0];
}
}
// 如果值改变,将新的值存入缓存
workInProgressHook.memoizedState = [callback, nextInputs];
return callback;
}
使用场景
useCallback到底是用来做什么的?应该什么时候使用?不知道大家有没有思考过这个问题,很多人似乎觉得不管什么情况,只要用 useMemo 或者 useCallback 「包裹一下」,似乎就能使应用远离性能的问题,其实这不是绝对的。
首先,我们需要知道 useCallback本身也有开销。刚才讲缓存时有讲到,useCallback会把某些值记录下来,这是一个空间换时间的过程,会消耗计算机内存,并且会将依赖数组中的值取出来和上一次记录的值进行比较,这需要消耗计算机的计算资源。因此,过度使用 useCallback 可能会影响程序的性能。
接下来我们就来研究一下useCallback的使用场景。
一 、useCallback的合理适用场景
1、有些计算开销很大,我们就需要缓存它的返回值,避免每次 render 都去重新计算。
这种情况很好理解,我们引入缓存这个概念,就是用来解决这种大运算量的问题。
2、由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。
我们都知道子组件的重新渲染取决于父组件传递的props的值是否发生变化,如果我们props的值是一个引用类型的数据(Array, Object, Function),而这个数据的指向一但发生变化,那么就算这个数据的值实际上并没有发生改变,子组件也会重新渲染。
举例:
function Example() {
const users = [1, 2, 3];
return <ExpensiveComponent users={users} />
}
以上面代码举例,每当Example组件render的时候,users变量都会被重新赋值,尽管每次users里面的数据都并没有发生改变,但由于users是引用数据,每次渲染时,users的指向都会发生变化,引发ExpensiveComponent子组件的重新渲染。
如果我们想在重新渲染时保持值的引用不变,不用每次都触发子组件的重新渲染,就可以用useCallback方法
function Example() {
const users = useCallback(() => [1, 2, 3], [])();
return <ExpensiveComponent users={users} />
}
这样每次render的时候,users的值就不会发生变化,子组件就不会重新渲染。
另外,应用useMemo和useRef都可以达到类似的效果
const users = useMemo(() => [1, 2, 3], []);
const {current: users} = useRef([1, 2, 3]);
二、无需使用 useCallback 的场景
1、如果返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括动态声明的 Symbol),一般不需要使用 useCallback。
2、仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件),且没有用到其他 Hook 的依赖数组中,一般不需要使用 useCallback。