React性能优化之useCallback与useMemo

React性能优化之useCallback与useMemo

useMemo

当我们在写一个函数式组件时,经常会遇到非依赖的变量改变导致某些方法重新执行,造成性能浪费,这个时候,我们可以考虑使用useMemo对我们的值进行缓存,只有当这个值的依赖项改变时,我们采取重新执行方法获取新值,否则直接从缓存中获取上一次的值直接返回。

场景与示例

我们来看下面这段伪代码:

function Demo(){
    const [count, setCount] = useState<number>(0);
    const [name, setName] = useState<string>("");
    
    const showCount = () => {
        console.log("执行了showCount");
        let sum = 0;
        for(let i=0;i<count;i++){
            sum+=i;
        }
        return sum;
    };
    
    return (
    	<div>
        	<h2>这是useMemo测试实例(未优化)</h2>
            <p>累加结果: {showCount()}</p>
            <p>计数器: {count}</p>
            <button onClick={()=>setCount(count + 1)}>增加</button>
            <input value={name} onChange={e => setName(e.target.value)} />
        </div>
    );
}

上面的伪代码是没有使用useMemo优化过的代码,当我们点击增加按钮时,确实能够达到我们预期的效果,无论是累加结果还是计数器都改变了。但当我们设置文本框的值得时候,此时只是改变了name,并没有改变count,我们的预期是不会重新出发showCount方法执行重新计算的,但是上述代码依然会反复的触发。我们可以想象一下,如果在showCount方法中执行的是一个极其复杂且耗费时间和性能的计算,那么这段看起来没几行的代码就有可能导致整个网站的卡顿甚至崩溃。

那么,我们来使用useMemo改造一下上述代码:

function Demo(){
    const [count, setCount] = useState<number>(0);
    const [name, setName] = useState<string>("");
    
    // 使用useMemo处理计算方法,只有当依赖变量count改变时才会触发重新计算获得新的结果,否则将会直接获取上一次计算的结果的缓存直接返回,避免了无异议的重复计算
    const showCount = useMemo(() => {
        console.log("执行了showCount");
        let sum = 0;
        for(let i=0;i<count;i++){
            sum+=i;
        }
        return sum;
    }, [count]);
    
    return (
    	<div>
        	<h2>这是useMemo测试实例(未优化)</h2>
            <p>累加结果: {showCount}</p>
            <p>计数器: {count}</p>
            <button onClick={()=>setCount(count + 1)}>增加</button>
            <input value={name} onChange={e => setName(e.target.value)} />
        </div>
    );
}

使用详解

useMemo(nextCreateFn, deps)

官方解释:

返回一个 memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

**你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。**将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。

其中nextCreateFn需要我们传入一个函数,用于计算目标结果的函数,这个函数需要一个返回值,函数的返回值就是我们最终的计算结果

deps是一个依赖数组,我们需要将在函数中所使用的外部状态,也就是依赖变量添加进去,这样一来,当依赖没有改变时,我们就可以直接获取上一次缓存的结果直接返回,无需重复执行nextCreateFn计算结果。当deps为null是,将每次渲染都会重新计算,这样其实就失去了这个hooks的意义。因此,及时不传这个依赖参数程序也不会报错,我们在开发时也需要明确函数依赖项并传入依赖数组,否则就无须使用此hooks。

源码分析

React源码中useMemo的实现

// 这个函数用于对比两个依赖数组的依赖是否相同
function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
) {
  // 此处删除一些与逻辑无关的开发环境调试代码

  // 上一次的依赖为null,当前依赖不为null的话,那肯定不相同,依赖发生改变,因此返回false
  if (prevDeps === null) {
   // 此处删除一些与逻辑无关的开发环境调试代码
    return false;
  }

  // 此处删除一些与逻辑无关的开发环境调试代码
  // 循环遍历每一个依赖项,并对比上一次的依赖于当前依赖是否相同,只要有一个不相同,则直接返回false
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  // 循环对比结束后,则说明所有的依赖项都没有发生改变,都是相同的,返回true
  return true;
}
// 以下为React中实现useMemo逻辑的主要代码
// 从下面代码我们可以看出React在执行useMemo的时候其实是分为两个阶段的,一个是挂载,一个是更新
// 挂载时会先执行一次我们传入的计算方法,即: nextCreate,得到首次计算结果nextValue,然后将计算结果和依赖数组都保存在memoizedState当中缓存起来,方便更新时用于对比与获取缓存结果。最后返回首次计算结果作为初次渲染的结果
function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}
// 当组件因为某些操作触发重新渲染时,会将上一次的依赖数组拿出来,与当前的依赖数组对比,如果发现依赖的状态并没有发生改变,则直接从memoizedState中缓存的上一次的计算结果返回,无须重新执行nextCreate进行重新计算。否则进行重新计算,并将最新的计算结果和新的依赖数组缓存,并将新的计算结果返回作为本次渲染的结果
function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    // Assume these are defined. If they're not, areHookInputsEqual will warn.
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

useCallback

场景与示例

我们经常会给一些元素绑定事件,或者是将一些函数通过属性的形式传递给子组件,如果在函数式组件中,如果不经任何处理的函数通过属性的形式传递给子组件,那么,一旦父组件的任意状态发生变化进行重新渲染时,也会因为每次的函数都是一个新的引用而导致子组件因为属性的改变而重新渲染。我们来看一下下面的例子就比较清晰了:

function Demo(){
    const [count, setCount] = useState<number>(0);
    const [name, setName] = useState<string>("");
    
    const showCount = () => {
        console.log("执行了showCount");
        let sum = 0;
        for(let i=0;i<count;i++){
            sum+=i;
        }
        return sum;
    };
    
    return (
    	<div>
        	<h2>这是useMemo测试实例(未优化)</h2>
            <p>计数器: {count}</p>
            <button onClick={()=>setCount(count + 1)}>增加</button>
            <input value={name} onChange={e => setName(e.target.value)} />
            <Child onClick={showCount} />
        </div>
    );
}
function Child(props) {
    console.log("child rerender!!");
    return <div onClick={props.onClick}>这是子节点</div>
}

上面的实例代码中,我们将showCount函数作为属性传递给子组件Child,当我们父组件Demo的任意状态如:countname发生改变时,都会重新创建showCount函数,导致函数引用不一致而触发Child组件的重新渲染。但是,我们的showCount方法很明显是跟我们的name这个状态没有关系的,因此,我们希望只有当count状态改变时才触发Child组件的重新渲染。那么,这个时候我们就可以用到useCallback了。

function Demo(){
    const [count, setCount] = useState<number>(0);
    const [name, setName] = useState<string>("");
    
    // 使用useCallback优化函数,当且仅当count改变时,我们才改变我们的回调,否则直接获取缓存的函数,保持引用一致
    const showCount = useCallback(() => {
        console.log("执行了showCount");
        let sum = 0;
        for(let i=0;i<count;i++){
            sum+=i;
        }
        return sum;
    }, [count]);
    
    return (
    	<div>
        	<h2>这是useMemo测试实例(未优化)</h2>
            <p>计数器: {count}</p>
            <button onClick={()=>setCount(count + 1)}>增加</button>
            <input value={name} onChange={e => setName(e.target.value)} />
            <Child onClick={showCount} />
        </div>
    );
}
function Child(props) {
    console.log("child rerender!!");
    return <div onClick={props.onClick}>这是子节点</div>
}

使用详解

useCallback(callback, deps)

官方解释:

返回一个 memoized 回调函数。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useCallbackuseMemo的用户基本相同,都是传入两个参数,第一个是函数,第二个是依赖数组,不同的是,useMemo会去执行我们传递过去的函数用来计算目标结果,而useCallback则仅仅只是将我们传入的函数缓存并返回,不会去执行它。

源码分析

React源码中useCallback的实现

// 与useMemo不同,useCallback不会去执行callback获得结果,而是直接缓存并返回callback
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
// 更新也是一样,除了不执行callback获取结果之外,其他的都跟useMemo一样的
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

结语

这次我们讨论的东西其实很简单,估计大家不用几分钟就能够融会贯通了,之所以把这个点单独拎出来作为一个话题的讨论,是因为随着我们的项目中对于函数式组件和hooks的使用越来越频繁,我们的业务功能也拆分的越来越细,组件越来越小,我们需要从每一个小点做好性能的优化处理,不然在页面上大量引入某些未经优化的组件时,可能就会把一些很小的性能问题无限放大,最终导致页面的卡顿甚至崩溃,在项目中善用useMemouseCallback,可以让我们尽可能得避免这些情况的发生,让项目运行得更加平稳高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星河阅卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值