本文翻译自 原文地址
翻译文章的目的纯粹是处于锻炼自己英文阅读的能力,如有翻译出现歧义的地方欢迎讨论,侵删
我们知道,在 React 中搭配 Hooks 使用函数组件进行开发使我们的开发工作变得更加轻松。然而,组件函数自身拥有复杂性和陷阱。因此,有时候我们很难写出可读的、可优化复用的函数组件。今天,我们将会通过5个简单例子来帮助我们做到这一点。
缓存数据(Memoize Data)
让我们看一下下面这个 SortedListView React 组件
import React from 'react';
function SortedListView ({ title, items, comparisonFunc }) {
const sortedItems = [...items];
sortedItems.sort(comparisonFunc);
return (
<div>
<h1> {title} </h1>
<ul>
{sortedItems.map(item => <li> {item} </li>)}
</ul>
</div>
);
}
这个组件接收一个 items 数组,排序后展示。然而,如果数组太长或者排序方法太复杂会让排序消耗大量时间。最终这可能成为一个瓶颈,因为即使 items 数组或者 comparisonFunc 没有改变,但是一些其他的 prop 或者 state 更改了,组件会在重新渲染时进行重新排序。
我们可以通过记录排序方法并且只在 items 变化时重新排序来提高 CPU 效率。这可以使用 useMemo 这个 Hook 来轻松完成:
import React, { useMemo } from 'react';
function SortedListView ({ title, items, comparisonFunc }) {
const sortedItems = useMemo(() => {
const sortedItems = [...items];
sortedItems.sort(comparisonFunc);
return sortedItems;
}, [items, comparisonFunc]);
return (
<div>
<h1> {title} </h1>
<ul>
{sortedItems.map(item => <li> {item} </li>)}
</ul>
</div>
);
}
因此,我们可以使用 useMemo 来通过部分内存将代价高的操作进行存储或缓存。
缓存回调函数(Memoize Callback Functions)
就像缓存数据一样,我们也可以缓存一个组件传递给它渲染的其他组件的回调函数。这样做的好处是能够防止某些情况下无意义的重新渲染。为了说明这种情况,我们来看一下 SortController 组件如何使用 SortedListView 组件(原文提供的 jsfiddle 地址,需要科学上网):
const { useMemo, useState } = React
function SortController ({ items }) {
const [isAscending, setIsAscending] = useState(true);
const [title, setTitle] = useState('');
const ascendingFn = (a, b) => a < b ? -1 : (b > a ? 1 : 0);
const descendingFn = (a, b) => b < a ? -1 : (a > b ? 1 : 0);
const comparisonFunc = isAscending ? ascendingFn : descendingFn;
return (
<div>
<input
placeholder='Enter Title'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => setIsAscending(true)}>
Sort Ascending
</button>
<button onClick={() => setIsAscending(false)}>
Sort Descending
</button>
<SortedListView
title={title}
items={items}
comparisonFunc={comparisonFunc}
/>
</div>
);
}
function SortedListView ({ title, items, comparisonFunc }) {
const sortedItems = useMemo(() => {
const sortedItems = [...items];
sortedItems.sort(comparisonFunc);
return sortedItems;
}, [items, comparisonFunc]);
return (
<div>
<h1> {title} </h1>
<ul>
{sortedItems.map(item => <li> {item} </li>)}
</ul>
</div>
);
}
const items = [5,6,2,100,4,23,12,34]
ReactDOM.render(
<SortController items={items} />,
document.querySelector('#root')
)
上面这个例子,如果你进入“结果”页并输入一个新的标题,它会引起 SortController 重新渲染。结果,ascendingFn 和 descendingFn 将被重建。这将会引起 comparisonFunc 改变,由于 SortedListView中的 useMemo 依赖 comparisonFunc,即使 comparisonFunc 逻辑上没有发生变化,它也会重新排序。
我们可以通过使用 useCallback 包装 ascendingFn 和 descendingFn 解决这个问题。这个 Hook 用来缓存回调函数。记住,在这个地方我们不需要在 useCallback 的依赖数组中传递任何东西,因为它们不依赖组件中的任何东西。(原文提供的 jsfiddle 地址,需要科学上网)
const { useMemo, useState, useCallback } = React
function SortController ({ items }) {
const [isAscending, setIsAscending] = useState(true);
const [title, setTitle] = useState('');
const ascendingFn = useCallback(
(a, b) => a < b ? -1 : (b > a ? 1 : 0),
[]
);
const descendingFn = useCallback(
(a, b) => b < a ? -1 : (a > b ? 1 : 0),
[]
);
const comparisonFunc = isAscending ? ascendingFn : descendingFn;
return (
<div>
<input
placeholder='Enter Title'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => setIsAscending(true)}>
Sort Ascending
</button>
<button onClick={() => setIsAscending(false)}>
Sort Descending
</button>
<SortedListView
title={title}
items={items}
comparisonFunc={comparisonFunc}
/>
</div>
);
}
function SortedListView ({ title, items, comparisonFunc }) {
const sortedItems = useMemo(() => {
const sortedItems = [...items];
sortedItems.sort(comparisonFunc);
return sortedItems;
}, [items, comparisonFunc]);
return (
<div>
<h1> {title} </h1>
<ul>
{sortedItems.map(item => <li> {item} </li>)}
</ul>
</div>
);
}
const items = [5,6,2,100,4,23,12,34]
ReactDOM.render(
<SortController items={items} />,
document.querySelector('#root')
)
解耦不依赖组件的功能(Decouple Functions That Don’t Rely on the Component)
我们可以进行的另一个改进是讲上面函数中的 ascendingFn 和 descendingFn 移动到 SortController 外面。因为这两个函数不依赖于内部的任何条件。因此,这里无需在组件内部声明它们,如果我们这样做,这个组件的可读性将会更高。并且我们不需要再使用 useCallback,因为函数不会在重新渲染时进行重建。(原文提供的 jsfiddle 地址,需要科学上网)
const { useMemo, useState, useCallback } = React
function SortController ({ items }) {
const [isAscending, setIsAscending] = useState(true);
const [title, setTitle] = useState('');
const comparisonFunc = isAscending ? ascendingFn : descendingFn;
return (
<div>
<input
placeholder='Enter Title'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => setIsAscending(true)}>
Sort Ascending
</button>
<button onClick={() => setIsAscending(false)}>
Sort Descending
</button>
<SortedListView
title={title}
items={items}
comparisonFunc={comparisonFunc}
/>
</div>
);
}
function ascendingFn (a, b) {
return a < b ? -1 : (b > a ? 1 : 0);
}
function descendingFn (a, b) {
return b < a ? -1 : (a > b ? 1 : 0);
}
function SortedListView ({ title, items, comparisonFunc }) {
const sortedItems = useMemo(() => {
const sortedItems = [...items];
sortedItems.sort(comparisonFunc);
return sortedItems;
}, [items, comparisonFunc]);
return (
<div>
<h1> {title} </h1>
<ul>
{sortedItems.map(item => <li> {item} </li>)}
</ul>
</div>
);
}
const items = [5,6,2,100,4,23,12,34]
ReactDOM.render(
<SortController items={items} />,
document.querySelector('#root')
)
我们还可以将 sort 这样的实用函数保存在另一个文件中,并将其导入进行使用。
创建子组件(Create Subcomponents)
创建子组件是编写可优化、可读性高的 React 代码的方式---class 组件也是如此。子组件将代码库拆分成更小的、易理解的、可复用的块。这也使得 React 更容易优化渲染。因此,把大组件拆分成小组件通常是一个好的办法。
创建和复用自定义 Hooks(Create and Reuse Custom Hooks)
就像组件一样,我们同样可以创建自定义可复用的 Hooks。因为代码库被拆分成了更小、可复用的块,这使得代码可读性更高。在我们的例子中,我们可以把排序逻辑放进一个名叫 useSorted 的自定义 Hook 中。(原文提供的 jsfiddle 地址,需要科学上网)
const { useMemo, useState, useCallback } = React
function SortedListView ({ title, items, comparisonFunc }) {
const sortedItems = useSorted(items, comparisonFunc)
return (
<div>
<h1> {title} </h1>
<ul>
{sortedItems.map(item => <li> {item} </li>)}
</ul>
</div>
);
}
function useSorted (items, comparisonFunc) {
return useMemo(() => {
const sortedItems = [...items];
sortedItems.sort(comparisonFunc);
return sortedItems;
}, [items, comparisonFunc]);
}
function SortController ({ items }) {
const [isAscending, setIsAscending] = useState(true);
const [title, setTitle] = useState('');
const comparisonFunc = isAscending ? ascendingFn : descendingFn;
return (
<div>
<input
placeholder='Enter Title'
value={title}
onChange={e => setTitle(e.target.value)}
/>
<button onClick={() => setIsAscending(true)}>
Sort Ascending
</button>
<button onClick={() => setIsAscending(false)}>
Sort Descending
</button>
<SortedListView
title={title}
items={items}
comparisonFunc={comparisonFunc}
/>
</div>
);
}
function ascendingFn (a, b) {
return a < b ? -1 : (b > a ? 1 : 0);
}
function descendingFn (a, b) {
return b < a ? -1 : (a > b ? 1 : 0);
}
const items = [5,6,2,100,4,23,12,34]
ReactDOM.render(
<SortController items={items} />,
document.querySelector('#root')
)
结论(Conclusion)
这些是我们可以使用的五个简单技巧,以便在React中编写更具可读性和优化的函数组件。 随意分享你自己的技巧,以编写更好的函数组件。