React 自定义 hooks 和 useMemo hooks
我有两个“昂贵”的函数,我想在我的 React 应用程序中记住它们,因为它们需要很长时间才能渲染。这些昂贵函数用于数组映射中。我想记住数组映射的每个结果,这样如果数组的一个元素发生变化,则只会重新计算该元素的昂贵函数。 (并且独立地记忆昂贵的功能,因为有时只需要重新计算一个。)我正在努力记忆并传递当前数组值。
这是没有记忆的工作演示:
import React, { useMemo, useState } from "react";
const input = ["apple", "banana", "cherry", "durian", "elderberry", "apple"];
export default function App() {
const output = input.map((msg, key) => createItem(msg, key));
return <div className="App">{output}</div>;
}
const createItem = (message, key) => { // expensive function 1
console.log("in createItem:", key, message);
return (
<div key={key}>
<strong>{"Message " + (1 + key)}: </strong>
{message} =>{" "}
<span
id={"preview-" + key}
dangerouslySetInnerHTML={{ __html: msgSub(message) }}
/>
</div>
);
};
const msgSub = message => { // expensive function 2
const messageSub = message.replace(/[a]/g, "A").replace(/[e]/g, "E");
console.log("in msgSub:", message, messageSub);
return messageSub;
};
(我没有在 SO 的编辑器上运行它,因此请在 codesandbox 上查看并运行它。)
这是我使用自定义钩子和 useMemo 的 一次尝试 钩子。
任何指导都将不胜感激!
并且说明如何在 SO 的编辑器中让反应工作可以获得加分!
我的第一步是改变
createItem
以便 Item 成为其唯一的功能组件。这意味着我可以记住
<Item/>
组件,以便它仅在 props 发生变化时才渲染,重要的是消息和键/索引(就像您之前渲染值一样)。
此 https://codesandbox.io/s/funny-chaplygin-pvfoj 的工作示例。
const Item = React.memo(({ message, index }) => {
console.log("Rendering:", index, message);
const calculatedMessage = msgSub(message);
return (
<div>
<strong>{"Message " + (1 + index)}: </strong>
{message} =>{" "}
<span
id={"preview-" + index}
dangerouslySetInnerHTML={{ __html: calculatedMessage }}
/>
</div>
);
});
const msgSub = message => {
// expensive function 2
const messageSub = message.replace(/[a]/g, "A").replace(/[e]/g, "E");
console.log("in msgSub:", message, messageSub);
return messageSub;
};
您可以看到,在初始渲染时,它会渲染所有带有
message
的项目,尤其是
apple
两次。
如果两个组件彼此独立渲染,并且碰巧使用相同的 props,则这两个组件都将被渲染。 React.memo 不会保存组件渲染。
它必须渲染 Item 组件两次
<Item message="apple" />
,一次是索引 0 处的
apple
,另一次是索引 5 处的
apple
。
您会注意到我在 App 中放置了一个按钮,单击该按钮时会更改数组的内容。
稍微更改原始数组,我将
carrot
放在原始数组的索引 4 处,并在更新它时将其移动到新数组的索引 2 处。
const [stringArray, setStringArray] = React.useState([
"apple",
"banana",
"cherry",
"durian",
"carrot",
"apple" // duplicate Apple
]);
const changeArray = () =>
setStringArray([
"apple",
"banana",
"carrot", // durian removed, carrot moved from index 4 to index 2
"dates", // new item
"elderberry", // new item
"apple"
]);
如果查看控制台,您会看到在第一次渲染时您看到
in msgSub: carrot cArrot
但是当我们更新数组时,
in msgSub: carrot cArrot
再次被调用。这是因为
<Item />
上的键被强制重新渲染。这是正确的,因为我们的键基于索引,而 carrot 改变了位置。但是,您说
msgSub
是一个昂贵的函数...
本地记忆化
我注意到您的数组中有两个
apple
。
const input = ["apple", "banana", "cherry", "durian", "elderberry", "apple"];
我觉得您想要记忆计算,这样如果
apple
之前已经计算过,就不会再次计算
apple
。
我们可以将计算出的值存储在我们自己的记忆状态中,这样我们就可以查找消息值,看看我们是否之前已经计算过它。
const [localMemoization, setLocalMemoization] = useState({});
我们希望确保在
stringArray
发生变化时更新这个 localMemoization。
React.useEffect(() => {
setLocalMemoization(prevState =>
stringArray.reduce((store, value) => {
const calculateValue =
prevState[value] ?? store[value] ?? msgSub(value);
return store[value] ? store : { ...store, [value]: calculateValue };
})
);
}, [stringArray]);
这一行
const calculateValue = prevState[value] ?? store[value] ?? msgSub(value);
- 检查先前的状态以查看它是否具有先前的值(对于胡萝卜将第一个数组移动到第二个数组的情况)。
- 检查当前商店以查看它是否具有值(对于第二个苹果的情况)。
- 最后,如果没有任何东西看到它,我们使用昂贵的函数来第一次计算值。
使用与上一次渲染相同的逻辑,我们现在查找计算出的值并将其传递给
<Item>
,以便如果 App 再次渲染,React.memo 将阻止该组件的重新渲染。
const output = stringArray.map((msg, key) => {
const expensiveMessage = localMemoization[msg];
return (
<Item
key={key}
message={msg}
calculatedValue={expensiveMessage}
index={key}
/>
);
});
工作示例在这里 https://codesandbox.io/s/attempt-with-custom-hooks-and-usememo-y3lm5?file=/src/App.js:759-1007
从这里,在控制台中,我们可以看到在第一次渲染时,
apple
只计算了一次。
当数组发生变化时,我们不会再次计算
carrot
,而只计算更改的项目
dates
和
elderberry
。
使 item 成为纯组件:
const id = ((id) => () => id++)(1); //IIFE creating id
//pure component
const Item = React.memo(function Item({ increase, item }) {
console.log('rendering id:', item.id);
return (
<ul>
<button onClick={() => increase(item.id)}>
{item.count}
</button>
</ul>
);
});
const App = () => {
const [items, setItems] = React.useState([]);
//use callback, increase only created on app mount
const increase = React.useCallback(
(id) =>
setItems((items) =>
//use state setter callback
//no dependencies for useCallback and no stale
//closures
items.map((item) =>
item.id === id
? { ...item, count: item.count + 1 }
: item
)
),
[] //no dependencies
);
return (
<div>
<div>
<button
onClick={() =>
setItems((items) => [
{ id: id(), count: 0 },
...items,
])
}
>
add counter
</button>
</div>
<ul>
{items.map((item) => (
<Item
key={item.id}
item={item}
increase={increase}
/>
))}
</ul>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>