掌握React的稳定值
稳定值 的概念是一个明显的React术语,特别是在引入功能组件之后。它指的是在多次渲染中具有相同价值的值(通常来自一个钩子)。而且它们会立即引起混淆(正如你在下面的gist中看到的那样),所以在这篇文章中,我们将介绍一些它们真正重要的情况以及如何理解它们:
useState和 返回一个useReducer恒定的 状态更新函数*-* 钩子将永远返回相同的函数。这个值 被保证是稳定的,因为我们知道在我们调用状态更新函数之前会返回同一个对象。useRef返回一个被保证是恒定的对象。你可以改变 值,而不触发重新渲染。ref.current- 创建一个数组字面看起来很无辜......但这是*不稳定的!*每次渲染时都会创建一个新的数组实例。从性能的角度来看,这很好(除非你在数组中插入数百万个项目),但如果你把数组传递给一个期望有稳定值的函数......那就有龙了
- 对于匿名函数和对象字面也是如此。一旦你看到这一点就很明显了,但是当一个渲染问题被追踪到一个匿名函数被传递到一个
onPress道具时,许多React开发者都很惊讶。 - 为了创建一个稳定的值,
useCallback和useMemo是你的朋友。
从这个例子中,我们可以得出React中稳定性的定义:"稳定性 "是指会导致一个值改变的条件。当一个值在每次渲染时都不发生变化时,它就是稳定的 -count,setCount,ref, 和onPress 在这个定义下都是 "稳定 "的。当一个值在每次渲染时都发生变化时,它就是不稳定的 --countsArray 和func 是不稳定的。
我们可能不清楚这些都与这个稳定性问题有关,所以我们将逐一解读这些情况:
- 期待稳定值的函数(通常是依赖关系,但也包括
useFocusEffect) - 返回组件的内联函数(这有点像反模式,但也很常见,可以谈谈)。
- 组件记忆化
期待稳定值的函数
你可能会遇到稳定值的重要性的第一个地方是React钩子useEffect,useCallback, 和useMemo 。所有这些函数都接受一个依赖关系的列表。我们期望这些钩子的行为与依赖列表中的值的变化相关联。如果这些值中有任何一个是不稳定的 (如果它们在每次渲染时都会改变),那么我们就会有不想要的行为。例如,如果你的依赖列表包括一个不稳定的值,那么由useCallback``/useMemo 返回的缓存值将永远不会被重复使用,而你的useEffect 函数将在每次渲染时被调用。
另一种思考这些钩子的方式是,它们给了我们另一种生成稳定值的方法。如果我们有一个稳定值的来源,我们可以使用useMemo 或useCallback 来生成一个新的 稳定值,或者我们可以在useEffect 内响应特定的状态变化。
识别期待稳定值的API可以很微妙地注意到。一个几乎每个React Native开发者都必须了解的这样一个令人惊讶的API是来自React Navigation的useFocusEffect 。你可能会认为这个函数的工作原理和它的名字一样,useEffect ,但正如React Navigation文档中非常温和地指出的那样。
"注意:为了避免过于频繁地运行效果,重要的是在将回调传递给useFocusEffect之前将其包裹在useCallback中,正如例子中所示。"
让我们来解开这个问题。在每个焦点事件中,我们将订阅与userId 有关的API事件。如果我们转到另一个屏幕,就会调用取消订阅函数(在useEffect 的上下文中又称清理函数 ,其行为类似)。那么,[userId] 的依赖性呢?它的行为就像一个先模糊后聚焦的事件,如果userId 的值发生变化,首先会调用取消订阅函数,然后立即调用聚焦效果(API.subscribe )。如果我们想让API.subscribe 完全被调用一次,我们可以将userId 包裹在useRef 中,并将其视为一个常量--但我们不会在这篇文章中讨论这个问题。
为了完整起见,不正确的代码是很容易写的。
我还想指出围绕useCallback 的一个常见的错误观点:**它并不节省内存!**传递给useCallback 的函数并没有神奇地被编译掉,它仍然被创建,就像钩子根本不存在一样,如果依赖关系没有改变,它就被忽略了。这与useMemo 略有不同,后者接受一个函数,只在需要新值的时候调用该函数(你注意到了吗?在这两种情况下,我们都有一个可能被忽略的函数--无论内存是多少)。
我在这里要特别详细地解释一下为什么useFocusEffect 的工作方式。useFocusEffect 没有办法知道所提供的函数是否相同 --每次这个组件渲染时,都会创建一个新的匿名函数。这个新函数关闭了它所需要的任何局部变量--这些变量的值与上次创建函数时的值相同吗?有可能吗?
所以useFocusEffect ,必须假设这个新函数的行为是 不同的,它所做的(在每次 渲染时)是调用清理 函数unsubscribe 。所以在每次渲染时,这个事件都会被取消订阅和重新订阅。这是不是坏了?没有!没有。不是坏了,它可以正常工作。但这并不是我们的初衷。
返回组件的内联函数
我保证其余的例子要简单得多。想象一下,你有一个组件,要么显示一些细节,要么显示更多 按钮。我将在这里使用一个返回组件的内联函数--这不是我最喜欢的模式,但你几乎肯定会遇到它,而且我想指出一个陷阱。在这段代码中,细节函数将根据showMore ,返回不同的JSX。陷阱在于我们如何 告诉React渲染JSX。
我们有两种方法来调用我们的函数,并将结果插入组件的JSX中。幸运的是,最简单的方法是处理这种情况的正确方法。
但我们也可以把函数当作一个组件 ,并把它渲染成JSX。这不小心就会有很糟糕的性能!
很微妙,对吗? 在正确的 版本中(我确实发现了美学上的错误,我们当然根本不需要将逻辑包裹在一个函数中 ),我们调用我们的助手,直接将生成的JSX插入我们的返回值中。这就是传递给React进行比较的内容。在性能糟糕的版本中,<details> 被当作一个组件,所以React会把它当作一个组件来比较。这意味着什么呢?
details 这个函数是不稳定的。所以在两次渲染之间,我们保证会有一个看起来像一个全新组件的函数。React对待这两个函数的方式与比较<Apple...Apple> 和<Orange...Orange> 相同。它销毁所有的子组件,创建一个新的组件树(在React Native中,这意味着创建本地视图对象,创建起来相当昂贵),并将整个东西挂载起来。在每次渲染时。
渲染details() ,同时还能避免第二种方法的性能缺陷的其他方法。
你可以将details 的返回值记忆化,但你可能无法获得你所期望的渲染性能。用useMemo 记忆组件和用React.memo, 记忆组件是不一样的,你仍然需要注意重新渲染,但React调和器仍然能够检测到变化。
或者更好的是,如果它看起来像一个组件,走起来 像一个组件......为什么不直接把它变成一个组件。 以后如果你需要进一步优化(使用React.memo 或其他什么),你可以使用久经考验的方法来分析你的组件并提高其性能。
有一条规则可供借鉴:**组件必须生活在全局空间中。**如果它们被创建在另一个组件的体内,对任何人都没有好处。这是为什么呢?因为如果你把函数Toggle({...}) 放在函数Overview() 的体内,你就会在每次渲染时创建一个新的函数实例,而React不能对新的组件是否与之前渲染时创建的组件有任何共同之处做出任何假设。
组件Memoization(但不要这么称呼它)
最后但并非最不重要的是,我们来谈谈组件记忆化。我之所以提出这个问题,是因为当我们问自己*"我是否应该把这个包起来* ? *onPress*用 *useCallback*?"我们真正要问的是"我是否要把这个函数传递给一个记忆化的组件?"
注意:顺便说一下,Memoized 在技术上 (调整眼镜)不是正确的术语。它们的缓存 大小为1,当任何道具发生变化时,缓存被拒绝。同样,对于useMemo 。如果你搜索 "react memoization",你会发现很多很多人声称这些函数 "memoize"。现在你可以自鸣得意地(或者,你知道,礼貌地)纠正所有这些人。在这个孤独的基座上加入我吧。
如果你把一个回调推送到一个用React.memo 包装的组件上,如果这个回调不稳定,你将得不到任何好处。这并不是说你可以用useCallback 来包装每一个事件处理程序。你应该谨慎行事。
所以,你已经拥有了它!关于稳定值的一些想法和意见,它们如何工作,为什么它们很重要。如果你是一个React开发者,我希望这能帮助你。