在React中,经常需要根据传入的数据计算出一些派生的属性。本文将探讨如何在React中实现这一功能,包括在渲染时计算属性、将计算提取到函数中、类组件中的计算以及如何使用memoization来优化性能。
渲染时计算属性
在React中,处理派生数据的“React方式”是在渲染方法(如果是无状态组件,则在函数体中)计算它。是的,就在渲染时。是的,每一次渲染。(一会儿会讨论性能问题)最简单的方法是这样做。记住,在React中,可以在单花括号内运行任意JS代码,React会渲染出该表达式的结果。
function UrlPath({ fullUrl }) {
return (
{new URL(fullUrl).pathname}
);
}
// 使用方式:
<UrlPath fullUrl="https://daveceddia.com/pure-react/" />
// 将渲染:
<div>/pure-react/</div>
如果计算很简单,就直接在渲染中进行。简单的操作不太可能导致性能问题,但如果注意到了减速,可以查看浏览器性能工具。Chrome、Firefox、Safari和Edge都有内置工具,通常在devtools的“Performance”标签下,可以让记录运行中的应用程序,并查看减速发生在哪里。
将计算提取到函数中
如果计算很复杂,可能想要将其从组件中提取出来,并将其放入一个函数中。这也会使其在其他组件中可重用。以下是一个过滤和排序产品列表以仅显示“新”产品并按价格排序的示例:
function newItemsCheapestFirst(items) {
return items
.filter(item => item.isNew)
.sort((a, b) => {
if (a.price < b.price) {
return -1;
} else if (a.price > b.price) {
return 1;
} else {
return 0;
}
});
}
function NewItemsList({ items }) {
return (
{newItemsCheapestFirst(items).map(item =>
<li key={item.id}>{item.name}, ${item.price}</li>
)}
);
}
这里的newItemsCheapestFirst
函数做了大部分工作。可以将其内联到组件中,但将其写成独立函数更易于阅读(和重用)。注意,计算函数不处理为每个项目创建<li>
元素。这是有意为之的,以保持“项目处理”与“项目渲染”分开。让React组件NewItemsList
处理渲染,而newItemsCheapestFirst
函数处理数据。
类组件中的计算
可以将上述示例适应到类组件中,将newItemsCheapestFirst
函数移到类中,如下所示:
class NewItemsList extends React.Component {
newItemsCheapestFirst() {
return this.props.items.filter(item => item.isNew).sort((a, b) => {
if (a.price < b.price) {
return -1;
} else if (a.price > b.price) {
return 1;
} else {
return 0;
}
});
}
render() {
return (
<ul>
{
this.newItemsCheapestFirst().map(item => (
<li key={item.id}>
{item.name}, ${item.price}
</li>
))
}
</ul>
);
}
}
甚至可以更进一步,将计算变成一个getter,这样访问它就像访问一个属性一样:
class NewItemsList extends React.Component {
get newItemsCheapestFirst() {
return this.props.items.filter(item => item.isNew).sort((a, b) => {
if (a.price < b.price) {
return -1;
} else if (a.price > b.price) {
return 1;
} else {
return 0;
}
});
}
render() {
return (
<ul>
{
this.newItemsCheapestFirst.map(item => (
<li key={item.id}>
{item.name}, ${item.price}
</li>
))
}
</ul>
);
}
}
就个人而言,可能会坚持使用“函数”方法,而不是使用getter。认为混合使用两者会导致混淆——应该是this.newItemsCheapestFirst
还是this.newItemsCheapestFirst()
?无论选择做什么,都要保持一致。
Memoize昂贵的计算
如果计算很昂贵——也许正在过滤数百个项目的列表或其他东西——那么通过memoization(但不是memoRizing)计算函数,可以获得很好的性能提升。
Memoization是一个花哨的词,意思是缓存。它说:“记住调用这个函数的结果,下次用相同的参数调用时,直接返回旧的结果,而不是重新计算它。”这个函数基本上是在记住答案。尽管如此,它仍然被称为“memoization”。没有“r”。
可以使用现有的memoize函数,比如Lodash中的一个,或者可以用不多的代码自己写一个。这里有一个例子,模仿Lodash中的一个:
function memoize(func) {
let cache = new Map();
const memoized = function (...args) {
let key = args[0];
if (cache.has(key)) {
return cache.get(key);
}
let result = func.apply(this, args);
cache.set(key, result);
return result;
};
return memoized;
}
function doSort(items) {
console.log('doing the sort');
return items.sort();
}
let memoizedSort = memoize(doSort);
let numbers = [1, 7, 4, 2, 4, 9, 28, 3];
memoizedSort(numbers);
memoizedSort(numbers);
memoizedSort(numbers);
看看控制台,注意它只打印了一次'doing the sort'!
现在对memoization的工作原理有了把握,并且手头有一个memoization函数(自己的或别人的),可以用它来包装昂贵函数调用。那可能看起来像这样:
function newItemsCheapestFirst(items) {
return items
.filter(item => item.isNew)
.sort((a, b) => {
if (a.price < b.price) {
return -1;
} else if (a.price > b.price) {
return 1;
} else {
return 0;
}
});
}
// Memoize the function...
memoizedCheapestItems = memoize(newItemsCheapestFirst);
function NewItemsList({ items }) {
return (
<ul>
{memoizedCheapestItems(items).map(item =>
<li key={item.id}>{item.name}, ${item.price}</li>
)}
</ul>
);
}
一个巨大的警告:确保不是每次都重新创建memoized函数,否则不会从中看到任何好处——它将每次都被调用。换句话说,如果调用memoize
每次都发生,那就不好了。不要这样做:
function NewItemsList({ items }) {
const memoizedExpensiveFunction = memoize(expensiveFunction);
return (
<ul>
{memoizedExpensiveFunction(items).map(item =>
<li key={item.id}>{item.name}, ${item.price}</li>
)}
</ul>
);
}