目录
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 三者区别