做者 | Ohans Emmanuel译者 | 王强编辑 | Yonie在使用 React Hooks 的过程当中,做者发现过渡频繁的应用 useMemo 用会影响程序的性能。在本文中做者将与你们分享如何避免过分使用 useMemo,使程序远离性能问题。前端
通过观察总结,我发如今两种状况下 useMemo 要么没什么用,要么就是用得太多了,并且可能会影响应用程序的性能表现。react
第一种状况很容易就能推断出来,可是第二种状况就比较隐蔽了,很容易被忽略。若是你在生产环境的应用程序中使用了 Hook,那么你就可能会在这两个场景中使用 useMemo Hook。git
下面我就会谈一谈为何这些 useMemo 没什么必要,甚至可能影响你的应用性能。此外我会教你们在这些场景中避免过分使用 useMemo 的方法。github
咱们开始吧。小程序
不须要 useMemo 的状况为了方便,咱们把这两类场景分别称为狮子和变色龙。前端工程化
先不用纠结为何这么叫,继续读下去就是。数组
当你撞上一头雄狮,你的第一反应就是撒丫子跑,不要成为狮子的盘中餐,而后活下来跟别人吹牛。这时候可没空思考那么多。安全
这就是场景 A。它们是狮子,你应该下意识地躲开它们。前端框架
但在谈论它们以前,咱们先来看看更隐蔽的变色龙场景。微信
相同的引用和开销不大的操做参考下面的示例组件:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
const resolvedValue = useMemo(() => {
getResolvedValue(page, type)
}, [page, type])
return <ExpensiveComponent resolvedValue={resolvedValue}/>
}
如上所示,显然做者使用了 useMemo。这里他们的思路是,当对 resolvedValue 的引用出现更改时,他们不想从新渲染 ExpensiveComponent。
虽然说这个担心是正确的,但不管什么时候要用 useMemo 以前都应该考虑两个问题:其次,给定相同的输入值时,对记忆(memoized)值的引用是否会发生变化?例如在上面的代码块中,若是 page 为 2,type 为“GET”,那么对 resolvedValue 的引用是否会变化?简单的回答是考虑 resolvedValue 变量的数据类型。若是 resolvedValue 是原始值(如字符串、数字、布尔值、空值、未定义或符号),则引用就不会变化。也就是说 ExpensiveComponent 不会被从新渲染。
修正过的代码以下:
/**
@param {number} page
@param {string} type
**/
const MyComponent({page, type}) {
const resolvedValue = getResolvedValue(page, type)
return <ExpensiveComponent resolvedValue={resolvedValue}/>
}
如前所述,若是 resolvedValue 返回一个字符串之类的原始值,而且 getResolvedValue 这个操做的开销没那么大,那么这段代码就很是合理,效率够高了。
只要 page 和 type 是同样的,好比说没有 prop 更改,resolvedValue 的引用就会保持不变,只是返回的值不是原始值了(例如变成了对象或数组)。
记住这两个问题:要记住的函数开销很大吗,返回的值是原始值吗?每次都思考这两个问题的话,你就能随时判断使用 useMemo 是否合适。
出于多种缘由须要记住默认状态参考如下代码块:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
const defaultState = useMemo(() => ({
fetched: someOperationValue(),
type: type
}), [type])
const [state, setState] = useState(defaultState);
return <ExpensiveComponent />
}
有人会以为上面的代码没什么问题,但这里 useMemo 调用确定是没什么意义的。
首先咱们来试着理解一下这段代码背后的思想。做者的思路很不错。当 type prop 更改时他们须要新的 defaultState 对象,而且不但愿在每次从新渲染时都引用 defaultState 对象。
虽然说这些问题都很实际,但这种方法是错误的,违反了一个基本原则:useState 是不会在每次从新渲染时都从新初始化的,只有在组件重载时才会初始化。
传递给 useState 的参数更名为 INITIAL_STATE 更合理。它只在组件刚加载时计算(或触发)一次。
useState(INITIAL_STATE)
虽然做者担忧在 useMemo 的 type 数组依赖项发生更改时获取新的 defaultState 值,但这是错误的判断,由于 useState 忽略了新计算的 defaultState 对象。
懒惰初始化 useState 时也是同样的道理,以下所示:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
// default state initializer
const defaultState = () => {
console.log("default state computed")
return {
fetched: someOperationValue(),
type: type
}
}
const [state, setState] = useState(defaultState);
return <ExpensiveComponent />
}
在上面的示例中,defaultState 初始函数只会在加载时调用一次。这个函数不会在每次从新渲染时再被调用。所以“默认状态计算”这条日志只会出现一次,除非组件又重载了。
上面的代码改为这样:
/**
@param {number} page
@param {string} type
**/
const myComponent({page, type}) {
const defaultState = () => ({
fetched: someOperationValue(),
type,
})
const [state, setState] = useState(defaultState);
// if you really need to update state based on prop change,
// do so here
// pseudo code - if(previousProp !== prop){setState(newStateValue)}
return <ExpensiveComponent />
}
下面来谈一些更隐蔽的场景。
把useMemo看成ESLint Hook警告看看这些评论(详情见下方连接)就能知道,人们在千方百计避免官方的 ESLint Hooks 插件发出 lint 警告。我也很理解他们的困境。
评论连接: https://github.com/facebook/create-react-app/issues/6880
我赞成 Dan Abramov 的观点(详情见下方连接)。遏制插件中的 eslint-warnings 可能会在未来某天付出相应的代价。
Dan Abramov 的观点:
https://github.com/facebook/create-react-app/issues/6880#issuecomment-485912528
通常来讲,我认为咱们不该该在生产环境的应用程序中遏制这些警告,这样作的话未来就更有可能出现一些隐蔽的错误。
话虽如此,有些状况下咱们仍是想要遏制这些 lint 警告。如下是我遇到的一个例子。这里的代码是简化过的,方便理解:
function Example ({ impressionTracker, propA, propB, propC }) {
useEffect(() => {
// 追踪初始展现
impressionTracker(propA, propB, propC)
}, [])
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}
这是一个至关棘手的问题。
在上面这个场景中你不关心 props 是否改变。你只想用随便哪一个初始 props 调用 track 函数。这就是展现跟踪(impression tracking)的工做机制。你只能在组件加载时调用展现跟踪函数。这里的区别是你须要使用一些初始 props 调用该函数。
你可能会想只要简单地将 props 重命名为 initialProps 之类的东西就能解决问题了,但这是行不通的。这是由于 BeautifulComponent 也须要接收更新的 prop 值。
在这个示例中,你将收到 lint 警告消息:“React Hook useEffect 缺乏依赖项:'impressionTracker'、'propA'、'propB'和'propC'。能够包含它们或删除依赖数组。“
这条消息语气很让人不爽,但 linter 也只是在作本身的工做而已。简单的解决方案是使用 eslint-disable 注释,但这种方法不见得是最合适的,由于未来你可能在同一个 useEffect 调用中引入错误。
useEffect(() => {
impressionTracker(propA, propB, propC)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
个人建议是使用 useRef Hook 来保持对不须要更新的初始 prop 值的引用。
function Example({impressionTracker, propA, propB, propC}) {
// 保持对初始值的引用
const initialTrackingValues = useRef({
tracker: impressionTracker,
params: {
propA,
propB,
propC,
}
})
// 展现跟踪
useEffect(() => {
const { tracker, params } = initialTrackingValues.current;
tracker(params)
}, []) // 对 tracker 或 params 没有 ESLint 警告
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}
根据个人测试,在这些状况下 linter 只会考虑 useRef。使用 useRef 后,linter 就明白引用的值不会改变,所以你不会收到任何警告!哪怕你用 useMemo 也逃不开这些警告的。
例如:
function Example({impressionTracker, propA, propB, propC}) {
// useMemo 记住这个值,使它保持不变
const initialTrackingValues = useMemo({
tracker: impressionTracker,
params: {
propA,
propB,
propC,
}
}, []) // 这里出现 lint 警告
// 展现跟踪
useEffect(() => {
const { tracker, params} = initialTrackingValues
tracker(params)
}, [tracker, params]) // 这些依赖项必须放在这里
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}
上面这个方案就是错误的,即便我用 useMemo 记忆初始 prop 值来跟踪初始值,最后仍是无济于事。在 useEffect 调用中,记忆值 tracker 和 params 仍然必须做为数组依赖项输入。
有些人就会这样用 useMemo,这种用法不对,应该避免。咱们应该使用 useRef Hook,如前所述。
总而言之,若是你真的想要消除 lint 警告的话,你会发现 useRef 是你的好朋友。
useMemo 只用于引用相等不少人都喜欢使用 useMemo 来处理开销较大的计算并保持引用相等。我赞成第一条但不一样意第二条。useMemo Hook 不该该只用于引用相等。只有一种状况下能够这样作,稍后会提到。
为何 useMemo 只用于引用相等是不对的呢?人们不都是这么作的吗?
参考下面的示例:
function Bla() {
const baz = useMemo(() => [1, 2, 3], [])
return <Foo baz={baz} />
}
在组件 Bla 中,baz 值之因此被记忆不是由于对数组 [1,2,3] 的评估开销很大,而是由于对 baz 变量的引用在每次从新渲染时都会改变。
虽然这看起来不是个问题,但我认为这里不该该使用 useMemo 这个 Hook。
首先,咱们看看数组依赖。
useMemo(() => [1, 2, 3], [])
这里,一个空数组被传递给 useMemo Hook。也就是说值 [1,2,3] 仅计算一次——也就是组件加载的时候。
所以咱们得出:被记忆的值计算开销并不大,而且在加载以后不会从新计算。
出现这种状况时,但愿你能从新考虑要不要用 useMemo Hook。你正在记忆一个不是计算开销并不大的值,它未来也不会从新计算。这不符合“memoization”一词的定义。
这个 useMemo Hook 的用法大错特错。它在语义上就错了,并且会消耗更多内存和计算资源。
那你该怎么办?
首先,做者在这里究竟想要作什么?他们不是要记住一个值;相反,他们但愿在从新渲染时保持对值的 引用 不变。
别让那条黏糊糊的变色龙钻了空子。在这种状况下请使用 useRef Hook。
例如,若是你真的讨厌使用当前属性(就像个人不少同事同样),那么只需解构并重命名便可,以下所示:
function Bla() {
const { current: baz } = useRef([1, 2, 3])
return <Foo baz={baz} />
}
问题解决了。
实际上,你可使用 useRef 来保持对开销较大的函数评估的引用——只要该函数不须要在 props 更改时从新计算就没问题。
在这些状况下 useRef 才是正确的 Hook,useMemo Hook 不合适。
使用 useRef Hook 来模仿实例变量是 Hook 的强大武库中用的最少的武器之一。useRef Hook 能作的事情远不止保持对 DOM 节点的引用。尽情拥抱它吧。
请记住这里的条件,不要只为了保持一致的引用就记忆一个值。若是你须要根据更改的 prop 或值从新计算该值,那就请随意使用 useMemo Hook。在某些状况下你仍然可使用 useRef——可是给定数组依赖列表时 useMemo 最方便。
总结远离狮子,也不要让变色龙钻了你的空子。若是你放进来变色龙,它们就会改变本身的肤色,融入你的代码库,影响你的代码质量。别给它们机会。
英文原文: https://blog.logrocket.com/rethinking-hooks-memoization/?from=singlemessage&isappinstalled=0
活动推荐GMTC 全球大前端技术大会首次落地华南,走入大湾区深圳。
往届咱们请到了来自 Google、Twitter、Instagram、阿里、腾讯、字节跳动、百度、京东、美团等国内外一线公司的顶级前端专家,分享了关于小程序、Flutter、Node、RN、前端框架、前端安全、前端工程化、移动 AI 等 50 多个热门技术专题。目前深圳站正式启动,7 折最低价售票通道已经开启,详细请咨询:13269078023(同微信)。