前面两个章节我们已经了解过普通函数组件的更新流程以及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);
}
}
// 正常更新
在更新之前,同时校验props
与ref
对象的变化,只有都满足判断条件才能进入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 hook
的dispatch
修改方法:
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 hook
的dispatch
修改方法在组件首次加载时会被存储到内部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
比较属性值】。
所以传递此类方法,在父组件更新时就会造成子组件的重新渲染。
这时候我们就可以使用useCallback
与React.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>
)
}
注意: useMemo
比useCallback
功能更加强大,即使不配合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
方法基本无法满足函数组件更新优化的需求,一般都需要搭配useRef
,useCallback
,useMemo
等性能优化的hook
一起使用,才能满足要求。
2,因为受到hook
闭包问题,缓存占用内存问题以及自定义比较函数耗时问题等的影响,即使使用了这些性能优化方法,在部分情况下也难以得到想要的结果。所以函数组件性能优化只适合发现问题后再进行针对性的处理,保证比优化之前的效果要好即可,而不适宜进行滥用。
结束语
以上就是react函数组件性能优化的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!