Note-52055-270

Token ID: 3

ERC-721 1 Transfers

Metadata

{
  "title": "React 应用中性能优化的经验(二)",
  "tags": [
    "post",
    "react"
  ],
  "sources": [
    "xlog"
  ],
  "external_urls": [
    "https://innei-4525.xlog.app/posts/experience-in-performance-optimization-in-react-applications-2"
  ],
  "date_published": "2023-06-07T14:25:14.773Z",
  "content": "上回说道在 React 应用中列表组件应该去优化,今天复杂组件应该怎么写。Jotai 和 Zustand 咕咕咕了,下次再说。\n\n写过大厂屎山的大伙应该都经历过,一个组件能有上百甚至上千行都是常事。一个组件内部嵌套一个组件也是常事。简单总结了下三不要:\n\n- 不要在 Component 内部定义其他 Component\n- 不要用 render 的方式渲染 ReactNode, eg. `render(data1, data2, data3) => ReactNode`\n- 不用 useCallback 定义一个组件\n\n啥意思,简单列举一下上面的错误写法,大家千万不要学:\n\n```tsx\n// ❌ 在组件内部定义其他组件\nfunction ParentComponent(props) {\n    function ChildComponent() {\n        return <div>我是子组件</div>\n    }\n\n    return (\n        <div>\n            <ChildComponent />\n        </div>\n    )\n}\n\n// ❌ 用 render 的方式渲染 ReactNode:\nfunction MyComponent({ data1, data2, data3 }) {\n    const render = (data1, data2, data3) => {\n        return <div>{data1}{data2}{data3}</div>\n    }\n\n    return render(data1, data2, data3);\n}\n\n// ❌ 用 useCallback 定义一个组件\nimport React, { useCallback } from 'react';\n\nfunction ParentComponent(props) {\n    const ChildComponent = useCallback(() => {\n        return <div>我是子组件</div>\n    }, []);\n\n    return (\n        <div>\n            <ChildComponent />\n        </div>\n    )\n}\n```\n\n以上只是非常简单的例子,这样的错误肯定大家都不会犯,因为这个组件很简单,很好拆,怎么会写出这么新手的代码呢。在实际业务场景中,组件复杂度比这个大得多,有的时候好像没有办法去抽离一些逻辑就只能写出这种新手代码了,导致所有的状态全在组件顶层,组件中的组件也会因为顶层组件的 re-render 一直重建,性能是非常低下的。\n\n## 消除组件内部定义的其他组件\n\n我们定义一个稍微复杂一些的,数据耦合在一起的好像不得不使用组件中定位组件的一个业务组件,然后对它进行优化。\n\n假设我们有一个`OrderForm`组件,它需要根据传入的订单信息动态生成不同的`OrderItem`组件。这个时候,你可能会想在`OrderForm`中定义`OrderItem`组件,如下:\n\n错误写法:\n\n```tsx\nfunction OrderForm({ orders }) {\n  \tconst [myName, setMyName] = useState('Foo')\n    function OrderItem({ order }) {\n        return (\n            <li>\n                <h2>{order.name}</h2>\n                <p>数量: {order.quantity}</p>\n                <p>单价: {order.price}</p>\n                <p>{myName}</p>\n            </li>\n        )\n    }\n  \t\n   // 或者下面的形式\n\t  const OrderItem = React.useCallback(({ order }) {\n        return (\n            <li>\n                <h2>{order.name}</h2>\n                <p>数量: {order.quantity}</p>\n                <p>单价: {order.price}</p>\n\t\t\t\t<p>{myName}</p>\n            </li>\n        )\n    }, [order, myName])\n\n    return (\n        <ul>\n            {orders.map(order => \n                <OrderItem key={order.id} order={order} />\n            )}\n        </ul>\n    );\n}\n```\n\n但是这样做,每次`OrderForm`渲染时,`OrderItem`都会被重建,也就意味着这个列表遍历下的所有`OrderItem` 都在 re-mount 而不是 re-render。在 React 中,创建一个新的组件实例(即组件的首次渲染)通常比更新一个现有的组件实例(即组件的 re-render)要花费更多的时间。\n\n正确的做法是,将`OrderItem`组件定义在`OrderForm`外面,通过props传递数据:\n\n```tsx\nfunction OrderItem({ order, myName }) {\n    return (\n        <li>\n            <h2>{order.name}</h2>\n            <p>数量: {order.quantity}</p>\n            <p>单价: {order.price}</p>\n            <p>{myName}</p>\n        </li>\n    )\n}\n\nfunction OrderForm({ orders }) {\n    return (\n        <ul>\n            {orders.map(order => \n                <OrderItem key={order.id} order={order} myName={myName}/>\n            )}\n        </ul>\n    );\n}\n```\n\n这样,不管`OrderForm`如何渲染,`OrderItem`都只会被定义一次,而且它可以根据 props 的改变来决定是否需要重新渲染,从而提高性能。\n\n## 去除 render 方式传参的组件\n\n有时会看到一些组件是这样定义插槽的。比如下面的 Table 的组件。\n\n```tsx\ntype Data = (string | number)[][];\n\ntype ColumnRenderer = (data: string | number, rowIndex: number, columnIndex: number) => React.ReactNode;\n\ninterface TableProps {\n    data: Data;\n    columnRenderers: ColumnRenderer[];\n}\n\nconst Table: React.FC<TableProps> = ({ data, columnRenderers }) => {\n    return (\n        <table>\n            <tbody>\n                {data.map((row, rowIndex) => (\n                    <tr key={rowIndex}>\n                        {columnRenderers.map((render, columnIndex) => (\n                            <td key={columnIndex}>\n                                {render(row[columnIndex], rowIndex, columnIndex)}\n                            </td>\n                        ))}\n                    </tr>\n                ))}\n            </tbody>\n        </table>\n    );\n};\n\n```\n\n上面的写法也不是很好,首先是 render 是一个函数返回的一个 ReactNode,所以你在用这个组件的时候,传入的 render 是不能使用 React Hooks 的。例如:\n\n```jsx\n<Table data={[]} columnRenderers={[\n    (data, index) => {\n      useState() // ❌ 不能使用\n      return <div>{data.id}</div>\n  \t}\n]}\n```\n\n不仅不能使用 Hooks,而且这样的写法会导致 Table 组件re-render后,下面的所有列的 render 全部重建。因为 render 不是一个组件而是一个 ReactNode,不能用作性能优化。\n\n上述的 Table 可以改写成:\n\n```tsx\ntype Data = (string | number)[][];\n\ninterface TableColumnProps {\n    data: string | number;\n    render: (data: string | number) => React.ReactNode;\n}\n\ninterface TableProps {\n    data: Data;\n    columnComponents: Array<React.FC<TableColumnProps>>;\n}\n\nconst Table: React.FC<TableProps> = ({ data, columnComponents }) => {\n    return (\n        <table>\n            <tbody>\n                {data.map((row, rowIndex) => (\n                    <tr key={rowIndex}>\n                        {columnComponents.map((Component, columnIndex) => (\n                            <Component key={columnIndex} data={row[columnIndex]} />\n                        ))}\n                    </tr>\n                ))}\n            </tbody>\n        </table>\n    );\n};\n\n```\n\n这样就可以在定义列组件时使用 Hooks 了并且列组件只受到 `data` 的影响(前提需要给列组件套上 memo),而且也只需要把原本的 `(data1, data2) => ` 改写成 `({ data1, data2 })` 就行了。\n\n如:\n\n```jsx\n<Table data={[]} columnComponents={[\n   ColumnComponent1\n]}\n\nconst ColumnComponent1 = memo(({ data, index }) => {\n      return <div>{data.id}</div>\n})\n```\n\n今天先说到这。\n\n上述部分代码和文字由 GPT-4 编写。\n\n<span style=\"text-align: right;font-size: 0.8em; float: right\">此文由 [Mix Space](https://github.com/mx-space) 同步更新至 xLog\n原始链接为 <https://innei.ren/posts/programming/experience-in-performance-optimization-in-react-applications-2></span><br ><br >",
  "attributes": [
    {
      "value": "experience-in-performance-optimization-in-react-applications-2",
      "trait_type": "xlog_slug"
    }
  ]
}