如何编写高性能的 React 代码:规则、模式、注意事项

IQ前端

共 4872字,需浏览 10分钟

 · 2022-02-28

本文将通过逐步实现一个简单的应用来带大家看看如何编写编写高性能的 React 代码。首先会以常规的模式来实现组件,然后再考虑性能的情况下重构每个步骤,并从每个步骤中提取一个通用规则,这些规则可以应用于大多数应用程序。然后比较最后的结果。

下面将编写一个“国家设置”页面,用户可以从列表中选择国家,查看该国家的信息,可以保存国家:

可以看到,左侧有一个国家列表,带有“已保存”和“已选择”状态,当点击列表中的选项时,右侧会显示该国家的详细信息。当按下保存按钮时,选定的国家会变成已保存,已保存的选项的背景会变成蓝色。

1. 构建应用程序

首先,根据设计图,我们要考虑应用的结构以及要实现哪些组件:

  • 页面组件:在其中处理提交逻辑和国家选择逻辑;
  • 国家列表组件:将呈现列表中的所有国家,并进行过滤和排序等操作;
  • 国家选项组件:将所有国家呈现在国家列表组件中;
  • 选定国家组件:将呈现选定国家的详细信息,并具有保存按钮。

当然,这不是实现这个页面的唯一方式,实现方式仅供参考。下面就来看看如何实现这些组件。

2. 实现页面组件

下面终于要开始写代码了,下面就从根开始,实现Page组件,步骤如下:

  1. 需要组件包含页面标题、国家列表和选定国家组件;
  2. 将页面参数中的国家列表数据传递给CountriesList组件,以便它可以呈现这些数据;
  3. 页面中应该有一个已选中国家的状态,它将从 CountriesList 件接收,并传递给 SelectedCountry 组件;
  4. 页面中应该有一个已保存国家的状态,它将从 SelectedCountry 组件接收,并传递给 CountriesList 组件。
export const Page = ({ countries }: { countries: Country[] }) => {
  const [selectedCountry, setSelectedCountry] = useState(countries[0]);
  const [savedCountry, setSavedCountry] = useState(countries[0]);

  return (
    <>
      

Country settings</h1>
      
                  countries={countries}
          onCountryChanged={(c) => setSelectedCountry(c)}
          savedCountry={savedCountry}
        /
>
                  country={selectedCountry}
          onCountrySaved={() => setSavedCountry(selectedCountry)}
        />
      </div>
    >
  );
};

3. 重构页面组件(考虑性能)

在 React 中,当组件的 state 或者 props 发生变化时,组件会重新渲染。在 Page 组件中,当 setSelectedCountry 或者 setSavedCountry 被调用时,组件就会重新渲染。当组件的国家列表数据(props)发生变化时,组件也会重新渲染。CountriesList 和 SelectedCountry 组件也是如此,当它们的 props 发生变化时,都会重新渲染。

我们知道,React 会对 props 进行严格相等的比较,并且内联函数每次都会创建新值。这就导致了一个错误的的观念:为了减少 CountriesList 和SelectedCountry 组件的重新渲染,需要通过在 useCallback 中包装内联函数来避免在每次渲染中重新创建内联函数。

export const Page = ({ countries }: { countries: Country[] }) => {

  const onCountryChanged = useCallback((c) => setSelectedCountry(c), []);
  const onCountrySaved = useCallback(() => setSavedCountry(selectedCountry), []);

  return (
    <>
      ...
                  onCountryChanged={onCountryChange}
        />
                  onCountrySaved={onCountrySaved}
        />
      ...
    </>
  );
};

而实际上,这样并不会起作用。因为它没有考虑到:如果父组件 Page 被重新渲染,子组件 CountriesList 也总是会重新渲染,即使它根本没有任何 props。

可以这样来简化 Page 组件:

const CountriesList = () => {
  console.log("Re-render!!!!!");
  return 
countries list, always re-renders</div>;
};

export const Page = ({ countries }: { countries: Country[] }) => {
  const [counter, setCounter] = useState(1);

  return (
    <>
      

Country settingsh1>
      () => setCounter(counter + 1)}>
        Click here to re-render Countries list (open the console) {counter}
      </button>
      >
    </>
  );
};

当每次点击按钮时,即使没有任何 props,都会看到 CountriesList 组件被重新渲染。由此,总结出第一条规则:「如果想把 props 中的内联函数提取到 useCallback 中,以此来避免子组件的重新渲染,请不要这样做,它不起作用。」

现在,有几种方法可以处理上述情况,最简单的一种就是使用 useMemo,它本质上就是缓存传递给它的函数的结果。并且仅在 useMemo 的依赖项发生变化时才会重新执行。这就就将 CountriesList 组件使用 useMemo 包裹,只有当 useMemo 依赖项发生变化时,才会重新渲染 ComponentList 组件:

export const Page = ({ countries }: { countries: Country[] }) => {
  const [counter, setCounter] = useState<number>(1);

  const list = useMemo(() => {
    return ;
  }, []);

  return (
    <>
      

Country settings</h1>
       setCounter(counter + 1)}>
        Click here to re-render Countries list (open the console) {counter}
      button>
      {list}
    </>
  );
};

当然,在这个简化的例子中是不行的,因为它没有任何依赖项。那我们该如何简化 Page 页面呢?下面再来看一下它的结构:

export const Page = ({ countries }: { countries: Country[] }) => {
  const [selectedCountry, setSelectedCountry] = useState(countries[0]);
  const [savedCountry, setSavedCountry] = useState(countries[0]);

  return (
    <>
      

Country settings</h1>
      
                  countries={countries}
          onCountryChanged={(c) => setSelectedCountry(c)}
          savedCountry={savedCountry}
        /
>
                  country={selectedCountry}
          onCountrySaved={() => setSavedCountry(selectedCountry)}
        />
      </div>
    >
  );
};

可以看到:

  • 在 CountriesList 组件中不会使到 selectedCountry 状态;
  • 在 SelectedCountry 组件中不会使用到 savedCountry 状态;

这意味着当 selectedCountry 状态发生变化时,CountriesList 组件不需要重新渲染。savedCountry 状态发生变化时,SelectedCountry 组件也不需要重新渲染。可以使用 useMemo 来包裹它们,以防止不必要的重新渲染:

export const Page = ({ countries }: { countries: Country[] }) => {
  const [selectedCountry, setSelectedCountry] = useState(countries[0]);
  const [savedCountry, setSavedCountry] = useState(countries[0]);

  const list = useMemo(() => {
    return (
              countries={countries}
        onCountryChanged={(c) => setSelectedCountry(c)}
        savedCountry={savedCountry}
      />
    );
  }, [savedCountry, countries]);

  const selected = useMemo(() => {
    return (
              country={selectedCountry}
        onCountrySaved={() => setSavedCountry(selectedCountry)}
      />
    );
  }, [selectedCountry]);

  return (
    <>
      

Country settings</h1>
      
        {list}
        {selected}
      div>
    </>
  );
};

由此总结出第二条规则:「如果组件需要管理状态,就找出渲染树中不依赖于已更改状态的部分,并将其使用 useMemo 包裹,以减少其不必要的重新渲染。」

4. 实现国家列表组件

Page 页面已经完美实现了,是时候编写它的子组件了。首先来实现比较复杂的 CountriesList 组件。这个组件一个接收国家列表数据,当在列表中选中一个国家时,会触发 onCountryChanged 回调,并应以不同的颜色突出显示保存的国家:

type CountriesListProps = {
  countries: Country[];
  onCountryChanged: (country: Country) => void;
  savedCountry: Country;
};

export const CountriesList = ({
  countries,
  onCountryChanged,
  savedCountry
}: CountriesListProps) => {
  const Item = ({ country }: { country: Country }) => {
    // 根据国家选项是否已选中来切换不同的className
    const className = savedCountry.id === country.id ? "country-item saved" : "country-item";

    const onItemClick = () => onCountryChanged(country);
    return (
      
        



浏览 9
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报