去抖动、性能和 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>
);
}
在上面概述的 React 示例应用中,当用户点击“搜索”按钮时,会向OMDb API发出包含搜索字符串(电影标题)的 HTTP 请求(“开销较大的操作”)。API 会返回 JSON 格式的电影列表。
注意:上面的示例没有实现 Debounce 模式。
不去抖
由于上述示例 React 应用程序中的“昂贵操作”仅在单击组件内的“搜索”按钮时执行 HTTP 请求(即“搜索电影”)<Search />
- 去抖动对应用程序的性能几乎没有影响。
但大多数人使用现代网络应用程序的方式并非如此。
我们习惯于在网页应用中输入搜索结果时立即响应(例如google)。那么,如果我们重构代码使其也能如此运行,会发生什么呢?
动态搜索
最直接的方法是监听组件onChange
的事件<Search />
,并在每次文本改变时重新执行 HTTP 请求(搜索)。
这意味着,如果您搜索“Terminator”,该onChange
事件会针对字符串中的每个字符调用。假设输入没有拼写错误,这将至少创建 9 个get
HTTP 请求:
- “t”
- “特”
- “特尔”
- “学期”
- “终端”
- “终点站”
- “终止”
- “终结者”
- “终结者”
这意味着 9 个或更多 HTTP 请求可能会被快速重新执行,以至于在发出下一个请求之前,第一个请求还没有得到响应 - 更不用说处理和呈现了。
昂贵的操作
HTTP 请求被称为“昂贵”的操作,因为它们涉及创建请求、编码请求、通过 Web 传输请求、API 接收请求,然后当请求由 API 处理并返回到源(我们的 React 应用程序)时,该过程以相反的方式重复。
更糟糕的是,在我们的示例中,必须处理每个 HTTP 响应并将其映射到组件(<Movies />
和<MovieCard />
)以显示电影信息。
由于每个<MovieCard />
组件都有电影的图像,因此每个卡都必须向另一个资源创建另一个 HTTP 请求来检索图像。
或者,我们可以保持搜索的执行方式与原来一样,仅在触发组件的点击事件get
时才发起请求。<Search />
问题解决了吗?
当然,对于这个简单的例子 - 但是当你添加过滤时会发生什么:
OMDb API返回的每部电影都具有Poster
、Title
、Type
、Year
和属性。实际上,我们可能希望通过、 或 来imdbID
过滤返回的结果。Year
Type
为了简单起见,我们仅探讨按 进行过滤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
},[]);
接下来我们需要创建一个选择,并将所有不同的年份映射到<option>
其中的元素中<select>
。
// map the different years to
{<select>
{years.map((year) => {
return <option>{year}</option>
}})
}
结合这两个功能,我们应该有一个<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
接下来,我们将监控<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])
现在让我们包装我们的效果,确保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])
此示例中,围绕对我们的 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
现在,任何地方都有一个可能经常和/快速执行的昂贵操作,我们只需将该函数(“效果”)包装在自定义useDebounce
钩子中:
useDebounce(() => {
// effect
Axios.get(baseUrl, { params: {
apiKey: 'YOUR-API-KEY',
s: searchTitle
} }).then(response => setResults(response.Search))
}, [searchTitle], 400) // [dependencies, delay]
这就是 Debounce,以及如何抽象 Debounce 的行为以便在整个应用程序中重复使用该逻辑(以可维护的方式)。
结论
在 React 应用程序中实现去抖动功能有助于避免不必要的操作并提高性能。通过提高性能,我们的 React 应用程序变得更快,对用户输入的响应更快,并提供了更好的用户体验。
这种模式甚至可以抽象为自定义钩子,以便在整个应用程序中轻松实现该模式,但对频繁或快速重新执行(并且不需要重新执行)的“昂贵操作”或“效果”影响最大。
你觉得怎么样?Debounce 对你来说有意义吗?你会使用它吗?
文章来源:https://dev.to/jasonnordheim/debounce-performance-and-react-4de1