ne3*_*3zy 2 javascript dependencies reactjs react-hooks usecallback
据我了解,您使用 useCallback 来防止重新渲染,因此我一直在每个函数中使用它,而我的蜘蛛感觉告诉我这听起来已经很糟糕了。
但故事并没有就此结束,因为我一直在到处使用它,所以我现在将依赖项传递给我的所有子组件,他们不需要担心,如下例所示:
编辑//沙盒: https://codesandbox.io/s/bold-noether-0wdnp?file =/src/App.js
父组件(需要 colorButtons 和 currentColor)
const ColorPicker = ({onChange}) => {
const [currentColor, setCurrentColor] = useState({r: 255, g:0, b: 0})
const [colorButtons, setColorButtons] = useState({0: null})
const handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
return <div className="color-picker">
<RgbColorPicker color={currentColor} onChange={setCurrentColor} />
<div className="color-buttons">
{
Object.entries(colorButtons).map(button => <ColorButton
//...
currentColor={currentColor}
onClick={handleColorButtons}
colorButtons={colorButtons}
/>)
}
</div>
</div>
}
Run Code Online (Sandbox Code Playgroud)
第一个子级(需要 style 和 currentColor,但从其父级免费获取 colorButtons)
const ColorButton = ({currentColor, onClick, id, colorButtons}) => {
const [style, setStyle] = useState({})
const handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
return <ToggleButton
//...
onClick={handleClick}
style={style}
dependency1={style}
dependency2={currentColor}
dependency3={colorButtons}
>
</ToggleButton>
}
Run Code Online (Sandbox Code Playgroud)
第二个孩子(只需要自己的变量,但获得整个包)
const ToggleButton = ({children, className, onClick, style, data, id, onRef, ...dependencies}) => {
const [isToggled, setIsToggled] = useState(false)
const [buttonStyle, setButtonStyle] = useState(style)
const handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
return <button
className={className || "toggle-button"}
onClick={handleClick}
style={buttonStyle || {}}
ref={onRef}
>
{children}
</button>
}
Run Code Online (Sandbox Code Playgroud)
我是否在做反模式?如果是,它是什么以及如何修复它?感谢您的帮助!
useCallbackuseCallback是一个可以在函数式 React 组件中使用的钩子。功能组件是一个返回 React 组件并在每次渲染上运行的函数,这意味着在其主体中定义的所有内容每次都会获得新的引用标识。一个例外可以通过 React hook 来完成,它可以在功能组件内部使用来互连不同的渲染并维护状态。这意味着,如果您使用 ref 保存对功能组件中定义的常规函数的引用,然后将其与稍后渲染中的相同函数进行比较,它们将不相同(函数在渲染之间更改引用标识):
// Render 1
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = () => {}
const fn = useRef(null)
console.log(fn.current === fnInBody) // false due to different identity
fn.current = fnInBody
...
Run Code Online (Sandbox Code Playgroud)
根据文档,useCallback返回“回调的记忆版本,仅在依赖项之一发生更改时才会更改”,这在“将回调传递给依赖引用相等性的优化子组件以防止不必要的渲染时”非常有用。
总而言之,useCallback只要依赖关系不改变,就会返回一个保持其引用标识(例如被记忆)的函数。返回的函数包含一个带有所使用的依赖项的闭包,因此一旦依赖项发生变化就必须更新。
这导致了上一个示例的更新版本
// Render 1
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // false since fn.current is null
fn.current = fnInBody
...
// Render 2
...
const fnInBody = useCallback(() => {}, [])
const fn = useRef(null)
console.log(fn.current === fnInBody) // true
fn.current = fnInBody
...
Run Code Online (Sandbox Code Playgroud)
记住上面的描述,让我们看看您对useCallback.
ColorPickerconst handleColorButtons = useCallback((isToggled, id) => {
/* code that uses colorButtons and currentColor */
}, [colorButtons, currentColor])
Run Code Online (Sandbox Code Playgroud)
这个函数每次colorButtons或者currentColor变化都会得到一个新的标识。ColorPicker当这两者之一被设置或者它的 proponChange改变时,它本身会重新渲染。当或改变时,双方handleColorButtons和孩子都应该更新。孩子们唯一从使用中受益的时候就是只有改变的时候。鉴于这是一个轻量级组件,并且重新渲染主要是由于和的更改,因此此处的使用似乎是多余的。currentColorcolorButtonsuseCallback onChangeColorButtonColorPickercurrentColorcolorButtonsuseCallback
ColorButtonconst handleClick = useCallback((isToggled) => {
/* code that uses setStyle and currentColor */
}, [style, currentColor, colorButtons])
Run Code Online (Sandbox Code Playgroud)
这种情况与第一种情况类似。ColorButton当currentColor、onClick、id或colorButtons更改时重新渲染,并且子级在handleClick、style、colorButtons或currentColor更改时重新渲染。就位后useCallback,道具id可能onClick会在不重新渲染子项的情况下发生变化(至少根据上面的可见代码),所有其他重新渲染都ColorButton将导致其子项重新渲染。同样,孩子ToggleButton是轻量级的,并且id不太onClick可能比任何其他道具更频繁地改变,因此useCallback这里的使用似乎也是多余的。
ToggleButtonconst handleClick = useCallback(() => {
/* code that uses isToggled, data, id and setButtonStyle */
}, [isToggled, data, id, ...Object.values(dependencies)])
Run Code Online (Sandbox Code Playgroud)
这种情况很复杂,有很多依赖关系,但从我看来,无论哪种方式,大多数组件道具将导致一个“新版本”,handleClick并且子组件是轻量级组件,使用的论点useCallback似乎很弱。
useCallback呢?正如文档所说,当您需要一个函数在渲染之间具有引用相等性时,请在非常具体的情况下使用它......
您有一个包含子组件子集的组件,这些子组件的重新渲染成本很高,并且重新渲染的频率应该比父组件少得多,但由于函数 prop 在父组件重新渲染时更改身份,因此会重新渲染。对我来说,这个用例也标志着糟糕的设计,我会尝试将父组件划分为更小的组件,但我知道什么,也许这并不总是可能的。
您在功能组件的主体中有一个函数,该函数在另一个钩子(列为依赖项)中使用,由于每当组件重新渲染时函数都会更改身份,因此每次都会触发该函数。通常,您可以通过忽略 lint 规则来从依赖项数组中省略此类函数,即使这不是书中规定的。其他建议是将这样的函数放置在组件主体之外或使用它的钩子内部,但可能存在这些情况都无法按预期运行的情况。
很高兴知道与此相关的是......
位于功能组件外部的函数在渲染之间始终具有引用相等性。
返回的设置器useState在渲染之间始终具有引用相等性。
我在评论中说过,useCallback当有函数在经常重新渲染的组件中进行昂贵的计算时,您可以使用它,但我有点偏离了。假设您有一个函数,它基于某些 prop 进行大量计算,而这些 prop 的更改频率低于组件重新渲染的频率。然后你可以使用useCallback并运行其中的一个函数,该函数返回一个带有带有一些计算值的闭包的函数
const fn = useCallback(
(
() => {
const a = ... // heavy calculation based on prop c
const b = ... // heavy calculation based on prop c
return () => { console.log(a + b) }
}
)()
, [c])
...
/* fn is used for something, either as a prop OR for something else */
Run Code Online (Sandbox Code Playgroud)
这将有效地避免每次组件重新渲染而不a进行更改的计算,但更直接的方法是bc
const a = useMemo(() => /* calculate and return a */, [c])
const b = useMemo(() => /* calculate and return b */, [c])
const fn = () => console.log(a + b)
Run Code Online (Sandbox Code Playgroud)
所以这里使用useCallback只会让事情变得更复杂。
理解编程中更复杂的概念并能够使用它们固然很好,但其中的一个优点是知道何时使用它们。添加代码,尤其是涉及复杂概念的代码,代价是可读性降低,并且代码更难以通过许多相互作用的不同机制进行调试。因此,请确保您了解这些钩子,但如果可以的话,尽量不要使用它们。特别是useCallback,useMemo和React.memo(不是钩子,而是类似的优化),在我看来,应该只在绝对需要时才引入。useRef有其自己的用例,但当您可以在没有它的情况下解决问题时,也不应该引入它。
沙盒上的工作做得很好!总是让代码推理变得更容易。我冒昧地分叉了你的沙箱并对其进行了一些重构:沙箱链接。如果您愿意,您可以自己研究这些变化。总结如下:
您知道并使用是件好事useRef,useCallback但我能够删除所有用途,使代码更容易理解(不仅删除这些用途,还删除使用它们的上下文)。
尝试使用React来简化事情。我知道这不是一个实际操作的建议,但是你越深入 React,你就越会意识到你可以与 React 合作做事情,或者你可以按照自己的方式做事情。两者都会起作用,但后者会给你和其他人带来更多的麻烦。
尝试隔离组件的范围;仅将必要的数据委托给子组件,并不断询问您将状态保存在哪里。早些时候,所有三个组件中都有单击处理程序,并且流程非常复杂,我什至懒得完全理解它。在我的版本中,只有一个点击处理程序ColorPicker被委托下来。这些按钮不必知道单击它们时会发生什么,只要单击处理程序负责处理即可。闭包和将函数作为参数传递的能力是 React 和 Javascript 的强大优势。
键在 React 中很重要,很高兴看到您使用它们。通常,密钥应对应于唯一标识特定项目的内容。在这里使用很好,${r}_${g}_${b}但是这样我们就只能在按钮数组中获得每种颜色的一个样本。这是一个自然的限制,但如果我们不想要它,分配键的唯一方法是分配一个唯一的标识符,您就是这么做的。我更喜欢使用,Date.now()但有些人可能会出于某种原因建议不要使用。如果您不想使用引用,也可以在功能组件外部使用全局变量。
尝试以函数式(不可变)方式做事,而不是“旧的”Javascript 方式。例如,当添加到数组时,使用[...oldArray, newValue];当分配给对象时,使用{...oldObject, newKey: newValue }。
还有更多的事情要说,但我认为你最好研究重构的版本,如果你有什么疑问可以告诉我。