React Hooks之使用useCallback和useMemo进行性能优化方式

 更新时间:2023年06月06日 16:53:54   作者:学全栈的灌汤包  
这篇文章主要介绍了React Hooks之使用useCallback和useMemo进行性能优化方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

useCallback和useMemo性能优化

useCallback的解析

useCallback的使用

useCallback实际的目的是为了进行性能的优化。

useCallback进行什么样的优化呢?

例如下面这个计数器的案例, 我们点击按钮时, counter数据会发生变化, App函数组件就会重新渲染, 意味着increment函数就会被重新定义一次, 每点击一次按钮, increment函数就会重新被定义;

虽然每次定义increment函数, 垃圾回收机制会将上一次定义的increment函数回收, 但是这种不必要的重复定义是会影响性能的

import React, { memo, useState } from 'react'
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  function increment() {
    setCounter(counter + 1)
  }
  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={() => increment()}>+1</button>
    </div>
  )
})
export default App

如何进行性能的优化呢?

调用useCallback会返回一个 memoized(有记忆的) 的回调函数;

在依赖不变的情况下,多次定义的时候,返回的回调函数是相同的;

  • 参数一: 传入一个回调函数, 如果依赖发生改变会定义一个新的该回调函数使用, 如果依赖没有发生改变, 依然使用原来的回调函数
  • 参数二: 用于控制依赖的, 第二个参数要求传入一个数组, 数组中可以传入依赖, 传空数组表示没有依赖
const memoizedCallback = useCallback(
  () => {
   doSomething(a, b)
  },
  [a, b]
)

useCallback拿到的结果是函数

useCallback的作用

通常使用useCallback的目的是在向子组件传递函数时, 将要传递的函数进行优化在传递给子组件, 避免子组件进行多次渲染;

并不是为了函数不再重新定义, 也不是对函数定义做优化

我们来看下面这样一个案例:

定义一个子组件Test, 并将increment函数传递到子组件中, 我们在子组件中可以拿到increment方法修改App组件中的counter;

由于counter发生改变, 就会重新定义一个新的increment函数, 因此我们只要修改了counter, 就会传递一个新的increment函数到Test组件中; Test组件中的props就会发生变化, Test组件会被重新渲染

import React, { memo, useState, useCallback } from 'react'
const Test = memo((props) => {
  console.log("Test组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Test+1</button>
    </div>
  )
})
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  function increment() {
    setCounter(counter + 1)
  }
  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>+1</button>
      <Test increment={increment}/>
    </div>
  )
})
export default App

如果此时App组件中再定义一个方法changeMessage用来修改message;

我们会发现当message发生改变时, 子组件Test也会被重新渲染; 这是因为message发生改变, App组件会重新渲染, 那么就会重新定义一个新的increment函数, 将新的increment函数传递到Test组件, Test组件的props发生改变就会重新渲染

import React, { memo, useState, useCallback } from 'react'
const Test = memo((props) => {
  console.log("Test组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Test+1</button>
    </div>
  )
})
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  const [message, setMessage] = useState("哈哈哈哈")
  function increment() {
    setCounter(counter + 1)
  }
  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>+1</button>
      <h2>{message}</h2>
      <button onClick={() => setMessage("呵呵呵呵")}>修改message</button>
      <Test increment={increment}/>
    </div>
  )
})
export default App

但是如果我们使用useCallback, 就可以避免App组件中message发生改变时, Test组件重新渲染

因为message组件发生改变, 但是我们下面的useCallback函数是依赖counter的, 在依赖没有发生改变时, 多次定义返回的值是相同的(也就是修改message重新渲染App组件时, increment并没有重新定义, 依然是之前的); 就意味着Test组件中的props没有改变, 因此Test组件不会被重新渲染

如果是counter值发生改变, 因为useCallback函数是依赖counter的, 所以会定义一个新的函数给increment; 当向Test组件传递新的increment时, Test组件的props就会改变, Test依然会重新渲染, 这也是我们想要实现的效果

import React, { memo, useState, useCallback } from 'react'
const Test = memo((props) => {
  console.log("Test组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Test+1</button>
    </div>
  )
})
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  const [message, setMessage] = useState("哈哈哈哈")
  // 使用useCallback依赖于counter
  const increment = useCallback(() => {
    setCounter(counter + 1)
  }, [counter])
  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>+1</button>
      <h2>{message}</h2>
      <button onClick={() => setMessage("呵呵呵呵")}>修改message</button>
      <Test increment={increment}/>
    </div>
  )
})
export default App

还可以再进一步的进行优化:

现在我们的代码是counter发生变化时, useCallback会重新定义一个新的函数返回给increment; 但是我们想做到, counter发生变化, 依然使用原来的函数, 不需要重新定义一个新的函数;

可能会有小伙伴想, 直接将依赖改为一个空数组, 但是如果是这样的话就会产生闭包陷阱;

我们修改counter时确实不会重新生成一个新的函数, 但是原来的函数中使用的counter永远是之前的值, 也就是0;

这是因为我们旧的函数在定义的那一刻, counter的值是0;

由于修改counter依然使用旧的函数, 这样无论我们修改多少次counter, 页面展示的数据永远是 0 + 1 的结果

const increment = useCallback(() => {
  setCounter(counter + 1)
}, [])

这个时候我们就需要结合使用另一个hook: useRef

useRef函数在组件多次进行渲染时, 返回的是同一个值;

我们就可以将最新的counter储存到useRef返回的对象的current属性中;

这样做的好处就是, counter发生改变时, 也不会重新定义一个函数, 意味着修改counter也不会导致Test组件重新渲染

import React, { memo, useState, useCallback, useRef } from 'react'
const Test = memo((props) => {
  console.log("Test组件被重新渲染")
  return (
    <div>
      <button onClick={props.increment}>Test+1</button>
    </div>
  )
})
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  const [message, setMessage] = useState("哈哈哈哈")
  // 组件进行多次渲染, 返回的是同一个ref对象
  const counterRef = useRef()
  // 将最新的counter保存到ref对象current属性中
  counterRef.current = counter
  const increment = useCallback(() => {
    // 在修改数据时, 引用保存到ref对象current属性的最新的值
    setCounter(counterRef.current + 1)
  }, [])
  return (
    <div>
      <h2>{counter}</h2>
      <button onClick={increment}>+1</button>
      <Test increment={increment}/>
      <h2>{message}</h2>
      <button onClick={() => setMessage("呵呵呵呵")}>修改message</button>
    </div>
  )
})
export default App

useMemo的解析

useMemo实际的目的也是为了进行性能的优化, 例如下面这个例子

我们定义一个计算累加的函数calcNumTotal, 在App组件中调用这个函数计算结果

但是counter改变时, App组件就会重新渲染, 那么calcNumTotal函数又会重新计算; 但是counter的改变和calcNumTotal函数并没有关系, 却要重新渲染; 这种类似的场景我们就可以使用useMemo进行性能优化

import React, { memo } from 'react'
import { useState } from 'react'
// 定义一个函数求和
function calcNumTotal(num) {
  let total = 0
  for (let i = 1; i <= num; i++) {
    total += i
  }
  return total
}
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  return (
    <div>
      {/* couter改变, 组件重新渲染, 意味着calcNumTotal函数也会重新执行, 重新计算结果 */}
      <h2>计算结果: {calcNumTotal(100)}</h2>
      <h2>当前计数: {counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+1</button>
    </div>
  )
})
export default App

如何使用 useMemo进行性能的优化呢?

useMemo返回的也是一个 memoized(有记忆的) 值; 在依赖不变的情况下,多次定义的时候,返回的值是相同的;

  • 参数一: 传入一个回调函数
  • 参数二: 传入一个数组, 表示依赖, 什么都不依赖传入空数组; 如果不传则该函数什么都不会做, 无意义
const memoizedValue = useMemo(
  () => {
    computeExpensiveValue(a, b)
  }, 
  [a, b]
)

这样我们就可以对上面的代码进行优化了, 实现counter发生变化, 而calcNumTotal函数不需要重新计算结果

import React, { memo, useMemo, useState } from 'react'
// 定义一个函数求和
function calcNumTotal(num) {
  console.log("calcNumTotal函数被调用")
  let total = 0
  for (let i = 1; i <= num; i++) {
    total += i
  }
  return total
}
const App = memo(() => {
  const [counter, setCounter] = useState(10)
  let result = useMemo(() => {
    return calcNumTotal(50)
  }, [])
  return (
    <div>
      {/* couter改变, 组件重新渲染, 意味着calcNumTotal函数也会重新执行, 重新计算结果 */}
      <h2>计算结果: {result}</h2>
      <h2>当前计数: {counter}</h2>
      <button onClick={() => setCounter(counter + 1)}>+1</button>
    </div>
  )
})
export default App

useMemo与useCallback的区别:

useMemo拿到的传入回调函数的返回值, useCallback拿到的传入的回调函数本身;

简单来说useMemo是对函数的返回值做优化, useCallback是对函数做优化;

useCallback(fn, [])和uesMemo(() => fn, [])表达的是同一个意思

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 如何使用Next.js + Prisma + MySQL开发全栈项目

    如何使用Next.js + Prisma + MySQL开发全栈项目

    在Next.js项目的任何地方都可以使用Prisma来访问数据库,Prisma提供了丰富的查询方法,满足您在项目中的各种需求,这篇文章主要介绍了如何使用Next.js+Prisma+MySQL开发全栈项目的相关资料,需要的朋友可以参考下
    2026-03-03
  • react源码中的生命周期和事件系统实例解析

    react源码中的生命周期和事件系统实例解析

    这篇文章主要为大家介绍了react源码中的生命周期和事件系统实例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • DOM 注入实践之如何在 React 中优雅地扩展第三方组件

    DOM 注入实践之如何在 React 中优雅地扩展第三方组件

    本文将介绍一种更优雅的解决方案——DOM 注入 + React Portal,通过抽象的示例展示这种技术方案的应用场景和实现方法,感兴趣的朋友跟随小编一起看看吧
    2026-03-03
  • react开发中如何使用require.ensure加载es6风格的组件

    react开发中如何使用require.ensure加载es6风格的组件

    本篇文章主要介绍了react开发中如何使用require.ensure加载es6风格的组件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-05-05
  • React创建虚拟DOM的两种方式小结

    React创建虚拟DOM的两种方式小结

    本文主要介绍了两种创建React虚拟DOM的方式,包括JS方式和jsx方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-01-01
  • ReactNative列表ListView的用法

    ReactNative列表ListView的用法

    本篇文章主要介绍了ReactNative列表ListView的用法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • 使用React MUI库实现用户列表分页功能

    使用React MUI库实现用户列表分页功能

    MUI是一款基于React的UI组件库,可以方便地构建美观的用户界面,使用MUI的DataTable组件和分页器组件可以轻松实现用户列表分页功能,这篇文章使用MUI库实现了用户列表分页功能,感兴趣的同学可以参考下文
    2023-05-05
  • React Hook 监听localStorage更新问题

    React Hook 监听localStorage更新问题

    这篇文章主要介绍了React Hook 监听localStorage更新问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • React中的路由嵌套和手动实现路由跳转的方式详解

    React中的路由嵌套和手动实现路由跳转的方式详解

    这篇文章主要介绍了React中的路由嵌套和手动实现路由跳转的方式,手动路由的跳转,主要是通过Link或者NavLink进行跳转的,实际上我们也可以通JavaScript代码进行跳转,需要的朋友可以参考下
    2022-11-11
  • 聊聊ant design charts 获取后端接口数据展示问题

    聊聊ant design charts 获取后端接口数据展示问题

    今天在做项目的时候遇到几个让我很头疼的问题,一个是通过后端接口成功访问并又返回数据,但拿不到数据值。其二是直接修改state中的data,console中数组发生变化但任然数据未显示,这篇文章主要介绍了ant design charts 获取后端接口数据展示,需要的朋友可以参考下
    2022-05-05

最新评论