useMemo & useCallback?
在 React 16.8 新增了 Hook 的特性,在不编写 Class
的情况下使用 state
以及其他的 React 特性。
都知道函数组件中是 没有生命周期构子 和 state
的。React 新增的 Hook 为其增加了生命周期的特性在此就不多加讨论了。
这次主要讲的是 useMemo
和 useCallback
,主要是作为性能优化的手段
useMemo
使用 function
的形式来声明组件(即函数组件),失去了shouldCompnentUpdate
(在组件更新之前)这个生命周期,主要进行状态对比,如果需要则进行改变。
也就是说使用Hooks的 useEffect
(替代生命周期)后我们没有办法通过组件更新前条件来决定组件是否更新。
而且在函数组件中,也不再区分 mount
和 update
两个状态,这意味着函数组件的每一次调用都会执行内部的所有逻辑,就带来了非常大的性能损耗,例如子组件里有异步请求,那每次执行都会请求一次。
useMemo
是作为一个具有暂存值能力的 Hook。
// 格式,只有依赖数组中的依赖项改变才会重新计算新的值
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
举例:
有一个父组件和一个子组件,父组件有个点击累加器,父组件会传递一个 data
给子组件
import React, { memo, useState } from 'react'
const Child = memo(({ data, onChange }) => {
console.log('child render...')
return (
<div>
<div>child</div>
<div>{data.name}</div>
<input type="text" onChange={onChange} />
</div>
);
})
const Father = () => {
console.log('Hook render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('rose')
const [text, setText] = useState('')
const data = {
name
}
return (
<div>
<div>count: {count}</div>
<div>text : {text}</div>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<Child data={data} />
</div>
)
}
export default Father
运行效果图:
分析
父组件和子组件都执行一遍,没有问题。当我点击 count+1
按钮后,count
的值改变,name
没有改变,即依赖 name
的 data
也没有改变,但实际子组件也重新执行了一边。
🤔🤔🤔
因为,count
每次更新时,Hook
都会执行一遍,则 data
每次都会重新声明一遍,导致 data
的数据没有变,但地址变了,因此 child
组件会有不必要的更新。
// 将 data 改为
const data = useMemo(() => {
return { name }
}, [name])
重复上例步骤,结果, child
组件并没有更新了,只有在 name
改变时 child
组件才更新一遍。这就是 useMemo
带来的性能优化效果。
useCallback
useCallback
和 useMemo
类似,都有着缓存的能力。而 useCallback
是缓存函数的。
// 格式
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback(fn, deps)
// 相当于
useMemo(() => fn, deps)
官方解析:把内联回调函数及依赖项数组作为参数传入
useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如shouldComponentUpdate
)的子组件时,它将非常有用。
举例:
import React, { memo, useCallback, useState, useMemo } from 'react'
const Child = memo(({ data, onChange }) => {
console.log('child render...')
return (
<div>
<div>child</div>
<div>{data.name}</div>
<input type="text" onChange={onChange} />
</div>
);
})
const Father = () => {
console.log('Hook render...')
const [count, setCount] = useState(0)
const [name, setName] = useState('zhian')
const [text, setText] = useState('')
const data = useMemo(() => {
return { name }
}, [name])
const onChange = () => {
setText(e.target.value)
}
return (
<div>
<div>count: {count}</div>
<div>text : {text}</div>
<button onClick={() => setCount(count + 1)}>count + 1</button>
<Child data={data} onChange={onChange} />
</div>
)
}
export default About
这个例子执行上面 useMemo
同样的操作,出现同样的问题,Child
组件被更新了一遍,但实际值并没有改变。
导致的原因也是和上面的一样,地址改变了。
// 将onChange改为一下就好了,原理原因也是一样
const onChange = useCallback((e) => {
setText(e.target.value)
}, [])
这时候应该会提出一个疑问,为什么,值每修改一遍,组件都会执行一遍???
解析
在js中,当函数执行时,会创建一个被称为执行环境的对象,这个对象在每次函数执行时都是不同的,当多次执行该函数时会创建多个执行环境。这个执行环境会在函数执行完毕后销毁。所以每次 rerender 时都会创建新的执行环境,并为其内部的方法重新分配空间。
附:REACT.memo
优化
class
组件中可以使用 shoudComponentUpdate
, PureComponent
来做性能优化。 React为函数式组件提供了叫React.memo
一个高阶组件, 只适用于函数组件,而不适用 class
组件。
通过将组件包装在React.memo
中调用,通过这种记忆组件渲染结果的方式来提高组件的性能。这意味着当props
没有变化时, React
将跳过渲染组件的操作并直接复用最近一次渲染的结果。
const MyComponent = React.memo(function (props) {
// only renders if props have changed
})
默认情况下其只会对 props
做浅层对比,遇到层级比较深的复杂对象时,表示力不从心了。可以传入第二个参数进行深度比较。(比如对象,可以使用 lodash 的 _.isEqual(value, other)
进行深比较)
function arePropsEqual(prevProps, nextProps) {
// your code
return prevProps === nextProps;
}
export default memo(Button, arePropsEqual);
- 首先,
memo
的用法是:函数组件里面的PureComponent
但是,如果函数组件被
React.memo
包裹,且其实现中拥有useState
或useContext
的 Hook,当 context 发生变化时,它仍会重新渲染。
- 而且,memo是浅比较,意思是,对象只比较内存地址,只要你内存地址没变,管你对象里面的值千变万化都不会触发render