React 简单性能优化 - useCallback and useMemo
前言
在编写react组件的时候,部分组件被称为纯函数,既是同样的输入就会有着同样的输出,如:
function double(number) {
return 2 * number;
}
你输入 2 返回的就一定是 4,同理,对于一些函数式组件,也能够是纯函数
function Child({data}) {
console.log("渲染")
return (
<div>child</div>
)
}
所以同理,我们肯定是希望一个纯组件,在他的依赖(也就是data)没有发生变化的时候,他是不需要重新渲染的。
React.memo
和 React.PureComponent
react 提供给了我们两个方法去解决这个问题,一个是函数式组件的 React.memo
以及类组件的React.PureComponent
首先是类组件,我们一般类组件需要继承于 React.Component
import React, { Component, PureComponent } from 'react';
import PropTypes from 'prop-types';
class App extends Component {
constructor(props) {
super(props);
this.state = {number: 0}
}
add = () => {
this.setState({
number: 0
}
}
componentDidUpdate () {
console.log('组件更新')
}
render() {
console.log('组件更新')
return (
<div>
<span>{this.state.number}</span>
<button onClick={this.add}>+</button>
</div>
);
}
}
export default App;
这样哪怕是没有修改组件的状态,组件还是会不断地更新,但是只要把 extends Component
替换为 extends PureComponent
这样在组件状态没有发生变化的时候,组件就不会触发重渲染
然后是函数式组件,函数式组件比较不同的地方是,要是组件的状态没有改变的话,函数式组件是不会重新渲染的,但是要是父组件引用子组件,父组件的状态发生变化,子组件就会重新渲染,哪怕改变的这个状态子组件并没有依赖。
import React ,{useState}from 'react';
function Child({data}) {
console.log("渲染")
return (
<div>child</div>
)
}
function App(){
const [count, setCount] = useState(0);
return (
<div>
<Child data={123}></Child>
{count}<br/>
<button onClick={() => { setCount(count + 1)}}>
增加
</button>
</div>
);
}
export default App;
同样的,可以加入 React.memo
来解决这个问题,只要在 子组件被引用前加入一层包装 Child = memo(Child)
就不会触发重复渲染了。
useCallback
在上面的例子当中,我们来为 Child 组件新增一个入参 onClick 事件,具体为
const addClick = ()=>{console.log("addClick")}
...
<Child data={123} onClick={addClick}></Child>
这样哪怕是组件使用 memo 包裹了一层,也还是会在父组件改变状态以后发生重渲染。
因为父组件状态改变了,那么 addClick 函数就会重新赋值 const addClick = ()=>{console.log("addClick")}
每次赋值的都是一个新的函数
memo
是通过校验 Props 中的数据的 内存地址 是否改变来决定组件是否重新渲染组件的一种技术。父组件重新构建的时候,如果不缓存计算属性,则会返回了一个在新的存储地址的返回值,它传入到子组件中会被检测为栈地址更新,从而发生重新渲染
这时候我们就可以使用 useCallback
包裹目标函数来避免这种情况。useCallback
是一个函数,入参为两个,第一个是需要缓存的函数,第二个是依赖项。
const addClick = useCallback(()=>{console.log("addClick")}, [])
当你传入一个空数组的时候,就只有在最开始会渲染一次子组件,之后因为函数 addClick
没有发生变化,所以子组件就不会触发重渲染,当你在依赖中传入一个会发生改变的状态值的时候,比如说我把 [count]
这样传入,那么每次 count
发生变成就会导致 函数 获取到一个全新的函数,子组件重新渲染。
useMemo
useMemo
和 useCallback
作用类似,不过 useCallback
用于缓存函数, useMemo
用于缓存函数返回值
假如我现在有一个 getData 的函数,然后我们要将这个函数的返回值赋值为 data 并且传给子组件
const [aaa, setAaa] = useState(0);
data = (()=>{
console.log("🚀 ~ file: App.js:45 ~ data=useMemo ~ aaa:", aaa)
return aaa + 1
})()
虽然函数 getData
返回的值在依赖不变得情况下可能是固定的,但是每次 data
都重新赋值,导致 log
不断打印
为了解决这个问题,我们就可以使用 useMemo
来缓存下函数得返回值,并且用于传入子组件,useMemo
的入参和 useCallback
是相同的,要么传入一个空数组,那就只有在首次渲染的时候改变,要么传入依赖项,这样就只有在依赖项改变的时候才会改变。
data = useMemo(()=>{
console.log("🚀 ~ file: App.js:45 ~ data=useMemo ~ aaa:", aaa)
return aaa + 1
}, [aaa])
这样只要依赖变量没有发生改变,那么函数就不会重新运行。
手写 useCallback
useCallback
需要传入两个参数,一个函数,一个依赖数组.
首先我们需要在全局定义一个 index 代表当前是第几个 hook,再定义一个 states 数组用于保存之前按的状态,并且在每次组件重渲染的时候,index 应该要置为初始值,这样才能够从头按顺序查找保存进 states 的函数。
并且在依赖项发生改变的时候,需要去重新获取 states 里面保存的值。进行更新操作
let hookIndex = 0
let hookState = []
function useCallback(callback, dependencies) {
// 如果已经缓存过对象
if (hookState[hookIndex]) {
let [lastCallback, lastDependencies] = hookState[hookIndex] // 读取缓存的hookState中的useCallback
// 遍历dependencies 和lastDependencies,如果没有变化则返回原来的对象
// 如果有变化,返回最新的dependenies
const isSame = dependencies.every((item, index) => item === lastDependencies[index]) // 遍历判断
if (isSame) {
// 如果没有变化,直接返回原来的
hookIndex++
return lastCallback
} else {
// 如果有变化,进行更新并返回新结果
hookState[hookIndex++] = [callback, dependencies]
return callback
}
} else {
// 没有缓存过对象
hookState[hookIndex++] = [callback, dependencies] // 将第一次的结果缓存起来
return callback
}
}
function App(){
hookIndex = 0
// 最开始的时候恢复为 0
...
}
这样就可以简单模拟,并且实现了多个同时调用也不会出错
手写 useMemo
useMemo
和 useCallback
的实现类似,只不过 useCallback
缓存的是函数,useMemo
缓存的是这个函数的返回值
function useMemo (callback, dependencies) {
// 如果已经缓存过对象
if (hookState[hookIndex]) {
let [lastCallback, lastDependencies] = hookState[hookIndex] // 读取缓存的hookState中的useCallback
// 遍历dependencies 和lastDependencies,如果没有变化则返回原来的对象
// 如果有变化,返回最新的dependenies
const isSame = dependencies.every((item, index) => item === lastDependencies[index]) // 遍历判断
if (isSame) {
// 如果没有变化,直接返回原来的
hookIndex++
return lastCallback
} else {
let callbackData = callback()
// 如果有变化,进行更新并返回新结果
hookState[hookIndex++] = [callbackData, dependencies]
return callbackData
}
} else {
// 没有缓存过对象
let callbackData = callback()
hookState[hookIndex++] = [callbackData, dependencies] // 将第一次的结果缓存起来
return callbackData
}
}
至此简单也简单模拟了 useMemo
,能够缓存函数的返回值。
转载自:https://juejin.cn/post/7270534122610573324