2

这是我之前提出的问题的更简洁版本。希望它得到更好的解释和更容易理解。

这是一个小应用程序,它有 3 个需要数字的输入(请忽略您也可以输入非数字,这不是重点)。它计算所有显示数字的总和。如果您将其中一个输入更改为另一个数字,则总和将更新。

应用捕获

这是它的代码:

import { useCallback, useEffect, useState } from 'react';

function App() {

  const [items, setItems] = useState([
    { index: 0, value: "1" },
    { index: 1, value: "2" },
    { index: 2, value: "3" },
  ]);

  const callback = useCallback((item) => {
    let newItems = [...items];
    newItems[item.index] = item;
    setItems(newItems);
  }, [items]);

  return (
    <div>
      <SumItems items={items} />
      <ul>
        {items.map((item) =>
          <ListItem key={item.index} item={item} callback={callback} />
        )}
      </ul>
    </div>
  );
}


function ListItem(props) {

  const [item, setItem] = useState(props.item);

  useEffect(() => {
    console.log("ListItem ", item.index, " mounting");
  })

  useEffect(() => {
    return () => console.log("ListItem ", item.index, " unmounting");
  });

  useEffect(() => {
    console.log("ListItem ", item.index, " updated");
  }, [item]);

  const onInputChange = (event) => {
    const newItem = { ...item, value: event.target.value };
    setItem(newItem);
    props.callback(newItem);
  }

  return (
    <div>
      <input type="text" value={item.value} onChange={onInputChange} />
    </div>);
};

function SumItems(props) {
  return (
    <div>Sum : {props.items.reduce((total, item) => total + parseInt(item.value), 0)}</div>
  )

}

export default App;

这是启动时的控制台输出,在将第二个输入 2 更改为 4 之后:

ListItem  0  mounting App.js:35
ListItem  0  updated App.js:43
ListItem  1  mounting App.js:35
ListItem  1  updated App.js:43
ListItem  2  mounting App.js:35
ListItem  2  updated App.js:43
ListItem  0  unmounting react_devtools_backend.js:4049:25
ListItem  1  unmounting react_devtools_backend.js:4049:25
ListItem  2  unmounting react_devtools_backend.js:4049:25
ListItem  0  mounting react_devtools_backend.js:4049:25
ListItem  1  mounting react_devtools_backend.js:4049:25
ListItem  1  updated react_devtools_backend.js:4049:25
ListItem  2  mounting react_devtools_backend.js:4049:25

如您所见,当更新单个输入时,所有子项都不会重新渲染,它们首先被卸载,然后重新安装。太浪费了,所有的输入都已经是正确的状态,只需要更新总和。想象一下有数百个这样的输入。

如果只是重新渲染的问题,我可以看看 memoization。但这行不通,因为callback正是因为items变化而更新。不,我的问题是关于所有孩子的下马。

问题 1:可以避免卸载吗?

如果我相信Kent C. Dodds 的这篇文章,答案是否定的(强调我的):

React 的 key prop 让你能够控制组件实例。每次 React 渲染你的组件时,它都会调用你的函数来检索它用来更新 DOM 的新 React 元素。如果您返回相同的元素类型,它会保留这些组件/DOM 节点,即使所有 * 道具都已更改。

(...)

唯一的例外是 key prop。这允许您返回完全相同的元素类型,但强制 React 卸载前一个实例,并安装一个新实例。这意味着当时组件中存在的所有状态都将被完全删除,并且该组件已“重新初始化”以用于所有意图和目的。

问题 2:如果这是真的,那么我应该考虑什么设计来避免因为每个输入组件中发生异步处理而在我的真实应用程序中看起来不必要并导致问题?

4

2 回答 2

2

如您所见,当更新单个输入时,所有子项都不会重新渲染,它们首先被卸载,然后重新安装。太浪费了,所有的输入都已经是正确的状态,只需要更新总和。想象一下有数百个这样的输入。

不,您从 中看到的日志useEffect不代表组件安装/卸载。您可以检查 DOM 并验证即使所有三个组件都重新呈现,也只有一个输入被更新。

如果只是重新渲染的问题,我可以看看 memoization。但这不起作用,因为回调正是因为项目发生变化而更新。不,我的问题是关于所有孩子的下马。

这是您将使用功能状态更新来访问先前状态并返回新状态的地方。

const callback = useCallback((item) => {
    setItems((prevItems) =>
      Object.assign([...prevItems], { [item.index]: item })
    );
}, []);

现在,您可以使用React.memo回调不会改变。这是更新的演示:

编辑鼓舞人心的 bash-utk30

如您所见,当其中一个被更改时,只记录相应的输入日志,而不是全部三个。

于 2021-09-20T09:15:21.190 回答
1

首先让我们澄清一些术语:

  • “重新挂载”是 React 删除组件的内部表示,即子项(“隐藏的 DOM”)和状态。重新安装也是重新渲染,因为清理了效果并渲染了新安装的组件
  • “重新渲染”是当 Reactrender再次调用方法或功能组件时,函数本身,将返回的值与存储的子项进行比较,并根据前一次渲染的结果更新子项

您观察到的不是“重新安装”,而是“重新渲染”,因为useEffect(fn)调用在每次重新渲染时传递的函数。要登录卸载,请使用useEffect(fn, []). 当您正确使用 key 属性时,不会重新安装组件,只是重新渲染。这也可以在 App 中轻松观察到:输入没有被重置(状态保持不变)。

现在,如果道具没有改变,您要防止的是重新渲染。这可以通过将组件包装在 a 中来实现React.memo

const ListItem = React.memo(function ListItem() {
  // ...
}); 

请注意,通常重新渲染和区分子级通常“足够快”,并且通过使用React.memo,如果道具更改但组件未更新(由于areEqual第二个参数不正确),您可以引入新的错误。因此React.memo,如果您遇到性能问题,请谨慎使用。

于 2021-09-20T09:20:29.450 回答