聊聊 React 里的 Memoize

Posted 奴心

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊 React 里的 Memoize相关的知识,希望对你有一定的参考价值。

本文将阐述 React 里何时何处应该使用 memoize,希望您读后能明白放心地丢掉多余的 useCallback,让 React Hooks 用的更自然。

1

UI 是稳定的

React 这条路之所以能走通,都是基于这一条假设:UI 整体上是稳定的。原话记不得了,大体这个意思,翻了一下 React 文档也暂时没有找到,有知道的朋友可以告诉我。

网上找不到关于这个假设的详细解释,这里谈一下个人的理解,因为这个概念对后面的理解很重要。我们都知道 io 是计算机里最慢的部分,而 io 里最慢的,估计就是人机交互(人人交互也很慢 )了。UI 的稳定全靠这个 U 了,同时对一个页面的各个部分发起高频点击应该不是一般 U 可以做到的。UI稳定就代表这 React 的 state 是稳定的,也就是说 render 的调用“大多”是低频的。

整体稳定不代表局部是稳定的,你还是可以在一个输入框里“啪啪啪……”不停的(按键盘 )。如果这个“啪啪啪”的位置在组件树的叶子上还好,但如果在根部就麻烦了,整棵树都会“晃”的。

2

如何处理频繁更新的局部状态?

最直接的办法是把这个不安分分子踢出组件树,到 dom 层去处理。但这么做有几点弊端,一是没了 vdom 的跨平台,另一点是太麻烦。还有就是可能关联的节点太多,不可能都拿掉,这时候就需要 memoize 来救场了。

3

memoize 的位置

通常,我们不应该 export 一个 memoize 的组件,原因是组件本身通常不知道自身被使用时的环境,也就是不知道传过来的参数是否有 memoize 的价值。而且基于前面的假设,我们应该认为传过来的参数是稳定的。所以这个 memoize 应该由使用方来做的。useCallback 和 memo 成对出现的关系也可以侧面印证这个观点。如果 memoize 被组件提供方封装了,使用方不了解组件有没有被 memoize,根本无法判断要不要 useCallback。(注:这里讲的和 React 官方文档里 memo 示例是有出入的,请读者自行思考验证。)

另外一点,memoize 应该尽量把变化隔离在更小的影响范围内,也就是越早越好。这个位置就是那个“啪啪啪”状态所在的组件内部。

function Container() {
// 假设 value 常变
const [value, setValue] = useState();
// foo 代表不经常变的一批状态
const [foo, setFoo] = useState();
const other = useMemo(() => <Other value={foo} />, [foo]);

return <div>
<input onChange={e => setValue(e.target.value)} placeholder="在这里“啪啪啪”" type="text" />
<Display text={value} foo={foo} />
{/* 这里应该对 Other 进行 memoize */}
{/* <Other value={foo} /> */}
{ other }
</div>;
}


4

频繁变更状态的使用方内部是否需要 memoize

考虑下面 Display 实现:

export function Display({text, foo}) {
return <div>
<TextRender value={text} />
{/* 这里要 memoize Foo 吗?*/}
<Foo foo={foo} />
</div>;
}

在 Display 内部看来,是不知道 text 和 foo 哪个会频繁更新的,所以不应该做任何 memoize 的优化的。因为下一个调用者可能就会频繁更新 foo 而不更新 text,也可能两个都不频繁更新(大概率,基于 UI 稳定的假设)。

在不破坏封装性的情况下,Container 是没办法知道内部组件如何使用 text 和 foo 的,所以 Container 只能假设 Display 是一个原子的轻量组件。那么很可能会遇到性能问题,至少我们可以看到 Foo 可以被 memoize 优化。

这时候就遇到一个取舍问题了,我们是应该维持 Display 的通用性,还是针对场景做性能优化?我觉得在没遇到性能问题时首先保持 Display 的通用性,如果真的遇到问题,再去选择或实现 Display 特定场景的版本。

比如 text 被深层次传递了,Display 可以和 Container 【约定】 通过 context 来传递。

5

memo vs useMemo

因为 memo 是组件的使用方来做的,所以 memo 的使用场景还是比较窄的。举例:

import React, {memo} from 'react';
import Foo from 'foo';
import Bar from 'bar';

const MemoFoo = memo(Foo);

function Baz() {
// ...

return <div>
<MemoFoo {...} />
<Bar {...} />
<MemoFoo {...} />
</div>;
}

所以,大多数情况用 useMemo 就可以满足使用场景。


6

总结

大多数情况下不必考虑 rerender 的问题, 除非发现一个频繁变更的状态确实影响了体验。

如果需要使用 memoization 优化渲染:

  1. 找到频繁变更的状态所在的组件

  2. 找到这个组件里这个状态的消费者

  3. 对这个消费者之外的其它子组件用 useMemo 进行状态隔离

  4. 如果消费者内部还有性能问题,使用一个特殊优化的组件代替

忘掉 useCallback 和 memo API 吧!等你遇到钉子的时候再去找你的锤子。


-- end --

往期推荐






以上是关于聊聊 React 里的 Memoize的主要内容,如果未能解决你的问题,请参考以下文章

聊聊自动化测试里的数据驱动和关键字驱动

javascript 用于在节点#nodejs #javascript内设置react app的代码片段

ModuleNotFoundError:没有名为“memoize”的模块

Perl性能优化之Memoize的实现原理

React memoization技术:将函数计算结果缓存起来

lodash _.memoize