缓存钩子:useMemo和useCallback和memo

79 阅读7分钟

1.memo - 组件缓存

1.1 问题

函数组件的更新的条件:state/ props/ context上下文/ 父组件更新子组件无脑更新(不论props或者state)

上面几个更新条件中,前三个还算合理,但是第四个-无脑更新就有点浪费性能了,而比较合理的父子组件更新方式是:父组件给子组件的props更新,子组件再更新才行。而memo的诞生就是为了实现这个功能的-- 即当父组件有props交互时,让子组件根据props更新触发渲染,而不是无脑渲染。

1.2 语法

使用memo-API包装函数组件即可

import React, { memo } from "react"; 

const ChildComponent = memo(function ChildComponent({ text }) { 
  console.log("ChildComponent rendered"); 
  return <div>{text}</div>; }
); 

function ParentComponent({ showChild }) { 
  return ( 
    <div> 
      {showChild && <ChildComponent text="Hello, world!" />} 
      <button onClick={() => setShowChild(!showChild)}>Toggle child</button> 
    </div> 
  ); 
}

在这个例子中,我们创建了一个名为 ChildComponent 的组件,并使用 memo 高阶组件对其进行了优化。我们还创建了一个名为 ParentComponent 的组件,它可以切换 ChildComponent 的显示。当 ParentComponent 重新渲染时,ChildComponent 的属性没有发生变化,因此它不会重新渲染。

存在问题

虽然使用memo包裹的组件可以实现只有父组件的props变化之后,子组件才更新渲染,但是因为react是浅比较,不保证props中的简单类型和复杂类型存储地址变化,因此出现了useMemo和useCallback。

1.3 对比pureComponent

pureComponent是class类组件对重新渲染的.在类组件中通过pureComponent继承来实现组件的避免重复渲染。

2.useMemo - 数据缓存

2.1 解决问题:同computed,避免重复计算

我们都知道react的渲染特性相当于每一次渲染都是一次快照,会生成新的state状态,然后根据这个新的state状态构建dom,交给GUI渲染线程进行style树构建、合成树、分成、绘制、合成绘制。那么就存在以下问题:

import React, { useState } from "react";

function App() {
  const [likeCount, setLikeCount] = useState(2)

  // 桃子的单价和价格
  const [peach, setPeach] = useState({
    num: 10,
    unitPrice: 5
  });

  // 香蕉的单价和价格
  const [banana, setBanana] = useState({
    num: 10,
    unitPrice: 10
  });

  // 2. 每次渲染时候立即计算:计算桃子总价
  let price = 0
  function count() {
    console.log("价格重新计算了--");
    price =  (peach.num * peach.unitPrice)
  }
  count() // 每次渲染时候立即计算:计算桃子总价
  
  // 点赞个数+1
  const addLike = () => {
    setLikeCount(likeCount + 1)
  }


  return (
    <div>
      <h3>店铺点赞数:{likeCount}</h3>
      <button onClick={addLike}>点赞</button>
      <h4> 购买的水果清单: </h4>
      <h4>  桃子数量: {peach.num}  单价:{peach.unitPrice}元 </h4>
           桃子总价:{price}元
           
      <h4>  香蕉数量: {banana.num} 单价:{banana.unitPrice}元 </h4>

    </div>
  );
}

但是,如果我们只点击点赞事件,那么就会触发执行:setLikeCount(likeCount + 1),会走一次重新render。那么函数组件在每次重新渲染的时候就会再一次执行桃子总价js计算。显然,如果这部分计算非常耗时的话,那么每次点赞都会影响性能。显然,从也上看,只有桃子相关价格变化后我才需要执行这部分计算。 如下所示

  let price = 0
  function count() {
    console.log("价格重新计算了--");
    price =  (peach.num * peach.unitPrice)
  }
  const countNum = useMemo(() => {
    const result = count()
    return result
  }, [peach.num, peach.unitPrice])  // 每次渲染时候立即计算:计算桃子总价
  
  return (
      <div>
        {{ countNum }} // 在useMemo中定义执行函数,该执行函数的返回值(有返回值)就是对应到视图的变量
      </div>
  )

因此,使用useMemo就登场了,和vue的computed类似,都是为了保证「部分js代码」不必在每次函数render时候都执行(进而优化组件更新时js的执行时间),而仅在依赖数据变化的时候才会执行。避免了在每个渲染阶段都执行高成本的计算。,重新渲染时候大量不必要且结果一致的重复js计算。

总结:
useMemo的优化目的:就是我们每次使用setState之后,一定会引起函数组件的重新渲染(异步),那么就会导致函数组件的重新执行函数体,那么有些函数的计算和本次更新的state无关,也被重复执行了。为了避免每次state更新后的组件渲染,导致函数不必要重复执行。因此将这部分(和state更新无关)函数执行返回值缓存,来缩短时间。
useMemo语法上:需要在第二个参数中注明依赖项,这样才能有效的监听到数据的变化,及时重新计算。如果监听数据没有变化,那么在组件重新渲染的时候,useMemo钩子对应的第一个函数参数不会再次执行,而是直接拿缓存数据。

2.2 useMemo和computed的区别

共同点:都是为了减少不必要的计算,优化组件的性能。
不同点

区别computeduseMemo
1.依赖项的收集监听自动收集依赖项手动添加依赖
2.执行时机依赖变化时组件重新渲染时执行
3.使用范围vue组件函数式组件

1.依赖项的监听上:
computed是自动收集依赖项,因为在computed原理中,通过执行computed函数然后会触发响应式依赖数据的获取,这个时候会将computed函数放在依赖的订阅的队列中,这样当数据变化之后,遍历执行订阅队列,就可以自动执行computed函数。
useMemo是手动添加依赖,因为react本身没有双向绑定,因此视图的更新都是通过特定的dispatch函数执行的,因此就没有像vue那样的依赖收集阶段,故需要指定对应的依赖数据。

2.执行时机上 因为vue是在数据变化的时候,触发订阅队列,执行对应的update函数。但是react是在函数式组件重新渲染的时候,会生成新的FiberNode,然后对应的hooks链路会重新执行每一个hook,获取最新的数据,在这个时候useMemo才会去比较依赖数据的变化。

3.useCallback

3.1 使用场景

现状1:我们知道当我们使用setState去更新数据的时候,一定会引起函数式组件重新执行函数体,那么这个时候在函数组件内部中定义的函数就会重新创建,就会导致新创建的函数引用值和之前的引用值不一样。
现状2:我们知道当函数组件的重新渲染的条件是:state的变化 && props的变化 && 组件依赖的上下文变化 && 父组件的重新渲染(不论子组件的props和state是否变化)

那么我们通常基于现状2会有个避免子组件的重新渲染措施:

问题:父组件定义的函数作为参数传给子组件,然后父组件更新。但是一旦props发生变化,子组件还是会重新渲染。在此前提下,就会存在一个问题,因为react是“浅比较”,也就是复杂数据类型的时候,比较的是引用值。因此,如果父组件更新的话,那么重新执行函数体就会导致传给子组件的函数引用值变化,那么从表面上函数体不变,但是不函数的引用值变了,依然导致了子组件的重复渲染了。

目的:避免子组件在接受父组件函数体的时候,跟着父组件重新渲染 如下所示:

import React, { useCallback } from "react"; 

function ButtonComponent({ onClick, children }) { 
  return <button onClick={onClick}>{children}</button>;
} 

function ParentComponent() { 
  const handleClick = useCallback(() => { 
    console.log("Button clicked"); }, []
  ); 
  return ( 
     <div> <ButtonComponent onClick={handleClick}>Click me</ButtonComponent> </div> ); 
}

析:在这个例子中,我们创建了一个名为 ButtonComponent 的组件,它接受一个 onClick 函数属性。我们还创建了一个名为 ParentComponent 的组件,它使用 useCallback 钩子来创建一个 handleClick 函数。当 ParentComponent 重新渲染时,useCallback 会返回上一次创建的 handleClick 函数实例,避免了不必要的函数创建。

3.2 使用语法

接受两个参数:一个函数和一个依赖数组。
当依赖数组中的值发生变化时,useCallback 会返回一个新的函数实例。否则,它将返回上一次创建的函数实例。

const handleClick = useCallback(() => {     
   console.log("Button clicked"); }, []
);
然后useCallback返回的函数传递给子组件,作为props。

3.3 引发的闭包问题

juejin.cn/post/722957…