【翻译】如何在 React 中书写更好的函数组件

327 阅读5分钟

本文翻译自 原文地址
翻译文章的目的纯粹是处于锻炼自己英文阅读的能力,如有翻译出现歧义的地方欢迎讨论,侵删

我们知道,在 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 重新渲染。结果,ascendingFndescendingFn 将被重建。这将会引起 comparisonFunc 改变,由于 SortedListView中的 useMemo 依赖 comparisonFunc,即使 comparisonFunc 逻辑上没有发生变化,它也会重新排序。

我们可以通过使用 useCallback 包装 ascendingFndescendingFn 解决这个问题。这个 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)

我们可以进行的另一个改进是讲上面函数中的 ascendingFndescendingFn 移动到 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中编写更具可读性和优化的函数组件。 随意分享你自己的技巧,以编写更好的函数组件。