上回说道在 React 應用中列表組件應該去優化,今天複雜組件應該怎麼寫。Jotai 和 Zustand 咕咕咕了,下次再說。
寫過大廠屎山的大夥應該都經歷過,一個組件能有上百甚至上千行都是常事。一個組件內部嵌套一個組件也是常事。簡單總結了下三不要:
- 不要在 Component 內部定義其他 Component
- 不要用 render 的方式渲染 ReactNode, eg.
render(data1, data2, data3) => ReactNode
- 不用 useCallback 定義一個組件
啥意思,簡單列舉一下上面的錯誤寫法,大家千萬不要學:
// ❌ 在組件內部定義其他組件
function ParentComponent(props) {
function ChildComponent() {
return <div>我是子組件</div>
}
return (
<div>
<ChildComponent />
</div>
)
}
// ❌ 用 render 的方式渲染 ReactNode:
function MyComponent({ data1, data2, data3 }) {
const render = (data1, data2, data3) => {
return <div>{data1}{data2}{data3}</div>
}
return render(data1, data2, data3);
}
// ❌ 用 useCallback 定義一個組件
import React, { useCallback } from 'react';
function ParentComponent(props) {
const ChildComponent = useCallback(() => {
return <div>我是子組件</div>
}, []);
return (
<div>
<ChildComponent />
</div>
)
}
以上只是非常簡單的例子,這樣的錯誤肯定大家都不會犯,因為這個組件很簡單,很好拆,怎麼會寫出這麼新手的代碼呢。在實際業務場景中,組件複雜度比這個大得多,有的時候好像沒有辦法去抽離一些邏輯就只能寫出這種新手代碼了,導致所有的狀態全在組件頂層,組件中的組件也會因為頂層組件的 re-render 一直重建,性能是非常低下的。
消除組件內部定義的其他組件#
我們定義一個稍微複雜一些的,數據耦合在一起的好像不得不使用組件中定位組件的一個業務組件,然後對它進行優化。
假設我們有一個OrderForm
組件,它需要根據傳入的訂單信息動態生成不同的OrderItem
組件。這個時候,你可能會想在OrderForm
中定義OrderItem
組件,如下:
錯誤寫法:
function OrderForm({ orders }) {
const [myName, setMyName] = useState('Foo')
function OrderItem({ order }) {
return (
<li>
<h2>{order.name}</h2>
<p>數量: {order.quantity}</p>
<p>單價: {order.price}</p>
<p>{myName}</p>
</li>
)
}
// 或者下面的形式
const OrderItem = React.useCallback(({ order }) => {
return (
<li>
<h2>{order.name}</h2>
<p>數量: {order.quantity}</p>
<p>單價: {order.price}</p>
<p>{myName}</p>
</li>
)
}, [order, myName])
return (
<ul>
{orders.map(order =>
<OrderItem key={order.id} order={order} />
)}
</ul>
);
}
但是這樣做,每次OrderForm
渲染時,OrderItem
都會被重建,也就意味著這個列表遍歷下的所有OrderItem
都在 re-mount 而不是 re-render。在 React 中,創建一個新的組件實例(即組件的首次渲染)通常比更新一個現有的組件實例(即組件的 re-render)要花費更多的時間。
正確的做法是,將OrderItem
組件定義在OrderForm
外面,通過 props 傳遞數據:
function OrderItem({ order, myName }) {
return (
<li>
<h2>{order.name}</h2>
<p>數量: {order.quantity}</p>
<p>單價: {order.price}</p>
<p>{myName}</p>
</li>
)
}
function OrderForm({ orders }) {
return (
<ul>
{orders.map(order =>
<OrderItem key={order.id} order={order} myName={myName}/>
)}
</ul>
);
}
這樣,不管OrderForm
如何渲染,OrderItem
都只會被定義一次,而且它可以根據 props 的改變來決定是否需要重新渲染,從而提高性能。
去除 render 方式傳參的組件#
有時會看到一些組件是這樣定義插槽的。比如下面的 Table 的組件。
type Data = (string | number)[][];
type ColumnRenderer = (data: string | number, rowIndex: number, columnIndex: number) => React.ReactNode;
interface TableProps {
data: Data;
columnRenderers: ColumnRenderer[];
}
const Table: React.FC<TableProps> = ({ data, columnRenderers }) => {
return (
<table>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columnRenderers.map((render, columnIndex) => (
<td key={columnIndex}>
{render(row[columnIndex], rowIndex, columnIndex)}
</td>
))}
</tr>
))}
</tbody>
</table>
);
};
上面的寫法也不是很好,首先是 render 是一個函數返回的一個 ReactNode,所以你在用這個組件的時候,傳入的 render 是不能使用 React Hooks 的。例如:
<Table data={[]} columnRenderers={[
(data, index) => {
useState() // ❌ 不能使用
return <div>{data.id}</div>
}
]}
不僅不能使用 Hooks,而且這樣的寫法會導致 Table 組件 re-render 後,下面的所有列的 render 全部重建。因為 render 不是一個組件而是一个 ReactNode,不能用作性能優化。
上述的 Table 可以改寫成:
type Data = (string | number)[][];
interface TableColumnProps {
data: string | number;
render: (data: string | number) => React.ReactNode;
}
interface TableProps {
data: Data;
columnComponents: Array<React.FC<TableColumnProps>>;
}
const Table: React.FC<TableProps> = ({ data, columnComponents }) => {
return (
<table>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{columnComponents.map((Component, columnIndex) => (
<Component key={columnIndex} data={row[columnIndex]} />
))}
</tr>
))}
</tbody>
</table>
);
};
這樣就可以在定義列組件時使用 Hooks 了並且列組件只受到 data
的影響(前提需要給列組件套上 memo),而且也只需要把原本的 (data1, data2) =>
改寫成 ({ data1, data2 })
就行了。
如:
<Table data={[]} columnComponents={[
ColumnComponent1
]}
const ColumnComponent1 = memo(({ data, index }) => {
return <div>{data.id}</div>
})
今天先說到這。
上述部分代碼和文字由 GPT-4 編寫。
此文由 Mix Space 同步更新至 xLog 原始鏈接為 https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-2