React Hooks之useMemo

1、触发界面渲染,触发函数执行

参考博客《useMemo和useEffect有什么区别?(使用场景推荐阅读)》中的例子。当组件中点击按钮触发界面重新渲染时,也会触发界面其他内容重新渲染,如果其他内容包含由函数执行返回的结果,那么该函数会被重新执行计算内容,那么这样会导致不必要的性能浪费。

这样就引出一个问题:在DOM发生变化时不想触发不相关的函数,该如何做到?

// 例子在这里:该组件中存在两个按钮,一个用于让价格改变,一个用于让名字改变

// 点击价钱+1的按钮会导致页面渲染,然而这也会让getProductName()函数重新执行,实际上是不需要的。因为我们只想让这个函数在产品名字改变的时候再触发,而不是每次重新渲染都触发。
import React, {Fragment} from 'react'
import { useState, useMemo } from 'react'
 
// 产品名称列表
const nameList = ['apple', 'peer', 'banana', 'lemon']
 
const example = (props) => {
    // 产品名称、价格
    const [price, setPrice] = useState(0)
    const [name, setName] = useState('apple')
  
    // 假设有一个业务函数  获取产品的名字
    function getProductName() {
        console.log('getProductName触发')
        return name
    }
 
    return (
        <Fragment>
            <p>{name}</p>
            <p>{price}</p>
            <p>{getProductName()}</p>
            <button onClick={() => setPrice(price+1)}>价钱+1</button>
            <button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
        </Fragment>
    )
}
export default example
 

1.1、使用useEffect函数解决,在DOM发生变化时不想触发不相关的函数,这个问题?

官网Effect Hook中明确给出:当你调用 useEffect 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。(副作用如:数据获取、订阅或者手动修改DOM等等操作)

也就是说useEffect是在DOM更新/渲染完毕之后才会执行,这显然与我们的初衷不符。

比如下面的组件,React 就是在更新 DOM 后会设置一个页面标题:

import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  // 相当于 componentDidMount 和 componentDidUpdate:
  useEffect(() => {
    // 使用浏览器的 API 更新页面标题
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

使用useMemo可以解决这个问题,因为官网useMemo中给出:传入 useMemo 的函数会在渲染期间执行,所以可以使用useMemo解决渲染DOM期间会执行不相关函数的需求。

2、useMemo

为了进行性能优化,React提供了useMemo 和 useCallback。这两个函数的调用形式为

useCallback(fn, deps) 
useMemo(() => fn, deps)

其中useMemo 用来保持一个对象引用不变

React-useMemo中给出了使用useMemo的介绍:它返回一个memoized 值,且仅会在某个依赖项改变时才重新计算memoized值。如果没有提供依赖项数组,则它在每次渲染时都会计算新的值。(渲染会触发函数执行,从而导致重新计算)

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
import React, {Fragment} from 'react'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { observer } from 'mobx-react'
 
const nameList = ['apple', 'peer', 'banana', 'lemon']
const Example = observer((props) => {
    const [price, setPrice] = useState(0)
    const [name, setName] = useState('apple')
    
    
    function getProductName() {
        console.log('getProductName触发')
        return name
    }
    // 只对name响应
    useEffect(() => {
        console.log('name effect 触发')
        getProductName()
    }, [name])
    
    // 只对price响应
    useEffect(() => {
        console.log('price effect 触发')
    }, [price])
  
    // memo化的name属性,只对name响应
    const memo_name = useMemo(() => {
        console.log('name memo 触发')
        return () => name  // 返回一个函数
    }, [name])
 
    return (
        <Fragment>
            <p>{name}</p>
            <p>{price}</p>
            <p>普通的name:{getProductName()}</p>
            <p>memo化的:{memo_name()}</p>
            <button onClick={() => setPrice(price+1)}>价钱+1</button>
            <button onClick={() => setName(nameList[Math.random() * nameList.length << 0])}>修改名字</button>
        </Fragment>
    )
})
export default Example

此时

// 点击修改价格按钮
"getProductName触发" // DOM改变,触发在p标签中的getProductName函数
"price effect 触发"  // 在DOM渲染结束调用effect
// 点击修改名字按钮
"name memo 触发" // DOM渲染,触发了name的memo
"getProductName触发" // DOM渲染触发p标签内的getProductName函数重新获取新名字
"name effect" // 在DOM渲染后触发name的useEffect
"getProductName触发" // DOM操作结束后在name的effect中触发getProductName

从这个例子中可以看出,useMemo已经成功的控制了触发函数执行的操作。但要注意的是,不能在useMemo中操作DOM等类型的副作用操作/不能在函数内部执行与渲染无关的操作(比如setState,因为这会产生死循环,或者出现too many re-render警告,因为useMemo本身就是在DOM渲染期间执行的,如果你在useMemo内操作DOM那么又会重新触发渲染导致触发useMemo…)

useMemo类比生命周期就是[shouldComponentUpdate]
在这里插入图片描述

2.1、useMemo 是拿来保持一个对象引用不变的

如下<LineChart>组件会在dataConfig发生变化时重新取数

<LineChart 
  dataconfig={{ // 取数配置
    ...dataConfig,
    datasetId: getDatasetId(queryId)
  }} 
  fetchData={(newDataConfig) => { // fetcher
    realFetchData(newDataConfig);
  }} 
/>

如果<LineChart>是一个类组件,那它一般会在componentWillReceiveProps中对数据进行更新

class LineChart extends React.Component {
  componentWillReceiveProps(nextProps) {
    // 当 dataConfig 发生变化时重新取数
    if (nextProps.dataConfig !== this.props.dataConfig) {
      nextProps.fetchData(nextProps.dataConfig);
    }
  }
  
}

如果<LineChart>是一个函数式组件,那它一般会在useEffect中对数据进行更新操作

function LineChart ({ dataConfig, fetchData }) {
  useEffect(() => {
    fetchData(dataConfig);
  }, [dataConfig, fetchData])
}

从上面可以看出,类组件和函数式组件在开发心智上的区别在于:类组件需要开发者自己管理依赖;而函数式组件中则是交给react自动管理。虽然函数式组件减少了开发者手动diff的工作量,但是由于React做的是浅比较,所以当依赖的引用变化时也会导致重新获取数据。

等等,依赖的引用为什么会发生变化呢?

因此要想办法让依赖的引用不发生变化,这时可以使用useCallback来解决函数引用的问题

// 让linechart中的props引用不发生变化
const fetchData = React.useCallback((newDataConfig) => {
    realFetchData(newDataConfig);
  }, [realFetchData]);

const dataConfig = useMemo(() => ({
    ...dataConfig,
    datasetId: getDatasetId(queryId)
  }), [getDatasetId, queryId]);

return <LineChart 
  dataconfig={dataConfig} 
  fetchData={fetchData} 
/>

只用 useMemo 和 useCallback 来做性能优化可能是无法得到预期效果的,原因是如果 props 引用不会变化,子组件不会重新渲染,但它依然会重新执行。如果想要阻断重新执行,React 提供了一个 API:memo,它相当于 PureComponent,是一个高阶组件,默认对 props 做一次浅比较,如果 props 没有变化,则子组件不会重新执行。

3、总结

useMemo的使用场景:父组件将一个 【值】 传递给子组件时,若父组件的其他值发生变化时,子组件也会跟着渲染多次,会造成性能浪费。使用useMemo将父组件传递给子组件的值缓存起来,只有当 useMemo中的第二个参数状态变化时,子组件才重新渲染

参考

useMemo和useEffect有什么区别?(使用场景推荐阅读)

React Hooks: 深入剖析 useMemo 和 useEffect
React Hooks: 深入剖析 useMemo 和 useEffect-知乎版本
useMemo, useCallback, useEffect 三者区别

  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值