React函数组件性能优化三部曲(三)

前面两个章节我们已经了解过普通函数组件的更新流程以及React.memo方法优化函数组件的原理。

本节就从实践方面来讲解React.memo方法与性能优化hook的搭配使用。

1,原理回顾

首先回顾一下React.memo方法创建的memo组件更新优化的原理:

js 复制代码
# 优化原理
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
if (!hasScheduledUpdateOrContext) {
  compare = compare !== null ? compare : shallowEqual;
  if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
    // 校验通过,更新优化
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
}
// 正常更新

在更新之前,同时校验propsref对象的变化,只有都满足判断条件才能进入Bailout策略,进行更新优化。

js 复制代码
const newCom = React.memo(Com,  compare);
  • 如果传递了自定义compare比较函数,则调用此方法进行props变化的比较。
  • 如果没有传递这个参数,则会使用react内部默认的shallowEqual方法进行props的浅比较。

shallowEqual方法源码:

js 复制代码
function shallowEqual(objA, objB) {
  if (Object.is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' || objA === null ||
    typeof objB !== 'object' || objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  for (let i = 0; i < keysA.length; i++) {
    const currentKey = keysA[i];
    if ( !objB.hasOwnProperty(currentKey) || !Object.is(objA[currentKey], objB[currentKey]) ) {
      return false;
    }
  }
  return true;
}

2,最佳实践

首先我们来看一个最简单的memo组件:

js 复制代码
// MyFun.js
import { useState } from 'react'
import NewChild from './NewChild'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  function handleClick() {
    setCount(count + 1)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <NewChild name='NewChild'></NewChild>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}
js 复制代码
// Child.js
import React from 'react'

function Child(props) {
  console.log('Child组件运行了')
  return (
    <div className='Child'>
      <div>Child子组件</div>
      <div>name: {props.name}</div>
    </div>
  )
}

const NewChild = React.memo(Child)
export default NewChild

这里我们用React.memo方法来优化Child子组件,此时MyFun重新渲染,子组件在进行优化策略校验时,判断props无实际变化,满足优化条件即可跳过整个组件的更新。

但是在实际项目中,我们的组件肯定不会这么简单,一般父组件都会给子组件传递很多的动态内容。

下面我们就以一些常见的案例来进行优化解析。

注意: 下面案例的Child子组件默认使用了React.memo方法进行更新优化。

js 复制代码
export default React.memo(Child);
(一)方法的传递
  • 传递useState hookdispatch修改方法:
js 复制代码
// MyFun.js
import { useState } from 'react'
import NewChild from './NewChild'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  function handleClick() {
    setCount(count + 1)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <NewChild name='NewChild' setCount={setCount}></NewChild>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

MyFun组件更新时,会调用一次MyFun函数,在遇到useState hook时会对此hook进行更新处理,最终返回的是最新的state和原来的修改方法。所以传递此类方法,在父组件更新时不会造成子组件的重新渲染。

useState hookdispatch修改方法在组件首次加载时会被存储到内部hook对象之上,组件更新时直接返回原方法。

  • 传递父组件内定义的普通方法。
js 复制代码
// MyFun.js
import { useState } from 'react'
import NewChild from './NewChild'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  function handleClick() {
    setCount(count + 1)
  }
  function handleEdit() {
    console.log(count)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <NewChild name='NewChild' onEdit={handleEdit} ></NewChild>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

MyFun组件更新时,会调用一次MyFun函数,所以会创建一个新的handleEdit函数,此时传递给子组件的方法已经变成了一个新的函数,即子组件的props发生了真实的变化【Object.is比较属性值】。

所以传递此类方法,在父组件更新时就会造成子组件的重新渲染。

这时候我们就可以使用useCallbackReact.memo方法搭配使用。

js 复制代码
// MyFun.js
import { useState, useCallback } from 'react'
import NewChild from './NewChild'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  function handleClick() {
    setCount(count + 1)
  }
  // 缓存方法
  const handleEdit = useCallback(() => {
      console.log(count)
  }, [])
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <NewChild name='NewChild' onEdit={handleEdit} ></NewChild>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

使用useCallback来缓存要传递的方法,父组件在首次加载时就会将此方法缓存到内部的hook对象之上【不理解内部hook的可以直接理解为存储到父组件Fiber节点对象上】,所以父组件更新时就会直接返回原来的函数地址了,这样就不会造成子组件的重新渲染了。

同时在这里我们也可以发现useCallback的特点:它是一个非常纯粹的性能优化hook,如果单独使用没有任何意义,它只能与React.memo方法进行搭配使用。

注意: 在给子组件传递方法时,最好不要直接传递箭头函数,否则使用了React.memo包裹的子组件将无法进行性能优化,即子组件总是会被动的跟着父组件一起重新渲染【当然,前提是你正在使用性能优化,否则任意】。

不过要注意的是react hook基本都存在闭包问题 ,比如当前使用的useCallback,一般情况下我们都希望此方法只需要在组件加载后固定引用即可,所以我们会传递一个空数组。不过这样就会出现一个闭包问题,当组件更新之后,触发此方法打印的结果还是之前的状态,因为回调函数被缓存后,它的作用域是组件加载时形成的,此作用域内的变量为之前的状态。

js 复制代码
const handleEdit = useCallback(() => {
  console.log(count)  // 1
}, [])

此时打印结果还是之前的状态,就不符合我们的期望,就需要给useCallback加上对应的依赖,让它在依赖变化时重新生成一个回调函数,此时才满足功能的需求。

js 复制代码
const handleEdit = useCallback(() => {
  console.log(count)  // 2
}, [count])

不过这样的问题就是,子组件没有使用到此数据,但是传递此方法发生了变化【是一个新的函数】,所以子组件又得被动重新渲染了。

这就是react函数组件性能优化的一些问题所在,它的要求是比较苛刻的,所以react函数组件性能优化只能是遇到问题再解决问题,一般的情况下是不推荐都给加上的。

(二)普通数据的传递
js 复制代码
// MyFun.js
import { useState } from 'react'
import NewChild from './NewChild'

export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  const [info, setInfo] = useState({
    name: 'tom',
    age: 18
  })
  function handleClick() {
    setInfo({...info, age: 20})
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <NewChild name='NewChild' info={info} ></NewChild>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

在正常情况下,修改普通数据时,我们总是希望得到正确的更新。即传递的这些数据确实发生了变化,memo组件在校验时能够发现props的变化,然后正常的更新子组件。

不过有时候,我们可能会在组件中定义一些常量数据,或者组件首次加载计算的一些复杂的数据,此类数据传递给子组件时,为了不让父组件更新时重新生成一次,我们就可以使用useMemo将数据缓存起来,以此配合子组件的性能优化。

js 复制代码
export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  // 缓存一些常量数据 或者计算复杂更新较少的数据
  const list = useMemo(() => ... );
  function handleClick() {
    setCount(count + 1)
  }
  return (
    <div className='MyFun'>
      <div>state: {count}</div>
      <NewChild name='NewChild' list={list}></NewChild>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

注意: useMemouseCallback功能更加强大,即使不配合React.memo方法,它自身也有独立的使用场景。

比如常用的就是来缓存计算复杂且更新较少的数据,不过还是要注意的时,useMemo缓存的数据也是存储在组件的FIber节点之上,缓存的数据过多会增加内存的占用,一直到组件的卸载。

最后还有一些情况下:我们可能想对某个特定的属性进行校验来决定更新,这时候我们就可以使用自定义的compare比较函数。

js 复制代码
// Child.js
import React from 'react'

function Child(props) {
  console.log('Child组件运行了')
  return (
    <div className='Child'>
      <div>Child子组件</div>
      <div>name: {props.name}</div>
    </div>
  )
}
// 使用自定义比较函数
const NewChild = React.memo(Child, (oldProps, newProps) => {
    // 自定义逻辑
    ...
    if () {
        return true; // 表示无变化,不需要更新
    }
    return false; // 表示变化,需要更新
})
export default NewChild

不过要注意的是:react内部默认是使用的shallowEqual方法对props进行浅比较,如果我们在自定义函数中使用了深比较,或者执行了比较耗时的compare,导致我们执行差异比较的耗时已经超过了组件重新渲染的耗时,则我们的性能优化就变得完全不必要了。所以我们在使用自定义比较函数时应该对数据的结构变化有足够明确的认知。

(三)ref的传递

传递ref对象对函数组件性能优化的影响就比较有限了。

  • 如果没有使用ref:新旧Fiber.ref都是null,所以没有影响。
js 复制代码
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) 
  • 如果使用了ref:只需要保证父组件使用useRef创建的ref对象,以及使用React.memo方法正确的包裹forwardRef组件即可。
js 复制代码
function MyFun() {
	const ref = useRef();
	return <NewChild name='NewChild' ref={ref}></NewChild>
}
js 复制代码
// child.js
export default  React.memo(React.forwardRef(Child))

3,总结

1,在实际项目中,只使用React.memo方法基本无法满足函数组件更新优化的需求,一般都需要搭配useRefuseCallbackuseMemo等性能优化的hook一起使用,才能满足要求。

2,因为受到hook闭包问题,缓存占用内存问题以及自定义比较函数耗时问题等的影响,即使使用了这些性能优化方法,在部分情况下也难以得到想要的结果。所以函数组件性能优化只适合发现问题后再进行针对性的处理,保证比优化之前的效果要好即可,而不适宜进行滥用。

结束语

以上就是react函数组件性能优化的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!

相关推荐
WeiShuai10 分钟前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
forwardMyLife15 分钟前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
ice___Cpu16 分钟前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill18 分钟前
nestjs使用ESM模块化
前端
加油吧x青年37 分钟前
Web端开启直播技术方案分享
前端·webrtc·直播
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白1 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧2 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
mez_Blog2 小时前
个人小结(2.0)
前端·javascript·vue.js·学习·typescript
珊珊而川2 小时前
【浏览器面试真题】sessionStorage和localStorage
前端·javascript·面试