likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 49

推荐看原文

性能和React!这是一个有趣的话题,关于这个话题的很多争议和最佳实践在短短6个月内就会发生变化。既然如此,在这方面是否可能说出任何明确的观点或提出一些概括性的建议呢?

通常,性能专家都主张“过早优化是万恶之源”和“先测量”的原则。这大致意味着“不要修复没有问题的东西”。这个观点很难反驳。但我还是要反驳一下😉

我喜欢React的原因之一是它使得实现复杂的UI交互变得非常容易。我不喜欢React的原因是,它也很容易犯下具有巨大后果的错误,而这些错误并不会立即显现出来。好消息是,我们也可以非常容易地预防这些错误,并编写大部分时间都具有良好性能的代码,从而显著减少调试性能问题所需的时间和精力。基本上,当涉及到React和性能时,“过早优化”实际上是一件好事,每个人都应该这样做😉。您只需要了解一些需要注意的模式,以有意义地进行优化。

这正是我想在本文中证明的😊。我将通过逐步实现一个“真实”的应用程序来证明这一点,首先以“正常”的方式编写代码,使用您随处可见并且肯定多次使用过的模式。然后,针对性能进行每个步骤的重构,并从每个步骤中提取出一个可以应用于大多数应用程序的规则。并在最后进行结果比较。

让我们开始吧!

我们将编写一个在线商店的“设置”页面(我们在之前的“React开发人员的高级TypeScript”文章中介绍过)。在此页面上,用户将能够从列表中选择一个国家,查看该国家的所有可用信息(如货币、交付方式等),然后将该国家保存为他们的选择国家。页面可能如下所示:

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

在左侧我们有一个国家列表,每个国家有已保存和已选中两个状态。当一个国家被点击选中时,我们在右侧显示这个国家的详细信息。当保存按钮被点击,之前选中的国家将变为已保存状态,同时展示不同的背景色。

当然,我们想要提供黑暗模式,毕竟,已经是2023年了。

此外,考虑到在React中,90%的性能问题可以总结为“重新渲染过多”,我们将在本文中主要关注如何减少这些问题。(另外10%的问题是:“渲染过于繁重”和“需要进一步调查的异常情况”)。

构建app

首先,让我们来看一下设计,划定想象中的边界,并草拟我们未来应用程序的结构以及需要实现的组件:

  • 一个根组件“Page”,在这里我们将处理“提交”逻辑和国家选择逻辑
  • 一个“List of countries”组件,用于在列表中呈现所有国家,并在未来处理过滤和排序等功能
  • 一个“Item”组件,用于在“List of countries”中呈现国家
  • 一个“Selected country”组件,用于呈现有关选定国家的详细信息,并具有“保存”按钮

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

这当然不是实现此页面的唯一方式,这正是React的美丽和诅咒之处:一切都可以以无数种方式实现,没有对错之分。但是,在长期快速增长或已经很大的应用程序中,确实存在一些模式可以被称为“绝对不要这样做”或“必须遵守”。 让我们一起看看是否可以找出它们 🙂

实现Page组件

现在,终于到了动手编码的时候了。让我们从“根”开始实现Page组件。 首先,我们需要一个带有一些样式的包装器,用于呈现页面标题、“国家列表”和“选定国家”组件。 其次,我们的页面应该从某个地方接收到国家列表,然后将其传递给CountriesList组件,以便它可以渲染这些国家。 第三,我们的页面应该知道当前“选定”的国家,这个信息将从CountriesList组件接收到,并传递给SelectedCountry组件。 最后,我们的页面应该知道当前“保存”的国家,这个信息将从SelectedCountry组件接收到,并传递给CountriesList组件(并在将来发送到后端)。

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

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

这就是“Page”组件的完整实现,它是最基本的React组件,在任何地方都可以看到,这个实现中没有任何不好的行为。除了一件事。好奇的话,你能看到它吗?

重构页面组件 - 考虑性能

我认为现在这已经是常识了,即当状态或属性发生变化时,React会重新渲染组件。在我们的Page组件中,当调用setSelectedCountrysetSavedCountry时,它将重新渲染。如果Page组件的countries数组(属性)发生变化,它也会重新渲染。对于CountriesListSelectedCountry组件也是一样的,当它们的任何属性发生变化时,它们将重新渲染。

此外,任何花费一些时间使用React的人都知道JavaScript的相等比较,即React对属性进行严格的相等比较,并且内联函数在每次渲染时都会创建新的值。这导致了一个非常常见(而且完全错误)的观念,即为了减少CountriesListSelectedCountry组件的重新渲染,我们需要通过将内联函数包装在useCallback中来避免在每次渲染时重新创建内联函数。甚至React文档中也在同一句话中提到了useCallback与“防止不必要的渲染”!看看下面的模式是否熟悉:

这段话解释了在React中的重新渲染机制以及为了减少不必要的重新渲染而使用useCallback的常见做法。它指出React在比较属性时使用严格相等比较,并且内联函数在每次渲染时都会创建新的值。因此,为了避免在每次渲染时重新创建函数,可以使用useCallback来缓存函数。

export const Page = ({ countries }: { countries: Country[] }) => {
  // ... same as before

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

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

你知道最有趣的是什么吗?实际上,这并不起作用。因为它没有考虑到React组件重新渲染的第三个原因:当父组件重新渲染时。无论props如何,如果Page重新渲染,CountriesList将始终重新渲染,即使它根本没有任何props。

我们可以将Page的示例简化为以下代码:

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

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

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

每次点击按钮时,我们会看到CountriesList重新渲染,即使它根本没有任何props。可以在Codesandbox上查看代码。

最后,这使我们能够确定本文的第一条规则:

规则1:如果你想将内联函数的props提取到useCallback中的唯一原因是为了避免子组件的重新渲染,那就不要这样做。它不起作用。

现在,有几种处理上述情况的方法,我将在这个特定场景中使用最简单的一种:useMemo钩子。它的作用实质上是“缓存”你传递给它的任何函数的结果,并且只有在useMemo的依赖项发生变化时才会刷新它们。如果我将渲染的CountriesList提取为一个变量const list = <ComponentList />;,然后在它上面应用useMemo,只有当useMemo的依赖项发生变化时,ComponentList组件才会重新渲染。

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

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

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

在这种情况下,依赖项是空的,因此永远不会发生变化。这种模式基本上允许我打破“父组件重新渲染时,无论如何都重新渲染所有子组件”的循环,并对其进行控制。在Codesandbox上查看完整的示例。

最重要的是要注意useMemo的依赖项列表。如果它依赖于导致父组件重新渲染的相同内容,那么它将在每次重新渲染时刷新缓存,从而变得无效。例如,在这个简化的示例中,如果我将计数器的值作为list变量的依赖项传递(注意:甚至不是传递给被记忆化组件的属性!),那么每次状态更改时,useMemo都会刷新自身,并导致CountriesList重新渲染。

const list = useMemo(() => {
  return (
    <>
      {counter}
      <CountriesList />
    </>
  );
}, [counter]);

查看示例

好了,现在一切都很完美。但具体如何应用到我们的非简化Page组件呢?如果我们再仔细看一下它的实现:

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

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

我们可以看到:

  • selectedCountry状态没有被CountriesList组件使用
  • savedCountry没有被SelectedCountry组件使用

[翻译]如何编写高性能 React 代码:规则、模式、注意事项 这意味着当selectedCountry状态发生变化时,CountriesList组件根本不需要重新渲染!savedCountry状态和SelectedCountry组件也是同样的情况。我可以将它们都提取为变量,并使用memo进行缓存,以防止它们的不必要重新渲染:

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

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

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

  return (
    <>
      <h1>Country settings</h1>
      <div css={contentCss}>
        {list}
        {selected}
      </div>
    </>
  );
};

这样,我们最终可以总结出本文的第二条规则:

规则2:如果您的组件管理状态,请找出不依赖于已更改状态的渲染树的部分,并对其进行缓存,以最小化它们的重新渲染。

实现国家列表

现在,我们的Page组件已经准备好了,完美无缺,是时候完善它的子组件了。首先,让我们来实现复杂的组件:CountriesList。我们已经知道,这个组件应该接受国家列表,并在选择列表中的国家时触发onCountryChanged回调函数,并根据设计要求以不同的颜色突出显示savedCountry。所以,让我们从最简单的方法开始:

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

export const CountriesList = ({
  countries,
  onCountryChanged,
  savedCountry
}: CountriesListProps) => {
  const Item = ({ country }: { country: Country }) => {
    // different className based on whether this item is "saved" or not
    const className = savedCountry.id === country.id ? "country-item saved" : "country-item";

    // when the item is clicked - trigger the callback from props with the correct country in the arguments
    const onItemClick = () => onCountryChanged(country);
    return (
      <button className={className} onClick={onItemClick}>
        <img src={country.flagUrl} />
        <span>{country.name}</span>
      </button>
    );
  };

  return (
    <div>
      {countries.map((country) => (
        <Item country={country} key={country.id} />
      ))}
    </div>
  );
};

再次强调,这是一个非常简单的组件,实际上只做两件事情:

  • 我们根据接收到的props生成Item(它依赖于onCountryChanged和savedCountry)。
  • 我们循环遍历所有国家并渲染相应的Item。

就本身而言,这些都没有任何问题,我几乎在各个地方都见过这种模式的使用。

重构国家/地区组件列表 - 考虑到性能

让我们再次回顾一下React渲染的知识,这一次是关于如果一个组件(比如上面的Item组件)在另一个组件的渲染过程中被创建会发生什么。简短回答是,实际上不会有什么好结果。从React的角度来看,这个Item组件只是一个在每次渲染时都是新的函数,并且在每次渲染时都返回一个新的结果。因此,它在每次渲染时都会从头开始重新创建函数的结果,即它只是将先前生成的组件及其DOM树丢弃,从页面中移除,并在每次父组件重新渲染时生成并挂载一个全新的组件和全新的DOM树

如果我们简化一下国家示例来演示这种效果,代码可能如下所示:

const CountriesList = ({ countries }: { countries: Country[] }) => {
  const Item = ({ country }: { country: Country }) => {
    useEffect(() => {
      console.log("Mounted!");
    }, []);
    console.log("Render");
    return <div>{country.name}</div>;
  };

  return (
    <>
      {countries.map((country) => (
        <Item country={country} />
      ))}
    </>
  );
};

这是React中最重的操作之一。从性能角度来看,与重新挂载全新创建的组件相比,进行10次“正常”重新渲染微不足道。在正常情况下,具有空依赖项数组的useEffect只会在组件完成挂载和首次渲染后触发一次。此后,React的轻量级重新渲染过程启动,组件不会从头开始创建,而只在需要时进行更新(这也是React如此快速的原因之一)。但在这种情况下,情况并非如此-打开Codesandbox链接,点击“re-render”按钮,并观察控制台,你会发现每次点击都会触发250次渲染和挂载。

解决这个问题非常明显和简单:我们只需要将Item组件移出渲染函数之外。

const Item = ({ country }: { country: Country }) => {
  useEffect(() => {
    console.log("Mounted!");
  }, []);
  console.log("Render");
  return <div>{country.name}</div>;
};

const CountriesList = ({ countries }: { countries: Country[] }) => {
  return (
    <>
      {countries.map((country) => (
        <Item country={country} />
      ))}
    </>
  );
};

现在,在我们简化的Codesandbox中,父组件的每次重新渲染时都不会发生重新挂载。

作为额外的好处,进行这样的重构有助于保持不同组件之间的良好边界,并使代码更加清晰和简洁。当我们将这个改进应用到我们的“真实”应用程序时,这一点将特别明显。之前的代码示例为:

export const CountriesList = ({
  countries,
  onCountryChanged,
  savedCountry
}: CountriesListProps) => {

  // only "country" in props
  const Item = ({ country }: { country: Country }) => {
    // ... same code
  };

  return (
    <div>
      {countries.map((country) => (
        <Item country={country} key={country.id} />
      ))}
    </div>
  );
};

修改之后

type ItemProps = {
  country: Country;
  savedCountry: Country;
  onItemClick: () => void;
};

// 结果savedCountry和onItemClick也被使用了
// 但在之前的实现中并不明显
const Item = ({ country, savedCountry, onItemClick }: ItemProps) => {
  // ... same code
};

export const CountriesList = ({
  countries,
  onCountryChanged,
  savedCountry
}: CountriesListProps) => {
  return (
    <div>
      {countries.map((country) => (
        <Item
          country={country}
          key={country.id}
          savedCountry={savedCountry}
          onItemClick={() => onCountryChanged(country)}
        />
      ))}
    </div>
  );
};

现在,我们摆脱了每次父组件重新渲染时重新挂载Item组件的问题,我们可以提取出本文的第三条规则:

规则3:永远不要在另一个组件的渲染函数内部创建新的组件。

接下来是实现“选定国家”组件,这将是本文中最简短和最无聊的部分,因为实际上没有什么可展示的内容:它只是一个接受属性和回调的组件,并渲染一些字符串:

const SelectedCountry = ({ country, onSaveCountry }: { country: Country; onSaveCountry: () => void }) => {
  return (
    <>
      <ul>
        <li>Country: {country.name}</li>
        ... // whatever country's information we're going to render
      </ul>
      <button onClick={onSaveCountry} type="button">Save</button>
    </>
  );
};

🤷🏽‍♀️ 就是这样!它只是为了让演示代码和盒子更有趣🙂

最后润色:主题化

现在是最后一步:黑暗模式!谁不喜欢那些?考虑到当前主题应该在大多数组件中可用,通过到处的 props 传递它将是一场噩梦,因此 React Context 是这里的自然解决方案。 首先创建主题上下文:

type Mode = 'light' | 'dark';
type Theme = { mode: Mode };
const ThemeContext = React.createContext<Theme>({ mode: 'light' });

const useTheme = () => {
  return useContext(ThemeContext);
};

将Context和切换按钮添加到到页面组件:

export const Page = ({ countries }: { countries: Country[] }) => {
  // same as before
  const [mode, setMode] = useState<Mode>("light");

  return (
    <ThemeContext.Provider value={{ mode }}>
      <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Toggle theme</button>
      // the rest is the same as before
    </ThemeContext.Provider>
  )
}

然后使用Context hook为主题中的按钮着色:

const Item = ({ country }: { country: Country }) => {
    const { mode } = useTheme();
    const className = `country-item ${mode === "dark" ? "dark" : ""}`;
    // the rest is the same
}

同样,这种实现方式并不违法,是一种非常常见的模式,尤其是在主题化方面。

重构主题 - 考虑到性能。

在我们继续之前,让我们看一下React组件可能被重新渲染的第四个原因,这个原因经常被忽视:如果一个组件使用了上下文消费者(context consumer),那么它将在上下文提供者(context provider)的值发生变化时重新渲染。

还记得我们的简化示例吗?在那个示例中,我们使用了memo来缓存渲染结果,以避免它们的重新渲染。

const Item = ({ country }: { country: Country }) => {
  console.log("render");
  return <div>{country.name}</div>;
};

const CountriesList = ({ countries }: { countries: Country[] }) => {
  return (
    <>
      {countries.map((country) => (
        <Item country={country} />
      ))}
    </>
  );
};

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

  const list = useMemo(() => <CountriesList countries={countries} />, [
    countries
  ]);

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

Page组件每次点击按钮时都会重新渲染,因为它在每次点击时更新状态。但是,CountriesList已经被缓存,并且不依赖于该状态,所以它不会重新渲染,结果Item组件也不会重新渲染。在这里可以查看Codesandbox示例

现在,如果我在这里添加Theme上下文,将Provider放在Page组件中,会发生什么呢?

export const Page = ({ countries }: { countries: Country[] }) => {
  // everything else stays the same

  // memoised list is still memoised
  const list = useMemo(() => <CountriesList countries={countries} />, [
    countries
  ]);

  return (
    <ThemeContext.Provider value={{ mode }}>
      // same
    </ThemeContext.Provider>
  );
};

同时将context应用在Item组件中。

const Item = ({ country }: { country: Country }) => {
  const theme = useTheme();
  console.log("render");
  return <div>{country.name}</div>;
};

如果它们只是普通的组件和钩子,就不会发生这种情况:Item不是Page组件的子组件,CountriesList由于memo化而不会重新渲染,因此Item也不会重新渲染。然而,在这种情况下,它们是提供者-消费者的组合,因此每当提供者的值发生变化时,所有的消费者都会重新渲染。而且,由于我们每次都传递一个新的对象作为值,Item会在每次计数器更新时进行不必要的重新渲染。上下文基本上绕过了我们进行的memo化,使之几乎无用。请参考Codesandbox示例。

解决这个问题的方法,正如你可能已经猜到的那样,就是确保Provider中的值非必要不做更改。在我们的情况下,我们只需要将其也进行memo化:

export const Page = ({ countries }: { countries: Country[] }) => {
  // everything else stays the same

  // memoising the object!
  const theme = useMemo(() => ({ mode }), [mode]);

  return (
    <ThemeContext.Provider value={theme}>
      // same
    </ThemeContext.Provider>
  );
};

现在,计数器将能够正常工作,而不会导致所有的Item重新渲染!

同样的解决方案也适用于我们非简化的Page组件,以防止不必要的重新渲染:

export const Page = ({ countries }: { countries: Country[] }) => {
  // same as before
  const [mode, setMode] = useState<Mode>("light");

  // memoising the object!
  const theme = useMemo(() => ({ mode }), [mode]);

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={() => setMode(mode === 'light' ? 'dark' : 'light')}>Toggle theme</button>
      // the rest is the same as before
    </ThemeContext.Provider>
  )
}

并将新知识提取到本文的最终规则中:

规则4:在使用上下文时,如果value属性不是数字、字符串或布尔值,请确保始终对其进行缓存。

将一切综合起来

最后,我们的应用程序完成了!可以在这个Codesandbox中找到完整的实现。如果你使用的是最新款的MacBook,可以减慢CPU速度,以体验普通用户的世界,并尝试在列表中选择不同的国家。即使在6倍降低的CPU速度下,它仍然非常快!🎉

现在,我怀疑很多人都有一个想要问的重要问题:“但是,Nadia,React本身就非常快。你所做的那些‘优化’对于只有250个项目的简单列表来说并没有太大的影响,你是不是夸大了它的重要性?“

是的,当我刚开始写这篇文章时,我也是这么想的。但是后来我以“非性能良好”的方式实现了该应用程序。可以在Codesandbox中查看它。我甚至不需要降低CPU速度就能看到在选择项目之间的延迟 😱。将其降低6倍,它可能是行星上最慢的简单列表之一,甚至无法正常工作(与“性能良好”的应用程序相比,它存在焦点错误)。而且我在那里甚至没有做任何明显的恶意操作! 😅

所以让我们回顾一下React组件何时会重新渲染:

  • 当组件的状态发生变化时
  • 当父组件重新渲染时
  • 当一个组件使用上下文,并且其提供者的值发生变化时

而我们提取出的规则是:

  • 规则1:如果你将内联函数作为props提取到useCallback中的唯一原因是避免子组件的重新渲染:不要这么做,它不起作用。

  • 规则2:如果你的组件管理状态,请找出不依赖于已更改状态的渲染树的部分,并对其进行缓存,以最小化它们的重新渲染。

  • 规则3:永远不要在另一个组件的渲染函数内部创建新的组件。

  • 规则4:在使用上下文时,如果value属性不是数字、字符串或布尔值,请确保始终对其进行缓存。

这就是全部!希望这些规则能够帮助你从一开始就编写更高性能的应用程序,并为用户提供更快速的产品体验。

额外内容:useCallback之谜

在结束这篇文章之前,我觉得我需要解答一个谜团:为什么useCallback对于减少重新渲染没有用处,为什么React文档上直接说“当将回调函数传递给依赖于引用相等性的优化子组件时,useCallback很有用”?🤯

答案就在这句话中:“优化了 依赖引用相等的子组件”。

这里适用两种场景。

第一种:接收回调函数的组件被React.memo包裹,并将该回调函数作为依赖项。基本上就是这样的情况:

const MemoisedItem = React.memo(Item);

const List = () => {
  // this HAS TO be memoised, otherwise `React.memo` for the Item is useless
  const onClick = () => {console.log('click!')};

  return <MemoisedItem onClick={onClick} country="Austria" />
}

或者

const MemoisedItem = React.memo(Item, (prev, next) => prev.onClick !== next.onClick);

const List = () => {
  // this HAS TO be memoised, otherwise `React.memo` for the Item is useless
  const onClick = () => {console.log('click!')};

  return <MemoisedItem onClick={onClick} country="Austria" />
}

第二种情况是,如果接收回调函数的组件在像useMemo、useCallback或useEffect这样的钩子中将该回调函数作为依赖项。

const Item = ({ onClick }) => {
  useEffect(() => {
    // some heavy calculation here
    const data = ...
    onClick(data);

  // if onClick is not memoised, this will be triggered on every single render
  }, [onClick])
  return <div>something</div>
}
const List = () => {
  // this HAS TO be memoised, otherwise `useEffect` in Item above
  // will be triggered on every single re-render
  const onClick = () => {console.log('click!')};

  return <Item onClick={onClick} country="Austria" />
}

这些内容无法简单地概括为“要做”或“不要做”,它只能用于解决特定组件的确切性能问题,并且不适用于其他情况。

现在,这篇文章终于完成了!感谢你迄今为止的阅读,希望你觉得它有用!保持健康,下次再见!✌🏼

转载自:https://juejin.cn/post/7255138771984531511
评论
请登录