useCallback 与 componentDidUpdate 的对比及使用 useCallback 优化代码逻辑
本文是在:https://juejin.im/post/5ceb36dd51882530be7b1585 的基础上进行的探究,非常建议阅读原文
一、useCallback 与 componentDidUpdate 的区别
在 使用 useReducer 和 useCallback 解决 useEffect 依赖诚实与方法内置&外置问题 这篇文章中,我们使用 useCallback 解决了 useEffect 中的依赖诚实问题,并且做到了 useEffect 的间接依赖,通过 useCallback 将依赖变量移到了 useEffect 外部。
这个过程中我们的关注点是 依赖state/props 的变化对组件本身的影响 ,这会影响到我们的很多代码逻辑。
1、componentDidUpdate 带来的问题
在 class component 时代,判断 component 更新我们会使用 componentDidUpdate
,并且会拿到更新前的 props 和 state,然后在这个逻辑中判断我们是否需要做一些事情。
首先 componentDidUpdate 无法做到单个 prop 或者 state 依赖,是全局的,因此即使依赖的内容没发生变化,我们也得判断一次,否则就会重新取数据。
最尴尬的时候,如果传入的 prop 中包含了一个 function,并且 function 在父组件中依赖了某些父组件中的变量,这个变化会让人捉摸不透。
举个例子:
- 一个父组件中使用了两个 state ,分别是 count 和 step,同时一个
countNumber
方法,返回count + step
- 父组件会修改 step 和 count
- state 和 count 以及 countNumber 都会传给子组件
父组件
import React, { useEffect, useState, useCallback } from 'react';
import Child from './ClassComponent';
export default (props = {}) => {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const countNumber = () => {
return count + step;
}
const handleSetStep = () => {
setStep(step + 1);
setCount(count + step);
}
return (
<div>
<button onClick={handleSetStep}>set step is : {step} count is : {count}</button>
<Child countNumber={countNumber} count={count} step={step} />
</div>
);
}
子组件
import React, { Component } from 'react';
export default class Demo7Classs extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.countNumber();
}
componentDidUpdate(prevProps) {
console.log(prevProps, this.props);
console.log('---updated---');
if (prevProps.count !== this.props.count && prevProps.step !== this.props.step) {
console.log(`---- count number is ${this.props.countNumber()} ----`);
}
}
render() {
return (
<div>
<p>{this.props.count} -- {this.props.step} -- {this.props.countNumber()}</p>
</div>
);
}
}
子组件 componentDIdUpdate 中对依赖的使用
我们在子组件中判断了只有 count 和 step 都发生变化的时候才会重新取值,假设这是我们需要的逻辑
componentDidUpdate(prevProps) {
console.log(prevProps, this.props);
console.log('---updated---');
if (prevProps.count !== this.props.count && prevProps.step !== this.props.step) {
console.log(`---- count number is ${this.props.countNumber()} ----`);
}
}
如果少写了 count
和 step
中的任何一个,这次都不会触发重新计数。
这样带来的问题是什么?
很明显,这样带来的问题就是我们在子组件中显示的需要去理解 countNumber 中依赖的两个 state:count 和 step
一旦父组件这个相关的代码逻辑发生变化,所有的子组件都需要改动这块代码。
2.使用 useCallback 解决 componentDidUpdate 问题
class component 中使用 componentDidUpdate 有时候需要我们理解父组件的内部机制,否则很难明白依赖
使用 useCallback 可以方便的自己管理自己的依赖:
1、父组件
import React, { useEffect, useState, useCallback } from 'react';
import Child from './Child';
export default (props = {}) => {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const countNumber = useCallback(() => {
return count + step;
}, [count, step]);
const handleSetStep = () => {
setStep(step + 1);
setCount(count + step);
}
return (
<div>
<button onClick={handleSetStep}>set step is : {step} count is : {count}</button>
<Child countNumber={countNumber}/>
</div>
);
}
可以发现上面的代码中,父组件的 countNumber 通过 useCallback
包装了一下,并且内部依赖了 count 和 step 两个 state。
在传给子组件的时候,直接传入了 countNumber
这个包装后的方法
2、子组件
import React, { useEffect, useState, useCallback } from 'react';
export default (props = {}) => {
useEffect(() => {
console.log('.....')
console.log(props.countNumber());
}, [props.countNumber]);
return (
<div>
{props.countNumber()}
</div>
);
}
在上面的 class component 中,我们通过了在 componentDidUpdate 判断 count 和 state 是否发生变化来判断是否需要重新执行方法。
而 function component 的子组件中,我们在 useEffect 只依赖了 props.countNumber
:
useEffect(() => {
console.log('.....')
console.log(props.countNumber());
}, [props.countNumber]);
至于 countNumbe 内部依赖了什么我们并不关心
最终结果也是我们想要的,每次 count 或者 step 发生变化后,通过 useCallback 包装的方法在 props.countNumber 其实发生了变化,而子组件的 useEffect 也会重新执行
二、利用 useCallback 将函数抽离到组件外部
1、自定义 hook 包装 useCallback
之前我们实现的是通过 useCallback 将函数抽离到了 useEffect 外面,使得 useEffect 不会直接依赖某些变量
而某些时候,我们需要将函数抽离到整个组件的外面,实现代码的复用性和可读性
useCallback 的变量依赖上我们可以发现,一个 useCallback 依赖变化的变量我们可以通过传入的方式:
function a(count, step) {
return useCallback(() => {
return count + step;
}, [count, step])
}
既然已经有了这样的思路,那就可以通过一个自定义 hook 来实现:
const useCountNumber = (count, step) => {
console.log('----useCountNumber----');
return useCallback(() => {
return count + step;
}, [count, step]);
}
上面的 hook 传入了 count 和 step,在 useCallback 中依赖了这两个变量的变化,并且返回了 useCallback 包装后的结果。
在使用的过程中,我们只需要通过 useCountNumber 拿到包装后的方法,即可在 function component 中使用:
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const countNumber = useCountNumber(count, step);
useEffect(() => {
console.log(countNumber());
}, [countNumber]);
const handleSetStep = () => {
setStep(step + 1);
setCount(count + step);
}
上面代码中,const countNumber = useCountNumber(count, step);
拿到包装后的结果,在 useEffect 中只需依赖 countNumber 即可。
2、自定义 hook 包装 useCallback 带来的性能问题
从上面的截图中我们可以发现,每次变更 count 和 step 之后,countNumber() 都会因为依赖变化,而重新实例化并且执行一次。
但是对我们来说,我们只是希望在需要的时候 useCallback 里面的内容执行一次,而不是整个 useCallback 每次都要因为 count 和 step 的变化重新实例化
我们期望每次都拿到 count 和 step 的最新值,而不是每次都要通过依赖变化自动实例化依赖最新值的方法出来。
通过 useRef 我们可以将变量挂载到 useRef.current
上,之前在解决渲染闭包环境问题的时候有使用,具体可以参照:使用 useEffect 配合 useRef 在渲染闭包中总是获取最新值
在我们的自定义 hook 中,我们为了解决 useCallback 带来的性能问题(每次都会重新实例化一次 useCallback)
const useCountNumber2 = (count, step) => {
console.log('----useCountNumber[2]----');
const countRef = useRef(null);
const stepRef = useRef(null);
useEffect(() => {
console.log(`---number2--- count: ${count} - step: ${step}--`);
countRef.current = count;
stepRef.current = step;
});
return useCallback(() => {
return `number2 : ${countRef.current + countRef.current}`;
}, [countRef, stepRef]);
}
上面的代码证,我们可以发现,在自定义 hook 内部,我们通过 useEffect 每次都会将传入的 count 和 step 赋值给两个 ref.current,这样做的目的在于,在一次渲染闭包环境中,只要 count 和 step 改动了 ,那么 ref.current 中的值必然发生了改动
而在 useCallback 的依赖中,依赖的是两个 ref,首先这两个 ref 肯定是不会发生依赖变化的(浅比较),因此我们的 useCallback 不会每次在 count 和 step 发生变化的时候,实例化一个新的方法出来。
而在方法内部,通过 ref.current
即使不重新实例化,每次也能拿到最新的 count 和 step
两者的区别通过下面的代码可以作为比较:
onst useCountNumber = (count, step) => {
return useCallback(() => {
return `number1 : ${count + step}`;
}, [count, step]);
}
const useCountNumber2 = (count, step) => {
const countRef = useRef(null);
const stepRef = useRef(null);
useEffect(() => {
countRef.current = count;
stepRef.current = step;
});
return useCallback(() => {
return `number2 : ${countRef.current + stepRef.current}`;
}, [countRef, stepRef]);
}
export default (props = {}) => {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const countNumber = useCountNumber(count, step);
const countNumber2 = useCountNumber2(count, step);
useEffect(() => {
console.log('---re-useEffect--- useCountNumber')
}, [countNumber]);
useEffect(() => {
console.log('---re-useEffect--- useCountNumber2')
}, [countNumber2]);
const handleSetStep = () => {
setStep(step + 1);
setCount(count + step);
console.log(`handleSetStep number1: ${countNumber()}`)
console.log(`handleSetStep number2: ${countNumber2()}`)
}
return (
<div>
<button onClick={handleSetStep}>set step is : {step} count is : {count}</button>
</div>
);
}
结果:
通过上面的结果我们可以发现,每次点击触发更改 count 和 step 的时候,第一个 useCallback 封装的自定义 hook,每次都会执行一次重新实例化,而第二个则不会。
同样的输出结果,第二种方式除了第一次实例化之外,每次都能少实例化一次。
3、自定义 hook 解决 useCallback 重新实例化问题
上面虽然使用 useRef 解决了依赖问题,但本质上对函数代码改动比较大,后面维护可能也会出现问题。
如果按照 useRef 的方式,每次都会搞很多次 useRef 的使用,因此我们需要将其进行封装。
封装的基本思想是:
- 真实的计算逻辑发生的方法,通过参数的形式传入给 hook
- 依赖变化的 step 和 count 也通过参数的形式传入
- 将逻辑计算的方法赋值给
ref.current
- 无论是计算逻辑方法本身发生变化还是依赖的 state 发生变化,都会将 逻辑计算方法赋值给 ref.current
- 在 useCallback 中真正执行的方法是通过
ref.current
拿到的,保证每次拿到的都是最新的
核心代码如下:
const useOnceCallback = (fn, dependencies) => {
const ref = useRef(null);
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
具体代码:
import React, { useEffect, useState, useCallback, useRef } from 'react';
const useOnceCallback = (fn, dependencies) => {
const ref = useRef(null);
useEffect(() => {
ref.current = fn;
}, [fn, ...dependencies]);
return useCallback(() => {
const fn = ref.current;
return fn();
}, [ref]);
}
export default (props = {}) => {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const calNum = () => {
return count + step;
}
const countNumber = useOnceCallback(calNum, [count, step]);
useEffect(() => {
console.log('-----re-useEffect-----');
}, [countNumber])
const handleSetStep = () => {
setStep(step + 1);
setCount(count + step);
console.log(countNumber());
}
return (
<div>
<button onClick={handleSetStep}>set step is : {step} count is : {count}</button>
</div>
);
}
结果:
4、hack useCallback() 是不推荐的
上面我们用来优化 useCallback 多次实例化方法的这种方式非常的 hack ,React 官方不推荐使用这种方式。
官方推荐的方法是通过传递 dispatch
来解决类似的问题。
因为上面的这种方式,在同步渲染(同步渲染参考:https://zh-hans.reactjs.org/blog/2018/03/27/update-on-async-rendering.html)下可能存在问题。
向下传递回调的这种场景中,使用 dispatch
是最好的解决方案。(dispatch 不会在重新渲染之间变化)
四。代码地址:
useCallback 与 componentDidUpdate 的区别与实践:
useCallback 与 componentDidUpdate 的区别与实践:
自定义 hook 封装 useCallback 将函数抽离出组件:
自定义hook解决 useCallback 多次实例化方法带来的性能问题:
文章版权:Postbird-There I am , in the world more exciting!
本文链接:http://www.ptbird.cn/react-hook-useCallback.html
转载请注明文章原始出处 !