去抖动、性能和 React 去抖动、性能和 React

2025-06-07

去抖动、性能和 React

去抖动、性能和 React

去抖动、性能和 React

虽然“debounce”是一种更广泛的软件开发模式,但本文将重点介绍React中实现的 debounce 。

什么是 Debounce?

去抖动是一种延迟某些代码直到指定时间的方法,以避免不必要的 CPU 周期并提高软件性能。

这为什么重要?

表现。

通过限制“昂贵操作”的频率,去抖动可以让我们提高应用程序的性能。

具体来说,是指需要大量资源(CPU、内存、磁盘)才能执行的操作。“高成本操作”或缓慢的应用程序加载时间会导致用户界面卡顿和延迟,并且占用比最终所需更多的网络资源。

通过例子理解

在上下文中,去抖动是最有意义的。

假设我们有一个简单的电影搜索应用程序:

import React, { useState } from "react";
import Axios from "axios"; // to simplify HTTP request 
import "./App.css";

/**
 * Root Application Component
 */
export default function App() {
  // store/update search text & api request results in state
  const [search, setSearch] = useState("");
  const [results, setResults] = useState([]);

  /**
   * Event handler for clicking search
   * @param {event} e
   */
  const handleSearch = async (e) => {
    e.preventDefault(); // no refresh
    try {
      const searchResults = await searchAny(search);
      await setResults(searchResults.Search);
    } catch (error) {
      alert(error);
    }
  };

  return (
    <div className="app">
      <header>React Movies</header>
      <main>
        <Search value={search} setValue={setSearch} onSearch={handleSearch} />
        <Movies searchResults={results} />
      </main>
    </div>
  );
}

/**
 * Movie Card component
 * @param {{movie}} props with movie object containing movie data
 */
function MovieCard({ movie }) {
  return (
    <div className="movieCard">
      <h4>{movie.Title}</h4>
      <img alt={movie.Title} src={movie.Poster || "#"} />
    </div>
  );
}

/**
 * Container to hold all the movies
 * @param {searchResults} param0
 */
function Movies({ searchResults }) {
  return (
    <div className="movies">
      {searchResults !== undefined && searchResults !== []
        ? searchResults.map((m) => <MovieCard key={m.imdbID} movie={m} />)
        : null}
    </div>
  );
}


/**
 * Search bar
 * @param {{string, function, function}} props
 */
function Search({ value, setValue, onSearch }) {
  return (
    <div className="search">
      <input
        type="text"
        placeholder="Movie name..."
        value={value}
        onChange={(e) => setValue(e.currentTarget.value)}
      />
      <button onClick={onSearch}>Search</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

在上面概述的 React 示例应用中,当用户点击“搜索”按钮时,会向OMDb API发出包含搜索字符串(电影标题)的 HTTP 请求(“开销较大的操作”)。API 会返回 JSON 格式的电影列表。

注意:上面的示例没有实现 Debounce 模式。

不去抖

由于上述示例 React 应用程序中的“昂贵操作”在单击组件内的“搜索”按钮时执行 HTTP 请求(即“搜索电影”)<Search />- 去抖动对应用程序的性能几乎没有影响。

但大多数人使用现代网络应用程序的方式并非如此。

我们习惯于在网页应用中输入搜索结果时立即响应(例如google)。那么,如果我们重构代码使其也能如此运行,会发生什么呢?

动态搜索

最直接的方法是监听组件onChange的事件<Search />,并在每次文本改变时重新执行 HTTP 请求(搜索)。

这意味着,如果您搜索“Terminator”,该onChange事件会针对字符串中的每个字符调用。假设输入没有拼写错误,这将至少创建 9 个getHTTP 请求:

  1. “t”
  2. “特”
  3. “特尔”
  4. “学期”
  5. “终端”
  6. “终点站”
  7. “终止”
  8. “终结者”
  9. “终结者”

这意味着 9 个或更多 HTTP 请求可能会被快速重新执行,以至于在发出下一个请求之前,第一个请求还没有得到响应 - 更不用说处理和呈现了。

昂贵的操作

HTTP 请求被称为“昂贵”的操作,因为它们涉及创建请求、编码请求、通过 Web 传输请求、API 接收请求,然后当请求由 API 处理并返回到源(我们的 React 应用程序)时,该过程以相反的方式重复。

更糟糕的是,在我们的示例中,必须处理每个 HTTP 响应并将其映射到组件(<Movies /><MovieCard />)以显示电影信息。

由于每个<MovieCard />组件都有电影的图像,因此每个卡都必须向另一个资源创建另一个 HTTP 请求来检索图像。

或者,我们可以保持搜索的执行方式与原来一样,仅在触发组件的点击事件get时才发起请求。<Search />

问题解决了吗?

当然,对于这个简单的例子 - 但是当你添加过滤时会发生什么:

OMDb API返回的每部电影都具有PosterTitleTypeYear和属性。实际上,我们可能希望通过、 或 来imdbID过滤返回的结果YearType

为了简单起见,我们仅探讨按 进行过滤Year

我们可以创建一个<YearFilter />组件,将搜索结果作为道具,然后我们可以使用一个.reduce()函数来获取正在渲染的所有电影的年份:

  // use `reduce()` to get all the different years
  const years = searchResults.reduce((acc, movie) => {
    if(!acc.includes(movie.Year)) {
      acc = [...acc, movie.Year] 
    }
    return acc 
  },[]);
Enter fullscreen mode Exit fullscreen mode

接下来我们需要创建一个选择,并将所有不同的年份映射到<option>其中的元素中<select>

// map the different years to
{<select> 
{years.map((year) => {
  return <option>{year}</option>
}}) 
}
Enter fullscreen mode Exit fullscreen mode

结合这两个功能,我们应该有一个<YearFilter>显示搜索返回的电影年份的组件。

它可能看起来像这样:

// imports 
import React from 'react' 

/**
 * Component for filtering the movies 
 * @param {{searchResults}} props 
 */
export const YearFilter = ({ searchResults }) =>  {

  // no filter if 
  if(searchResults && searchResults.length < 1) return null

  // get all the different years
  const years = searchResults.reduce((acc, movie) => {
    if(!acc.includes(movie.Year)) {
      acc = [...acc, movie.Year] 
    }
    return acc 
  },[]);


  // map the different years to
  const options = years.map((year) => {
    return <option>{year}</option>;
  });

  // return JSX 
  return (
    <div className="yearFilter">
      <label>Year</label>
      <select>{options}</select>
    </div>
    )  
}

export default YearFilter
Enter fullscreen mode Exit fullscreen mode

接下来,我们将监控<select>onChange事件,并筛选出所有显示的电影,仅显示与结果匹配的电影。

希望你现在明白我的意思了。为了避免这篇文章变成教程,我会在这个例子上暂停一下。

我们正在解决的问题是,我们有这样一种场景,我们的 React 应用程序有一个昂贵的操作,该操作正在被快速重新执行,速度如此之快,以至于操作(“效果”)甚至可能在调用另一个函数“效果”之前尚未完成其执行。

介绍 Debounce

使用 Debounce,我们可以告诉 React 仅在一定时间后重新执行查询。实现此功能最简单的方法是利用setTimeout()JavaScript 提供的原生函数,并将超时包装在“昂贵操作”周围。

因此,让我们只关注我们关心的操作:检索电影名称。从逻辑上讲,我们可能希望等到有人停止输入,或者所有筛选条件都选择完毕后再发出请求。

由于OMDb API的免费套餐每天仅允许 1,000 个请求,因此我们可能也希望出于这个原因限制请求的数量。

因此,在这里我简化了我们想要在钩子内进行 Debounce 的昂贵操作useEffect

useEffect(() => {
  // using Axios for simplicity 
  Axios.get(baseUrl, { params: {
    apiKey: 'YOUR-API-KEY', s: searchTitle
  } }).then(response => setResults(response.Search))
}, [searchTitle])
Enter fullscreen mode Exit fullscreen mode

现在让我们包装我们的效果,确保setTimeout()效果仅在延迟后重新执行。

useEffect(() => {
  // capture the timeout 
  const timeout = setTimeout(() => {
    Axios.get(baseUrl, { params: {
      apiKey: 'YOUR-API-KEY', 
      s: searchTitle 
      } }).then(response => setResults(response.Search))
  }, 400) // timeout of 250 milliseconds 

  // clear the timeout 
  return () => clearTimeout(timeout)
}, [searchTitle])
Enter fullscreen mode Exit fullscreen mode

此示例中,围绕对我们的 API 的 HTTP 请求的函数setTimeout()现在确保无论调用效果多少次(即,任何时候发生searchTitle变化),实际网络请求的调用频率都不能超过400毫秒间隔。

时间是任意数字,根据需要调整

保持“干燥”

在大多数实际的 React 应用中,通常不会只有一个网络请求。所以,“复制粘贴”在软件开发中从来都不是一个好选择。如果我们只是简单地复制上面的效果并修改其中包装的函数,我们就会犯下第一个编程错误——重复自己,并承担以后可能带来问题的技术债务。

我们可以将行为抽象化,而不是“复制粘贴”并进行修改以满足独特的需求。

在 React 中,我们可以使用自定义钩子来抽象此功能。

// useDebounce.js 
import { useEffect, useCallback } from 'react' 

export const useDebounce(effect, dependencies, delay) => {
  // store the provided effect in a `useCallback` hook to avoid 
  // having the callback function execute on each render 
  const callback = useCallback(effect, dependencies)

  // wrap our callback function in a `setTimeout` function 
  // and clear the tim out when completed 
  useEffect(() => {
    const timeout = setTimeout(callback, delay)
    return () => clearTimeout(timeout)
  }, 
  // re-execute  the effect if the delay or callback changes
  [callback, delay]
  )  
}

export default useDebounce 
Enter fullscreen mode Exit fullscreen mode

现在,任何地方都有一个可能经常和/快速执行的昂贵操作,我们只需将该函数(“效果”)包装在自定义useDebounce钩子中:

useDebounce(() => {
  // effect 
  Axios.get(baseUrl, { params: {
      apiKey: 'YOUR-API-KEY', 
      s: searchTitle 
      } }).then(response => setResults(response.Search))
}, [searchTitle], 400)  // [dependencies, delay]
Enter fullscreen mode Exit fullscreen mode

这就是 Debounce,以及如何抽象 Debounce 的行为以便在整个应用程序中重复使用该逻辑(以可维护的方式)。

结论

在 React 应用程序中实现去抖动功能有助于避免不必要的操作并提高性能。通过提高性能,我们的 React 应用程序变得更快,对用户输入的响应更快,并提供了更好的用户体验。

这种模式甚至可以抽象为自定义钩子,以便在整个应用程序中轻松实现该模式,但对频繁或快速重新执行(并且不需要重新执行)的“昂贵操作”或“效果”影响最大。

你觉得怎么样?Debounce 对你来说有意义吗?你会使用它吗?

文章来源:https://dev.to/jasonnordheim/debounce-performance-and-react-4de1
PREV
15+ 值得参加的免费虚拟会议和聚会
NEXT
@import 在 CSS 中如何工作?它的优点和缺点是什么?🤔