在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>
    );
}