likes
comments
collection
share

React 简单性能优化 - useCallback and useMemo

作者站长头像
站长
· 阅读数 12

前言

在编写react组件的时候,部分组件被称为纯函数,既是同样的输入就会有着同样的输出,如:

function double(number) {  
	return 2 * number;  
}

你输入 2 返回的就一定是 4,同理,对于一些函数式组件,也能够是纯函数

function Child({data}) {
  console.log("渲染")
  return (
      <div>child</div>
  )
}

所以同理,我们肯定是希望一个纯组件,在他的依赖(也就是data)没有发生变化的时候,他是不需要重新渲染的。

React.memoReact.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;

React 简单性能优化 - useCallback and useMemo

这样哪怕是没有修改组件的状态,组件还是会不断地更新,但是只要把 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 简单性能优化 - useCallback and useMemo

同样的,可以加入 React.memo 来解决这个问题,只要在 子组件被引用前加入一层包装 Child = memo(Child) 就不会触发重复渲染了。

React 简单性能优化 - useCallback and useMemo

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")}, [])

React 简单性能优化 - useCallback and useMemo

当你传入一个空数组的时候,就只有在最开始会渲染一次子组件,之后因为函数 addClick 没有发生变化,所以子组件就不会触发重渲染,当你在依赖中传入一个会发生改变的状态值的时候,比如说我把  [count] 这样传入,那么每次 count 发生变成就会导致 函数 获取到一个全新的函数,子组件重新渲染。

React 简单性能优化 - useCallback and useMemo

useMemo

useMemouseCallback 作用类似,不过 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 
  ...
}

这样就可以简单模拟,并且实现了多个同时调用也不会出错

React 简单性能优化 - useCallback and useMemo

React 简单性能优化 - useCallback and useMemo

React 简单性能优化 - useCallback and useMemo

手写 useMemo

useMemouseCallback 的实现类似,只不过 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 ,能够缓存函数的返回值。