深入理解useMemo与useCallBack的区别
前言:在阅读本文前需了解基本数据类型和引用数据类型的区别。
true === true // true
false === false // true
1 === 1 // true
'a' === 'a' // true
{} === {} // false
[] === [] // false
() => {} === () => {} // false
目录
- React.memo()
- React.useCallback()
- React.useMemo()
*以下所有例子将采用函数式组件进行说明
React.memo()
Q: React中当组件的props或state变化时,会重新渲染视图,但实际开发中并不需要多次的渲染,直接导致的结果是用户的体验感不好。
子组件
function ChildComp () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
}
父组件
function ParentComp () {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp />
</div>
);
}
结果:子组件信息被多次打印
A:将子组件用React.memo()包一层,只用当子组件的props和state变化时,才会从新渲染子组件。
子组件(写法一)
import React, { memo } from 'react'
const ChildComp = memo(function () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
})
子组件(写法二)
import React, { memo } from 'react'
let ChildComp = function () {
console.log('render child-comp ...')
return <div>Child Comp ...</div>
}
ChildComp = memo(ChildComp)
结果:子组件信息只在在父组件被初次渲染的时候打印了一次
React.useCallback()
上述例子子组件并没有接收任何的参数,如果接收参数React.Memo还是否有用?
子组件
import React, { memo } from 'react'
const ChildComp = memo(function ({ name, onClick }) {
console.log('render child-comp ...')
return <>
<div>Child Comp ... {name}</div>
<button onClick={() => onClick('hello')}>改变 name 值</button>
</>
})
父组件
function ParentComp () {
const [ count, setCount ] = useState(0)
const increment = () => setCount(count + 1)
const [ name, setName ] = useState('hi~')
const changeName = (newName) => setName(newName) // 父组件渲染时会创建一个新的函数
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
结果:子组件的信息仍然打印了多次
原因:
- 点击按钮导致父组件重新渲染(count值发生了变化),并且会重新创建changeName函数(函数也属于引用数据类型,每次重新创建时地址不同),子组件的属性(函数)发生了变化,因此子组件被重新渲染
解决:修改父组件的changeName方法,用useCallback钩子函数包裹一层
import React, { useCallback } from 'react'
function ParentComp () {
// ...
const [ name, setName ] = useState('hi~')
// 每次父组件渲染,返回的是同一个函数引用
const changeName = useCallback((newName) => setName(newName), [])
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp name={name} onClick={changeName}/>
</div>
);
}
useCallback()起到了缓存的作用,即使父组件渲染了,被useCallback()的函数也不会重新创建
React.useMemo()
Q:父组件传递给子组件的数据不是字符串而是对象,React.Memo是否还有用?
子组件
import React, { memo } from 'react'
const ChildComp = memo(function ({ info, onClick }) {
console.log('render child-comp ...')
return <>
<div>Child Comp ... {info.name}</div>
<button onClick={() => onClick('hello')}>改变 name 值</button>
</>
})
父组件
import React, { useCallback } from 'react'
function ParentComp () {
// ...
const [ name, setName ] = useState('hi~')
const [ age, setAge ] = useState(20)
const changeName = useCallback((newName) => setName(newName), [])
const info = { name, age } // 复杂数据类型属性
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp info={info} onClick={changeName}/>
</div>
);
}
结果:子组件信息被打印了多次
原因:跟传递函数原因类似,当点击按钮时,父组件会被重新渲染,info会生成一个新的对象(拥有新的地址),进而导致子组件被重新渲染
解决:使用useMemo将该对象包裹住
useMemo有两个参数:
- 第一个参数是函数,返回的对象指向同一个引用,不会创建新的对象
- 第二参数是数组,当数组中的变量发生改变时,第一个参数的函数才会返回一个新的对象
function ParentComp () {
// ....
const [ name, setName ] = useState('hi~')
const [ age, setAge ] = useState(20)
const changeName = useCallback((newName) => setName(newName), [])
const info = useMemo(() => ({ name, age }), [name, age]) // 包一层
return (
<div>
<button onClick={increment}>点击次数:{count}</button>
<ChildComp info={info} onClick={changeName}/>
</div>
);
}
总结
- 上述三种方法都是为了解决组件不必要的重复渲染问题 ,但针对的情况有所不同
- React.Memo(),前提是子组件没有接收父组件传递的引用类型参数(函数,对象,数组),使用:用React.Memo包裹子组件
- React.useCallBack(),当子组件接收来自父组件的函数,使用:将函数用React.useCallback(函数,[])包裹
- React.useMemo(),当子组件接收来自父组件传递的值是对象,使用:将对象用React.useMemo(()=>{对象},[监听的值])进行包裹