看过了文档后了解到useCallback与useMomo其实是类似的,都是缓存一个东西,useCallback缓存的函数引用而useMemo缓存的值。
拿useCallback来说,useCallback的第一个参数就是需要缓存的函数,第二个参数是依赖数组,和useEffect一样,当然,也有相同的captureValue特性(闭包特性)。
不过对于怎么使用总觉得多少弄不明白。
今天读到了这篇文章:when to useCallback and useMomo 才有了点感觉。
细品之下,方有内味嘞~
不过这篇文章其实可以算是笔记,并不是翻译。
一次代码优化的实践探索
下面一段代码写了一个组件 CandyDispenser(糖果分发器)。这段代码实现了一个按钮无序列表,每个列表表示一种糖果,当点击一个按钮时就从糖果列表中移除这个按钮。
看下代码:
在组件内部先定义了一个初始化糖果的数组,又将数组作为useState的初始化的值。
接着定义了一个函数dispense,作为每一个糖果(button)的onClick触发的函数,这个函数的功能就是更新当前的糖果数组(把所点击的糖果过滤掉)。
function CandyDispenser() {
const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
const [candies, setCandies] = React.useState(initialCandies)
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
return (
<div>
<h1>Candy Dispenser</h1>
<div>
<div>Available Candy</div>
{candies.length === 0 ? (
<button onClick={() => setCandies(initialCandies)}>refill</button>
) : (
<ul>
{candies.map(candy => (
<li key={candy}>
<button onClick={() => dispense(candy)}>grab</button> {candy}
</li>
))}
</ul>
)}
</div>
</div>
)
}
值得怀疑的优化1
听说useCallback可以优化回调函数。好的,那就先试试这样:
const dispense = React.useCallback(candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}, [])
优化了吗?That‘s a question!
像这样把原先的dispense用useCallback包裹了之后,背后的想法是:原先的dispense函数在每次渲染之时都会新创建一个函数实例,将这个实例赋值给dispense。修改后的代码,试图利用useEffect的第二个参数,空数组依赖的不变性来使得dispense函数只创建一次实例。
看上去是这样,但是并没有优化什么。
原因是,这段修改后的代码等效于这样。
const dispense = candy => {
setCandies(allCandies => allCandies.filter(c => c !== candy))
}
const dispenseCallback = React.useCallback(dispense, [])
这样的话更容易看出,当组件一次一次的渲染中,尽管这个例子中,useCallback的返回值一直都是最初的那个函数。但是,希望被优化掉的函数创建过程却在一次次的进行(并不会因为它是作为useCallback参数匿名函数而没有创建的过程)。
值得怀疑的优化2
每次渲染都 const initialCandies = [‘snickers’, ‘skittles’, ‘twix’, ‘milky way’] ,让我想起了useMemo。最初的学习过程就是为了使用而使用。
那就把代码改成这样。
const initialCandies = React.useMemo(
() => ['snickers', 'skittles', 'twix', 'milky way'],
[],
)
没错,是避免了每次渲染都创建新的数组又不使用的问题。不过,也带来了代价:函数创建,第一次的函数调用。
这个特定的场景里,最好的解决方案还是:
const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
function CandyDispenser() {
// ....
}
简单粗暴却有力量。
不过,这一大段文字的意义在于:优化代码是又代价的。尤其是为了优化而进行的优化,代码不需要优化的时候可以不用优化。
但是,useCallback和useMeno的问题依旧没有解决:他们使用场景是?
使用场景
对子组件对象属性的比较
As we know,组件自己可以实现渲染前的属性对比,来决定是不是要触发渲染。
对于class组件,可以通过重写shouldComponentUpdate方法,或者直接继承PureCompoent,利用其默认重写的的浅比较版的shouldComponentUpdate来控制渲染前的属性对比。
对于funciton组件,我们又React.memo()方法,在做着与class中shouldComponentUpdate一样的事情。(第二个参数不是必填的,如果没有,那就默认做属性的浅比较)
function MyComponent(props) {
}
/* 这个函数要也是对比属性,return true或者false 来决定某次渲染是否进行 */
function areEqual(prevProps, nextProps) {
if( ... ) return false;
return ...
}
/* 组件包裹一层memo方法 */
export default React.memo(MyComponent, areEqual);
组件自己负责判断自己是否渲染解决了90%无效渲染的问题,这种思路是’自己的命运自己掌握’。
对应的另外一种思路就是,父组件帮你做判断。
// 自组件
function Foo({bar, baz}) {
// 不关心
}
// 父组件
function Blub() {
const bar = React.useCallback(() => {}, [])
const baz = React.useMemo(() => [1, 2, 3], [])
return <Foo bar={bar} baz={baz} />
}
上面的代码中,不关系自组件内部,因为我们的判断逻辑全在父组件里面:给自组件传递的属性bar和baz一个用useCallback、一个用useMemo,当他们的第二个参数表示的依赖数组发生变化后,才会产生新的数组和函数。这样对于自组件来讲,在没有新属性的时候,自然不会渲染。
对比看shouldComponentUpdate,能解决属性的类型是基本类型(直接浅比较),一般对象包括数组(我可以自己实现比较规则)。但是,如果属性是函数,这种’自己的命运自己掌握’的方式就很棘手了。(我怎么比较两个引用地址不一样的函数对象,但是却是相同的逻辑?)
这就是为啥上面文中说“组件自己负责判断自己是否渲染解决了90%无效渲染的问题”。另外的10%正是属性是函数的时候。
的确,儿子解决不了的问题,只有老子亲自来看了。
鲜见的耗时计算
鲜,普通话三声,表示少。就是这种情况不常见。
不过问题只要存在就需要解决。这里召唤的是useMemo。
代码说事:
// 函数组件
function BetterWorld({ people, environment, animal }) {
// 这里在渲染前有一个大量计算过程
// 纯函数
// 依赖两个参数
const plans = calculate_HowToMakeTheWorldBetter(people, environment)
// 渲染计算过程
return <div>
To Make The World Better~ <br />
We can save { animal } and { environment }!<br />
And also,<br />
The Only way to save us!<br /><br />
Plan is Here! <br />
<p>{plans}</p>
</div>
}
BetterWorld组件接受的props有people、environment和animal。但是需要大量计算的函数calculate_HowToMakeTheWorldBetter()却没有依赖所有的props属性。试想下,像这样每次渲染都执行这么有分量的函数,势必浪费了大量的计算资源,因为这个函数是纯函数,参数决定了结果,算来算去结果没变。
这种情况下优化势在必行:
function BetterWorld({ people, environment, animal }) {
// 优化部分
// useMemo暂存了计算值
// 并保证了只有依赖数组有变化才重新计算函数值
React.useMemo(
() => calculate_HowToMakeTheWorldBetter(people, environment),
[people,environment],
);
// 渲染计算过程
return <div>
To Make The World Better~ <br />
We can save { animal } and { environment }!<br />
And also,<br />
The Only way to save us!<br /><br />
Plan is Here! <br />
<p>{plans}</p>
</div>
}
我们的优化部分利用useMemo,最初的耗时计算不可避免,但是结果被暂存了起来,并保证了只有需要重新计算的时候才重新计算(useMemo依赖数组有变化的时候就是需要再计算的时候)。
细品代码,以及代码的展现的深远意境和美好愿景。
问题和总结
注意:CaptureValue特性依旧存在
useEffect、useMemo、useCallback中常见的迷惑点就是被称作capture value的闭包特性。
这种特性表现在,如果传入这些hooks的作为参数的函数中,需要引用外部的有些变量,那么函数内部拿到的值是函数创建那一次渲染的变量值。
比方说下面的例子,在点击自组件“look count value”按钮之前一直点击增加次数,尽管count的值在增加,可创建useCallback只在第一次渲染,这时候的count值是0,所以最终alert显示的值就是0。
const ChildComp = function (props) {
return <button onClick={props.showFunc}>look count value</button>
}
function ParentComp () {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
const showFunc = useCallback(()=>{
alert(count);
return count;
}, [])
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp showFunc={showFunc}/>
</div>
);
}
Captrue Value如果想绕过就还是借助useRef。就是把最新的值用ref保存(ref.current),然后需要使用的地方使用这个ref的值。
const refValue = useRef()
refValue.current = count;
alert(refValue.current);
考量优化的代价
优化是有成本的。
useCallback 和 useMemo的显而易见的成本是:对于你的同事来说,你使代码更复杂了。在优化之前的考量是一个综合的过程。如果代码没有特别显著的性能和bug问题,就没必要大刀阔斧的改革。
简单小结
useCallback和useMemo尽管看上去很像。但是解决的问题有相同也各有针对。
他们的共同点主要是,解决在父组件级别通过控制向自组件传递的参数的变化,来决定自组件是否渲染。解决不必要渲染的问题。与shouldComponent和React.memo不同的是,shouldComponent、memo是依靠组件自身的判断来解决不必要渲染。
另外,在解决不必要的渲染问题时,useCallback能处理函数类型的属性。
useMemo可以减少组件内部可能产生的大量计算。