看过了文档后了解到useCallback与useMomo其实是类似的,都是缓存一个东西,useCallback缓存的函数引用而useMemo缓存的值。
拿useCallback来说,useCallback的第一个参数就是需要缓存的函数,第二个参数是依赖数组,和useEffect一样,当然,也有相同的captureValue特性(闭包特性)。

不过对于怎么使用总觉得多少弄不明白。
今天读到了这篇文章:when to useCallback and useMomo 才有了点感觉。
细品之下,方有内味嘞~
不过这篇文章其实可以算是笔记,并不是翻译。

一次代码优化的实践探索

下面一段代码写了一个组件 CandyDispenser(糖果分发器)。这段代码实现了一个按钮无序列表,每个列表表示一种糖果,当点击一个按钮时就从糖果列表中移除这个按钮。
看下代码:
在组件内部先定义了一个初始化糖果的数组,又将数组作为useState的初始化的值。
接着定义了一个函数dispense,作为每一个糖果(button)的onClick触发的函数,这个函数的功能就是更新当前的糖果数组(把所点击的糖果过滤掉)。

  1. function CandyDispenser() {
  2. const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
  3. const [candies, setCandies] = React.useState(initialCandies)
  4. const dispense = candy => {
  5. setCandies(allCandies => allCandies.filter(c => c !== candy))
  6. }
  7. return (
  8. <div>
  9. <h1>Candy Dispenser</h1>
  10. <div>
  11. <div>Available Candy</div>
  12. {candies.length === 0 ? (
  13. <button onClick={() => setCandies(initialCandies)}>refill</button>
  14. ) : (
  15. <ul>
  16. {candies.map(candy => (
  17. <li key={candy}>
  18. <button onClick={() => dispense(candy)}>grab</button> {candy}
  19. </li>
  20. ))}
  21. </ul>
  22. )}
  23. </div>
  24. </div>
  25. )
  26. }

代码的确不难懂,很好理解,但是问题是我们希望优化它。

值得怀疑的优化1

听说useCallback可以优化回调函数。好的,那就先试试这样:

  1. const dispense = React.useCallback(candy => {
  2. setCandies(allCandies => allCandies.filter(c => c !== candy))
  3. }, [])

优化了吗?That‘s a question!
像这样把原先的dispense用useCallback包裹了之后,背后的想法是:原先的dispense函数在每次渲染之时都会新创建一个函数实例,将这个实例赋值给dispense。修改后的代码,试图利用useEffect的第二个参数,空数组依赖的不变性来使得dispense函数只创建一次实例。
看上去是这样,但是并没有优化什么。
原因是,这段修改后的代码等效于这样。

  1. const dispense = candy => {
  2. setCandies(allCandies => allCandies.filter(c => c !== candy))
  3. }
  4. const dispenseCallback = React.useCallback(dispense, [])

这样的话更容易看出,当组件一次一次的渲染中,尽管这个例子中,useCallback的返回值一直都是最初的那个函数。但是,希望被优化掉的函数创建过程却在一次次的进行(并不会因为它是作为useCallback参数匿名函数而没有创建的过程)。

值得怀疑的优化2

每次渲染都 const initialCandies = [‘snickers’, ‘skittles’, ‘twix’, ‘milky way’] ,让我想起了useMemo。最初的学习过程就是为了使用而使用。
那就把代码改成这样。

  1. const initialCandies = React.useMemo(
  2. () => ['snickers', 'skittles', 'twix', 'milky way'],
  3. [],
  4. )

没错,是避免了每次渲染都创建新的数组又不使用的问题。不过,也带来了代价:函数创建,第一次的函数调用。
这个特定的场景里,最好的解决方案还是:

  1. const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
  2. function CandyDispenser() {
  3. // ....
  4. }

简单粗暴却有力量。

不过,这一大段文字的意义在于:优化代码是又代价的。尤其是为了优化而进行的优化,代码不需要优化的时候可以不用优化。
但是,useCallback和useMeno的问题依旧没有解决:他们使用场景是?

使用场景

其实是值得使用的场景。

对子组件对象属性的比较

As we know,组件自己可以实现渲染前的属性对比,来决定是不是要触发渲染。
对于class组件,可以通过重写shouldComponentUpdate方法,或者直接继承PureCompoent,利用其默认重写的的浅比较版的shouldComponentUpdate来控制渲染前的属性对比。
对于funciton组件,我们又React.memo()方法,在做着与class中shouldComponentUpdate一样的事情。(第二个参数不是必填的,如果没有,那就默认做属性的浅比较)

  1. function MyComponent(props) {
  2. }
  3. /* 这个函数要也是对比属性,return true或者false 来决定某次渲染是否进行 */
  4. function areEqual(prevProps, nextProps) {
  5. if( ... ) return false;
  6. return ...
  7. }
  8. /* 组件包裹一层memo方法 */
  9. export default React.memo(MyComponent, areEqual);

组件自己负责判断自己是否渲染解决了90%无效渲染的问题,这种思路是’自己的命运自己掌握’。
对应的另外一种思路就是,父组件帮你做判断。

  1. // 自组件
  2. function Foo({bar, baz}) {
  3. // 不关心
  4. }
  5. // 父组件
  6. function Blub() {
  7. const bar = React.useCallback(() => {}, [])
  8. const baz = React.useMemo(() => [1, 2, 3], [])
  9. return <Foo bar={bar} baz={baz} />
  10. }

上面的代码中,不关系自组件内部,因为我们的判断逻辑全在父组件里面:给自组件传递的属性bar和baz一个用useCallback、一个用useMemo,当他们的第二个参数表示的依赖数组发生变化后,才会产生新的数组和函数。这样对于自组件来讲,在没有新属性的时候,自然不会渲染。
对比看shouldComponentUpdate,能解决属性的类型是基本类型(直接浅比较),一般对象包括数组(我可以自己实现比较规则)。但是,如果属性是函数,这种’自己的命运自己掌握’的方式就很棘手了。(我怎么比较两个引用地址不一样的函数对象,但是却是相同的逻辑?)
这就是为啥上面文中说“组件自己负责判断自己是否渲染解决了90%无效渲染的问题”。另外的10%正是属性是函数的时候。
的确,儿子解决不了的问题,只有老子亲自来看了。

鲜见的耗时计算

鲜,普通话三声,表示少。就是这种情况不常见。
不过问题只要存在就需要解决。这里召唤的是useMemo。
代码说事:

  1. // 函数组件
  2. function BetterWorld({ people, environment, animal }) {
  3. // 这里在渲染前有一个大量计算过程
  4. // 纯函数
  5. // 依赖两个参数
  6. const plans = calculate_HowToMakeTheWorldBetter(people, environment)
  7. // 渲染计算过程
  8. return <div>
  9. To Make The World Better~ <br />
  10. We can save { animal } and { environment }!<br />
  11. And also,<br />
  12. The Only way to save us!<br /><br />
  13. Plan is Here! <br />
  14. <p>{plans}</p>
  15. </div>
  16. }

BetterWorld组件接受的props有people、environment和animal。但是需要大量计算的函数calculate_HowToMakeTheWorldBetter()却没有依赖所有的props属性。试想下,像这样每次渲染都执行这么有分量的函数,势必浪费了大量的计算资源,因为这个函数是纯函数,参数决定了结果,算来算去结果没变。
这种情况下优化势在必行:

  1. function BetterWorld({ people, environment, animal }) {
  2. // 优化部分
  3. // useMemo暂存了计算值
  4. // 并保证了只有依赖数组有变化才重新计算函数值
  5. React.useMemo(
  6. () => calculate_HowToMakeTheWorldBetter(people, environment),
  7. [people,environment],
  8. );
  9. // 渲染计算过程
  10. return <div>
  11. To Make The World Better~ <br />
  12. We can save { animal } and { environment }!<br />
  13. And also,<br />
  14. The Only way to save us!<br /><br />
  15. Plan is Here! <br />
  16. <p>{plans}</p>
  17. </div>
  18. }

我们的优化部分利用useMemo,最初的耗时计算不可避免,但是结果被暂存了起来,并保证了只有需要重新计算的时候才重新计算(useMemo依赖数组有变化的时候就是需要再计算的时候)。
细品代码,以及代码的展现的深远意境和美好愿景。

问题和总结

注意:CaptureValue特性依旧存在

useEffect、useMemo、useCallback中常见的迷惑点就是被称作capture value的闭包特性。
这种特性表现在,如果传入这些hooks的作为参数的函数中,需要引用外部的有些变量,那么函数内部拿到的值是函数创建那一次渲染的变量值。
比方说下面的例子,在点击自组件“look count value”按钮之前一直点击增加次数,尽管count的值在增加,可创建useCallback只在第一次渲染,这时候的count值是0,所以最终alert显示的值就是0。

  1. const ChildComp = function (props) {
  2. return <button onClick={props.showFunc}>look count value</button>
  3. }
  4. function ParentComp () {
  5. const [ count, setCount ] = useState(0)
  6. const increment = () => setCount(count + 1)
  7. const showFunc = useCallback(()=>{
  8. alert(count);
  9. return count;
  10. }, [])
  11. return (
  12. <div>
  13. <button onClick={increment}>点击次数:{count}</button>
  14. <ChildComp showFunc={showFunc}/>
  15. </div>
  16. );
  17. }

Captrue Value如果想绕过就还是借助useRef。就是把最新的值用ref保存(ref.current),然后需要使用的地方使用这个ref的值。

  1. const refValue = useRef()
  2. refValue.current = count;
  3. alert(refValue.current);

考量优化的代价

优化是有成本的。
useCallback 和 useMemo的显而易见的成本是:对于你的同事来说,你使代码更复杂了。在优化之前的考量是一个综合的过程。如果代码没有特别显著的性能和bug问题,就没必要大刀阔斧的改革。

简单小结

useCallback和useMemo尽管看上去很像。但是解决的问题有相同也各有针对。
他们的共同点主要是,解决在父组件级别通过控制向自组件传递的参数的变化,来决定自组件是否渲染。解决不必要渲染的问题。与shouldComponent和React.memo不同的是,shouldComponent、memo是依靠组件自身的判断来解决不必要渲染。

另外,在解决不必要的渲染问题时,useCallback能处理函数类型的属性。
useMemo可以减少组件内部可能产生的大量计算。