如果你在React生态系统中待了足够长的时间,你很可能见过这样的代码:几乎所有的函数都被包裹在了useCallback中,而计算出的值也被包裹在了useMemo中。

这样做的原因在于“记忆化能够提升性能”。但大多数情况下,这种做法并不会真正带来更好的性能,反而会让代码更难调试。

在这篇文章中,你将学习如何合理组织代码,以避免过度使用useCallbackuseMemo

先决条件

在阅读本教程之前,你应该已经熟悉React的钩子函数和组件。我们假设你已经了解useStateuseEffect以及useRef。如果你需要复习useCallbackuseMemo的相关知识,可以阅读freeCodeCamp提供的以下文章:

目录

useCallbackuseMemo的作用

在讨论如何避免过度使用这些功能之前,我们先简要了解一下它们具体的作用。

`useMemo`

`useMemo`会将在函数重新渲染之间返回的值缓存起来。假设你在某个组件中维护了一个排序后的项目列表:

interface Item {
  name: string;
  createdAt: string;
}

function App() {
  // == 其他状态 ==
  const [items, setItems] = useState:;];

  const sortedItems = [...items].sort(
    (a, b) => new Date(a.createdAt).getTime() - new Date(bcreatedAt).getTime(),
  );

  return (
    <>
      
    {sortedItems.map((i) => (
  • {i.name}
  • ))}
); }

每当`App`组件重新渲染时,React都会重新计算`sortedItems`。这意味着,只要`App`组件的状态发生任何变化,`sortedItems`就会被重新计算。

React开发者通常会使用`useMemo`来缓存这类数据。

将`sortedItems`包装在`useMemo`中,可以确保只有当`items`的状态真正发生变化时,才会重新计算`sortedItems`:

const sortedItems = useMemo(() => {
  return [...items].sort(
    (a, b) => new Date(a.createdAt).getTime() - new Date(b createdAt).getTime(),
  );
}, [items]);

`useCallback`

`useCallback`会缓存函数本身。如果组件中的某些状态发生变化,下面的这个函数就会被重新创建:

function App() {
  // == 其他状态 ==
  const [userId, setUserId] = useState(0);

  const verifyUser = async () => {
    // 更新状态以显示加载中
    console.log("__ 正在处理用户ID __", userId);
    // 更新状态以停止显示加载中
  };

  return (
    <>
      
    
  );
}

但如果将`verifyUser`包装在`useCallback`中,只要`userId`的状态没有改变,就会继续使用同一个函数实例:

function App() {
  // == 其他状态 ==
  const [userId, setUserId] = useState(0);

  const verifyUser = useCallback(async () => {
    // 更新状态以显示加载中
    console.log("__ 正在处理用户ID __", userId);
    // 更新状态以停止显示加载中
  }, [userId]);

  return (
    <>
      
    
  );
}

记忆化带来的问题

生活中没有什么是免费的,记忆化也不例外。每当你使用`useCallback`或`useMemo`时:

  • 您的应用程序会分配内存来存储缓存值以及依赖关系数组。

  • 您的组件会进行比较,以判断这些依赖关系是否发生了变化。

大多数情况下,这种记忆化机制并没有实际作用。创建一个JavaScript函数的成本很低,对包含50个元素的列表进行排序也是如此。将这些操作包装在记忆化函数中,所增加的开销其实并不足以弥补其带来的好处。(不过需要注意的是,如果性能分析显示排序是整个程序的瓶颈,那么使用useMemo仍然是合理的。)

更好的做法是合理设计组件结构,从而减少不必要的重新渲染操作。

存在问题的页面

为了了解这一点的实际效果,你可以查看一个搜索页面的实现方式——在这个页面中,一个父组件负责管理整个页面的所有状态和逻辑。

如果你想跟着这个例子一起编写代码,可以克隆我为这个演示准备的简单Next.js项目:

git clone https://github.com/Olaleye-Blessing/freecodecamp-usecallback-usememo.git

# 进入该项目目录
cd freecodecamp-usecallback-usememo

# 安装所需的包
pnpm install

# 启动开发模式
pnpm dev

这个搜索页面由以下部分组成:

搜索页面的演示效果:用户先搜索“alpine”,然后清除搜索结果,接着通过筛选抽屉进行进一步筛选。

上述所有子组件都不维护任何状态或函数,它们的状态和功能都是从SearchPage组件中获取的。

我们不会详细讨论这些子组件的实现方式,因为它们仅仅负责渲染用户界面,完全不包含任何逻辑代码。

SearchPage组件的结构如下:

“`javascript

"use client";

import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
fetchColors,
fetchCountries,
fetchModes,
fetchProducts,
} from "./utils";
import { Header } from "./_components/header";
import { FilterDrawer } from "./_components/filter-drawer";
import { ProductTable } from "./_components/products-table";
import { FilterChips } from "./_components/filter-chips";
import { usePathname, useRouter, useSearchParams } from "nextnavigation";
import { FilterState, LocalSortField, SortDir, SortField } from "./interfaces";

const DEFAULTS: FilterState = {
query: "",
country: "",
color: "",
mode: "",
minPrice: "",
maxPrice: "",
sortField: "name",
sortDir: "asc",
};

export default function SearchPage() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const [drawerOpen, setDrawerOpen] = useState(false);

const [localSort, setLocalSort] = useState< { field: LocalSortField; dir: "asc" | "desc" } > || null;

const searchRef = useRef>(null);
const searchTimerRef = useRef> | null>(null);

const filters: FilterState = {
query: searchParams.get("q") ?? DEFAULTS.query,
country: searchParams.get("country") ?? DEFAULTS.country,
color: searchParams.get("color") ?? DEFAULTS.color,
mode: searchParams.get("mode") ?? DEFAULTS.mode,
minPrice: searchParams.get("minPrice") ?? DEFAULTS.minPrice,
maxPrice: searchParams.get("maxPrice") ?? DEFAULTS.maxPrice,
sortField:
(searchParams.get("sortField") as SortField) ?? DEFAULTS.sortField,
sortDir: (searchParams.get("sortDir") as SortDir) ?? DEFAULTS.sortDir,
};

const apiFilters = {
query: filters.query,
country: filters.country || undefined,
color: filters.color || undefined,
mode: filters.mode || undefined,
minPrice: filters.minPrice ? Number(filters.minPrice) : undefined,
maxPrice: filters.maxPrice ? Number(filtered.maxPrice) : undefined,
sortField: filters.sortField,
sortDir: filters.sortDir,
};

const productsQuery = useQuery({
queryKey: ["products", apiFilters],
queryFn: () => fetchProducts(api Filters),
});

const countriesQuery = useQuery({
queryKey: ["countries"],
queryFn: fetchCountries,
staleTime: Infinity,
});

const colorsQuery = useQuery({
queryKey: ["colors"],
queryFn: fetchColors,
staleTime: Infinity,
});

const modesQuery = useQuery({
queryKey: ["modes"],
queryFn: fetchModes,
staleTime: Infinity,
});

// 更新抽屉中的过滤条件
const setFilters = (partial: Partial) => {
const next = new URLSearchParams(searchParams.toString());
const merged = { ...filters, ...partial };

const keyMap: Record = {
query: "q",
country: "country",
color: "color",
mode: "mode",
minPrice: "minPrice",
maxPrice: "maxPrice",
sortField: "sortField",
sortDir: "sortDir",
};

(Object.keys(merged) as (keyof FilterState)[])++.forEach((k) => {
const paramKey = keyMap[k];
const val = merged[k];
const def = DEFAULTS[k];
if (val && val !== def) {
next.set(paramKey, val);
} else {
next.delete(paramKey);
}
});

router.push(`\({pathname}?\){next.toString()}`, { scroll: false });
};

const resetFilters = () => {
router.push(pathname, { scroll: false });
};

const handleQueryChange = (e: ChangeEvent) => {
const val = e.target.value;

if (searchTimerRef.current) clearTimeout(searchTimerRef.current);

searchTimerRef.current = setTimeout(() => {
setFilters({ query: val });
}, 400);
};

const handleClearQuery = () => {
if (searchRef.current) {
searchRef.current.value = "";
}

setFilters({ query: "" });
};

const handleColumnClick = (field: LocalSortField) => {
setLocalSort((prev) => {
if (!prev || prev.field !== field) return { field, dir: "asc" };

if (prev.dir === "asc") return { field, dir: "desc" };

return null;
});
};

const hasPriceFilter = filters.minPrice || filters.maxPrice;
const priceLabel = [
filters.minPrice ? `$${filters.minPrice}` : null,
filters.maxPrice ? `$${filters.maxPrice}` : null,
]
.filter(Boolean)
.join(" - ");

const activeFilterCount = [
filters.country,
filters.color,
filters.mode,
filters.minPrice,
filters.maxPrice,
].filter(Boolean).length;

let sortedProducts = [...(productsQuery.data || [])];
if (localSort) {
sortedProducts = [...sortedProducts].sort((a, b) => {
const aVal = a[localSort.field];
const bVal = b[localSort.field];
const cmp =
typeof aVal === "string"
? aVal.localeCompare(bVal as string)
: (aVal as number) - (bVal as number);
return localSort.dir === "desc" ? -cmp : cmp;
});
}

useEffect(() => {
return () => {
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
};
}, []);

return (

setDrawerOpen((v) => !v)}
activeFilterCount={activeFilterCount}
searchRef={searchRef}
handleChange={handleQueryChange}
/>

setDrawerOpen(false)}
filters={filters}
onChange={setFilters}
onReset={resetFilters}
countries={countriesQuery.data ?? []}
colors={colorsQuery.data ?? []}
modes={modesQuery.data ?? []}
activeFilterCount={activeFilterCount}
/>


{activeFilterCount > 0 && (

)}


);
}

“`

SearchPage组件负责处理渲染该页面所需的所有逻辑:

  • 它会获取productscountriescolors以及modes这些数据,并将countriescolorsmodes传递给抽屉组件。

  • 它还会跟踪抽屉组件的状态变化。

  • 它定义了用于在本地对产品进行排序等功能所需的相关逻辑。

问题在于,只要这些状态中的任何一个发生变化,SearchPage组件中所有的相关函数都会被重新生成。例如,当在获取产品的useQuery函数(productsQuery)中,isLoading的状态从false变为true时,所有相关的函数和计算结果都会被重新计算。

首先可能会想到的解决办法是使用useCallbackuseMemo来缓存这些函数和计算结果。虽然这种方法确实有效,但它会为这个页面带来不必要的性能开销。

一个更好的解决方案是将状态和相关逻辑放在它们实际被使用的位置附近。

如何将状态逻辑下移

其思路是这样的:如果只有某个组件需要使用某段状态或函数,那么就应该让这个组件自己拥有这些资源。当一个子组件管理自己的状态时,该状态的变更就不会导致父组件的重新渲染。这样一来,所有同级组件的状态和功能都会保持稳定,也就无需使用useMemo来进行缓存了。

不过,也不要将逻辑下移得过于深远,否则会导致共享功能的测试或协调变得困难。我们的目标并不是把所有的逻辑都隐藏在最深层的组件中,而是要将状态和逻辑放在对功能实现来说最为合适的层级上。

将ProductTable的逻辑移至其所属组件

观察一下产品的获取和排序过程,你会发现只有ProductsTable这个组件需要使用这些数据。因此,我们可以把获取数据和排序逻辑直接放到ProductsTable组件中。

目前,ProductsComponent是通过props来接收状态和逻辑的:

"use client";

interface ProductTableProps {
  products: Product[];
  isLoading: boolean;
  handleColumnClick: (field: LocalSortField) => void;
  localSort: { field: LocalSortField; dir: SortDir } | null;
}

export function ProductTable({
  products,
  isLoading,
  handleColumnClick,
  localSort,
}: ProductTableProps) {
  // 使用props来渲染UI
}

现在,ProductTable将负责自己获取数据并处理相关逻辑:

interface ProductTableProps {
  filters: FilterState;
}

export function ProductTable({ filters }: ProductTableProps) {
  const [localSort, setLocalSort] = useState<{
    field: LocalSortField;
    dir: "asc" | "desc";
  } | null>>(null);

  const apiFilters = {
    query: filters.query,
    country: filters.country || undefined,
    color: filters.color || undefined,
    mode: filters.mode || undefined,
    minPrice: filters.minPrice ? Number(filters.minPrice) : undefined,
    maxPrice: filters.maxPrice ? Number.filters.maxPrice) : undefined,
    sortField: filters.sortField,
    sortDir: filters.sortDir,
  };

  const { data: products = [], isLoading } = useQuery({
    queryKey: ["products", apiFilters],
    queryFn: () => fetchProducts(apiFilters),
  });

  const handleColumnClick = (field: LocalSortField) => {
    setLocalSort((prev) => {
      if (!prev || prev.field !== field) return { field, dir: "asc" };
      if (prev.dir === "asc") return { field, dir: "desc" };

      return null;
    });
  };

  let sortedProducts = products;
  if (localSort) {
    sortedProducts = [...products].sort((a, b) => {
      const aVal = a[localSort.field];
      const bVal = b[localSort.field];
      const cmp =
        typeof aVal === "string"
          ? aVal.localeCompare(bVal as string)
          : (aVal as number) - (bVal as number);
      return localSort.dir === "desc" ? -cmp : cmp;
    });
  }

  return <>/*== 使用props来渲染UI ==*/></>;
}

现在,当 `isLoading` 的值发生变化时,`SearchPage` 组件不会重新渲染。这意味着 `SearchPage` 组件中计算得出的衍生值以及其他函数也不会被重新生成。唯一会被重新生成的值和函数是 `sortedProducts` 和 `handleColumnClick`。

`SearchPage` 组件的变化后的结构如下:

"use client";

import { ChangeEvent, useEffect, useRef, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchColors, fetchCountries, fetchModes } from "./utils";
import { Header } from "./_components/header";
import { FilterDrawer } from "./_components/filter-drawer";
import { ProductTable } from "./_components/products-table";
import { FilterChips } from "./_components/filter-chips";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { FilterState, SortDir, SortField } from "./interfaces";

const DEFAULTS: FilterState = {
  query: "",
  country: "",
  color: "",
  mode: "",
  minPrice: "",
  maxPrice: "",
  sortField: "name",
  sortDir: "asc",
};

export default function SearchPage() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const [drawerOpen, setDrawerOpen] = useState(false);

  const searchRef = useRef<HTMLInputElement>>(null);
  const searchTimerRef = useRef<ReturnType<typeof setTimeout>> | null>(null);

  const filters: FilterState = {
    query: searchParams.get("q") ?? DEFAULTS.query,
    country: searchParams.get("country") ?? DEFAULTS.country,
    color: searchParams.get("color") ?? DEFAULTS.color,
    mode: searchParams.get("mode") ?? DEFAULTS.mode,
    minPrice: searchParams.get("minPrice") ?? DEFAULTS.minPrice,
    maxPrice: searchParams.get("maxPrice") ?? DEFAULTS.maxPrice,
    sortField:
      (searchParams.get("sortField") as SortField) ?? DEFAULTS.sortField,
    sortDir: (searchParams.get("sortDir") as SortDir) ?? DEFAULTS.sortDir,
  };

  const countriesQuery = useQuery({
    queryKey: ["countries"],
    queryFn: fetchCountries,
    staleTime: Infinity,
  });

  const colorsQuery = useQuery({
    queryKey: ["colors"],
    queryFn: fetchColors,
    staleTime: Infinity,
  });

  const modesQuery = useQuery({
    queryKey: ["modes"],
    queryFn: fetchModes,
    staleTime: Infinity,
  });

  // 更新抽屉中的过滤条件
  const setFilters = (partial: Partial<FilterState>>) => {
    const next = new URLSearchParams(searchParams.toString());
    const merged = { ...filters, ...partial };

    const keyMap: Record<keyof FilterState, string>> = {
      query: "q",
      country: "country",
      color: "color",
      mode: "mode",
      minPrice: "minPrice",
      maxPrice: "maxPrice",
      sortField: "sortField",
      sortDir: "sortDir",
    };

    (Object.keys(merged) as (keyof FilterState)[]).forEach((k) => {
      const paramKey = keyMap[k];
      const val = merged[k];
      const def = DEFAULTS[k];
      if (val && val !== def) {
        next.set(paramKey, val);
      } else {
        next.delete(paramKey);
      }
    });

    router.push(`\({pathname}?\){next.toString()}`, { scroll: false });
  };

  const resetFilters = () => {
    router.push(pathname, { scroll: false });
  };

  const handleQueryChange = (e: ChangeEvent<HTMLInputElement>>) => {
    const val = e.target.value;

    if (searchTimerRef.current) clearTimeout(searchTimerRef.current);

    searchTimerRef.current = setTimeout(() => {
      setFilters({ query: val });
    }, 400);
  };

  const handleClearQuery = () => {
    if (searchRef.current) {
      searchRef.current.value = "";
    }

    setFilters({ query: "" });
  };

  const hasPriceFilter = filters.minPrice || filters.maxPrice;
  const priceLabel = [
    filters.minPrice ? `$${filters.minPrice}` : null,
    filters.maxPrice ? `$${filters.maxPrice}` : null,
  ]
    .filter(Boolean)
    .join(" - ");

  const activeFilterCount = [
    filters.country,
    filters.color,
    filters.mode,
    filters.minPrice,
    filters.maxPrice,
  ].filter(Boolean).length;

  useEffect(() => {
    return () => {
      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
    };
  }, []);

  return (
    
0 && ( <\/main> <\/div> ); }

SearchPage组件不再负责获取和排序产品数据了。

将过滤逻辑移至相应的组件中

为实现这一功能,我们需要使用不同的状态、数据和函数:

  • drawerOpensetDrawerOpen用于控制过滤抽屉的显示/隐藏。

  • countriescolorsmodes这些数据允许用户选择不同的筛选条件。

  • activeFilterCount用于显示当前处于激活状态的过滤条件的数量。

目前,用于实现过滤功能的组件主要有两个。第一个是位于Header组件内的按钮,其外观如下:

<button
  onClick={onToggleFilters}
  className="relative ml-auto flex items-center gap-2 px-3 py-2 text-sm text-black font-medium border border-stone-300 rounded-lg hover:bg-stone-100 transition"
>
  <SlidersHorizontal className="w-4 h-4" />
  过滤器>
  {activeFilterCount > 0 && (
    
      {activeFilterCount}
    
  )}
</button>;

第二个组件是FilterDrawer,其外观如下:

"use client";

import { useEffect, useRef } from "react";
import { X, RotateCcw } from "lucide-react";
import { FilterState } from "../interfaces";
import { SortSection } from "./filter/sort-section";
import { NarrowResultsSection } from "./filter/narrow-result-section";

interface FilterDrawerProps {
  open: boolean;
  onClose: () => void;
  filters: FilterState;
  onChange: (partial: Partial) => void;
  onReset: () => void;
  countries: string[];
  colors: string[];
  modes: string[];
  activeFilterCount: number;
}

export function FilterDrawer({
  open,
  onClose,
  filters,
  onChange,
  onReset,
  countries,
  colors,
  modes,
  activeFilterCount,
}: FilterDrawerProps) {
  const drawerRef = useRef〈HTMLDivElement〉(null);

  // 在用户按下Escape键时关闭抽屉
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, [onClose]);

  // 当抽屉打开时,防止页面滚动
  useEffect(() => {
    document.body.style.overflow = open ? "hidden" : "";
    return () => {
      document.body.style.overflow = "";
    };
  }, [open]);

  return (
    <><>
      {/* 背景层 */}
      
{/* 页头 */}
0 && ( )}

>

{/* 页脚 */} {activeFilterCount > 0 && (
清除所有过滤器
)} <></aside> <>> ); }

你可以将这两个组件合并成一个单一的`Filter`组件,让这个组件包含所有这些逻辑:

import { RotateCcw, SlidersHorizontal, X } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { SortSection } from "./filter/sort-section";
import { NarrowResultsSection } from "./filter/narrow-result-section";
import { FilterState } from "../interfaces";
import { usePathname, useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { fetchColors, fetchCountries, fetchModes } from "../utils";

interface FilterProps {
  filters: FilterState;
  onChange: (partial: Partial) => void;
}

const Filter = ({ filters, onChange }: FilterProps) => {
  const { data: countries = [] } = useQuery({
    queryKey: ["countries"],
    queryFn: fetchCountries,
    staleTime: Infinity,
  });

  const { data: colors = [] } = useQuery({
    queryKey: ["colors"],
    queryFn: fetchColors,
    staleTime: Infinity,
  });

  const { data: modes = [] } = useQuery({
    queryKey: ["modes"],
    queryFn: fetchModes,
    staleTime: Infinity,
  });

  const router = useRouter();
  const pathname = usePathname();
  const [drawerOpen, setDrawerOpen] = useState(false);

  const drawerRef = useRef(null);

  const onClose = () => setDrawerOpen(false);
  const openDrawer = () => setDrawerOpen(true);
  const resetFilters = () => {
    onClose();
    router.push(pathname, { scroll: false });
  };

  const activeFilterCount = [
    filters.country,
    filters.color,
    filters.mode,
    filters.minPrice,
    filters.maxPrice,
  ].filter(Boolean).length;

  // 通过按下Esc键关闭抽屉
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === "Escape") setDrawerOpen(false);
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, []);

  // 当抽屉打开时阻止页面滚动
  useEffect(() => {
    document.body.style.overflow = drawerOpen ? "hidden" : "";
    return () => {
      document.body.style.overflow = "";
    };
  }, [drawerOpen]);

  return (
    <>
      
      {/* 背景图 */}
      
{/* 抽屉面板 */} ); }; export default Filter;

你可以进一步优化这个设计。请注意,NarrowResultsSection是唯一一个会使用获取到的countriescolorsmodes数据的组件。而在该组件内部,每一个SelectField》都会使用这些数据中的一部分。

import { FilterState } from "..interfaces";
import { PriceRangeField } from "./price-range";
import { SelectField } from "./select-field";

interface NarrowResultsSectionProps {
  filters: FilterState;
  onChange: (partial: Partial) => void;
  countries: string[];
  colors: string[];
  modes: string[];
}

export function NarrowResultsSection({
  filters,
  onChange,
  countries,
  colors,
  modes,
}: NarrowResultsSectionProps) {
  return (
    

筛选结果

onChange({ country: v})} placeholder="所有国家" /> onChange({ color: v})} placeholder="所有颜色" /> onChange({ mode: v})} placeholder="所有模式" />
); }

你不必一开始就获取所有数据,然后将其传递给所有的SelectField》,而是可以为每个SelectField》单独发起查询。

SelectField》的具体实现如下:

interface SelectFieldProps {
  label: string;
  value: string;
  options: string[];
  onChange: (v: string) => void;
  placeholder: string;
}

export function SelectField({
  label,
  value,
  options,
  onChange,
  placeholder,
}: SelectFieldProps) {
  return <>>{/*=== 渲染用户界面 ===*/</>>;
}

现在,它的实现方式变成了这样:

import { useQuery } from "@tanstack/react-query";

interface SelectFieldProps {
  label: string;
  value: string;
  onChange: (v: string) => void;
  placeholder: string;
  queryFn(): Promise>{/*=== 渲染用户界面 ===*/</>>;
}

现在,每个下拉列表都会管理自己对应的数据。当某个SelectField》的状态发生变化时,这种变化不会影响到它的兄弟元素或父元素。

将搜索逻辑移入相应的组件中

Search组件是唯一一个使用了去抖动逻辑的组件(searchTimerRefhandleQueryChangehandleClearQuery)。你需要将这些逻辑直接放入该组件内部:

"use client";

import { Search as SearchIcon, X } from "lucide-react";
import { ChangeEvent, useEffect, useRef } from "react";
import { FilterState } from "../interfaces";

interface SearchProps {
  query: string;
  onChange: (partial: Partial) => void;
}

const Search = ({ query, onChange }: SearchProps) => {
  const searchRef = useRef(null);
  const searchTimerRef = useRef | null>(null);

  const handleClearQuery = () => {
    if (searchRef.current) {
      searchRef.current.value = "";
    }

    onChange({ query: "" });
  };

  const handleQueryChange = (e: ChangeEvent) => {
    const val = e.target.value;

    if (searchTimerRef.current) clearTimeout(searchTimerRef.current);

    searchTimerRef.current = setTimeout(() => {
      onChange({ query: val });
    }, 400);
  };

  useEffect(() => {
    return () => {
      if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
    };
  }, []);

  return 
/*=== 显示用户界面 ===*/
; }; export default Search;

将筛选选项组件移入相应的组件中

FilterChips组件用于显示当前选中的筛选条件。其实,提供给该组件的hasPriceFilterpriceLabel这些数据完全可以直接放在FilterChips组件内部,而无需再通过SearchPage来传递:

interface FilterChipsProps {
  filters: FilterState;
  setFilters: (partial: Partial) => void;
  reset Filters: () => void;
}

export function FilterChips({
  filters,
  setFilters,
  resetFilters,
}: FilterChipsProps) {
  const priceLabel = [
    filters.minPrice ? `$${filters.minPrice}` : null,
    filters.maxPrice ? `$${filters.maxPrice}` : null,
  ]
    .filter(Boolean)
    .join(" - ");

  const hasPriceFilter = filters.minPrice || filters.maxPrice;

  return 
; }

最终的SearchPage组件

在将所有的状态和逻辑都移入到需要使用它们的组件之后,SearchPage组件的结构就变成了这样:

"use client";

import { Header } from "./_components/header";
import { ProductTable } from "./_components/products-table";
import { FilterChips } from "./_components/filter-chips";
import { usePathname, useRouter, useSearchParams } from "nextnavigation";
import { FilterState, SortDir, SortField } from "./interfaces";

const DEFAULTS: FilterState = {
  query: "",
  country: "",
  color: "",
  mode: "",
  minPrice: "",
  maxPrice: "",
  sortField: "name",
  sortDir: "asc",
};

export default function SearchPage() {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const filters: FilterState = {
    query: searchParams.get("q") ?? DEFAULTS.query,
    country: searchParams.get("country") ?? DEFAULTS.country,
    color: searchParams.get("color") ?? DEFAULTS.color,
    mode: searchParams.get("mode") ?? DEFAULTS.mode,
    minPrice: searchParams.get("minPrice") ?? DEFAULTS.minPrice,
    maxPrice: searchParams.get("maxPrice") ?? DEFAULTS.maxPrice,
    sortField:
      (searchParams.get("sortField") as SortField) ?? DEFAULTS.sortField,
    sortDir: (searchParams.get("sortDir") as SortDir) ?? DEFAULTS.sortDir,
  };

  const setFilters = (partial: Partial) => {
    const next = new URLSearchParams(searchParams.toString());
    const merged = { ...filters, ...partial };

    const keyMap: Record〈keyof FilterState, string〉 = {
      query: "q",
      country: "country",
      color: "color",
      mode: "mode",
      minPrice: "minPrice",
      maxPrice: "maxPrice",
      sortField: "sortField",
      sortDir: "sortDir",
    };

    (Object.keys(merged) as (keyof FilterState)[])++.forEach((k) => {
      const paramKey = keyMap[k];
      const val = merged[k];
      const def = DEFAULTS[k];
      if (val && val !== def) {
        next.set(paramKey, val);
      } else {
        next.delete(paramKey);
      }
    });

    router.push(`\({pathname}?\){next.toString()}`, { scroll: false });
  };

  const resetFilters = () => {
    router.push(pathname, { scroll: false });
  };

  const activeFilterCount = [
    filters.country,
    filters.color,
    filters.mode,
    filters.minPrice,
    filters.maxPrice,
  ].filter(Boolean).length;

  return (
    
{activeFilterCount > 0 && ( )}
); }

请注意,`setFilters`、`reset Filters`和`activeFilterCount`这些函数仍然存在于`SearchPage`组件中。这是有意为之的——因为这些值的计算结果实际上取决于URL地址,而任何从URL中获取数据的组件,在URL发生变化时都会重新渲染。因此,无论这些值是在哪里被计算的,并不会影响这一行为。

在使用这些钩子之前,请先修复你的代码

当遇到无限循环重绘的情况时,你可能会想要使用`useCallback`或`useMemo`来解决问题。然而,对象引用的不稳定性往往会导致无限次的重绘,尤其是当某个子组件的`useEffect`效应依赖于某些对象时。在这种情况下,最好的做法是弄清楚导致这种循环的原因,并从根本上解决它,而不是直接使用这些钩子。

来看这个例子:

"use client";

import { useEffect, useState } from "react";
import { fetchUsers, Filter, User } from "./utils";

interface UserListProps {
  filters: Filter;
  onLoad(data: User[]): void;
  users: User[];
}

function UserList({ filters, onLoad, users }: UserListProps) {
  console.count "__ USER LIST __");

  useEffect(() => {
    fetchUsers(filters).then((data) => {
      onLoad(data);
    });
  }, [filters, onLoad]);

  return (
    
    ]; const filters = { role: "admin", active: true, }; return

`UserList`组件在初始化时会从服务器获取用户数据,它将`filters`和`onLoad`作为依赖项来使用。

问题在于,在`UserPage`组件中,每次重新渲染时`filters`对象都会被重新创建。虽然它的值看起来没有变化,但实际上每次重绘时都是一个新的对象引用,因此`UserList`会将其视为一个不同的值,从而触发`useEffect`效应。

浏览器中不断显示“USER LIST”相关的日志信息。’ height="477" loading="lazy" src="https://cdn.hashnode.com/uploads/covers/629122ced97f80b5091d8058/c201dff7-3e6b-4648-8caf-9323057df3df.gif" style="display:block;margin:0 auto" width="800"/></p>
<p>将`filters`对象包装在`useMemo`中确实可以阻止无限循环重绘,但这并没有解决根本问题。实际上,`useMemo`并不是为了解决这种问题而设计的。有一些更好的方法可以解决这个问题:</p>
<p>第一种方法是,在依赖项数组中使用原始类型而不是对象。因为对象是通过引用进行比较的,所以每次重新渲染时,`useEffect`都会看到不同的对象引用;而原始类型则是通过值来进行比较的,因此不会导致这种问题。</p>
<pre><code class="language-typescript">function UserList({ filters, onLoad, users }: UserListProps) {
  console.count "__ 用户列表 __");

  useEffect(() => {
    fetchUsers(filters).then((data) => {
      onLoad(data);
    });
  }, [filters.active, filters.role]); // 原始类型按值进行比较,而非引用

  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
</code></pre>
<p>第二种方法是在组件外部定义这个对象,这样它就能拥有一个稳定的引用。</p>
<pre><code class="language-typescript">const filters = {
  role: "admin",
  active: true,
};

const UserPage = () => {
  const [userData, setUserData] = useState:<User[]&gt>];

  return <UserList filters={filters} onLoad={setUserData} users={userData} />;
};
</code></pre>
<p>如果这个对象是动态变化的,那么第三种解决方案就是将其存储在状态中。</p>
<pre><code class="language-typescript">const UserPage = () => {
  const [userData, setUserData] = useState)<User[]&gt>;
  const [filters, setFilters] = useState({
    role: "admin",
    active: true,
  });

  return <UserList filters={filters} onLoad={setUserData} users={userData} />;
};
</code></pre>
<h2 id="heading-when-to-use-usecallback-and-usememo">何时使用<code>useCallback</code>和<code>useMemo</code></h2>
<p>本文的目的并不是告诉你们永远不要使用这些钩子。在某些情况下,这些钩子确实能够发挥重要的作用。</p>
<h3 id="heading-measure-before-you-optimize">优化之前先进行评估</h3>
<p>在任何优化操作之前,无论是使用<code>useCallback</code>、<code>useMemo</code>还是重新构建组件结构,你都应该首先确认确实存在性能问题。对那些并不需要优化的代码进行优化,对任何人来说都没有好处。</p>
<p>React DevTools提供了一个“分析器”选项卡,你可以使用它来记录开发过程中的操作过程,从而准确了解哪些组件会被重新渲染、它们被重新渲染的频率以及每次渲染所花费的时间。建议你阅读<a href="https://www.freecodecamp.org/news/how-to-use-react-devtools/">《如何使用React开发者工具——结合实例进行讲解》</a>这篇文档。如果你更喜欢观看视频教程,也可以看看Ben是如何演示<a href="https://www.youtube.com/watch?v=00RoZflFE34">如何利用React分析器来查找并解决性能问题</a>的。</p>
<h3 id="heading-stabilize-references-for-reactmemo-children">为<code>React.memo</code>的子组件稳定引用</h3>
<p><code>React.memo</code>能够防止组件在属性没有发生变化的情况下被重新渲染。但是,如果你传递的是一个函数或对象作为属性,那么每当父组件被重新渲染时,这个子组件也会被重新渲染,因为函数和对象每次都会被创建成新的实例。</p>
<p>以下就是适合使用<code>useCallback</code>或<code>useMemo</code>的情况:</p>
<pre><code class="language-typescript">const Child = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log("子组件被渲染");
  return <button onClick={onClick}&gt>点击我</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log("被点击了");
  }, []);

  return (
    <><>
      <button onClick={() => setCount((c) => c + 1)}&gt>计数:{count}</button>
      <Child onClick={handleClick}。</Child>
    </>>
  );
}
</code></pre>
<p>如果没有使用<code>useCallback</code>,那么每当<code>count</code>的值发生变化时,<code>Child</code>组件就会重新渲染,尽管<code>handleClick</code>函数与<code>count</code>毫无关联。而使用了<code>useCallback</code>之后,该函数的引用地址就会保持不变。</p>
<p>通常来说,将<code>useCallback</code>与<code>React.memo</code>一起使用会是最佳选择。不过,如果组件的属性是基本类型或者其值本身是稳定的,那么单独使用<code>React.memo</code>也同样有效。此外,在某些情况下,<code>useCallback</code>也可以在不需要使用<code>React.memo</code>的时候发挥作用,比如在将稳定的回调函数传递给效果函数、自定义钩子或第三方组件时。</p>
<h2 id="heading-conclusion">结论</h2>
<p><code>useCallback</code>与<code>useMemo</code>确实是很有用的记忆化工具,但它们并不能自动提升性能。每次调用这些函数都会增加内存开销,并且在每次渲染时都会进行依赖项的比较。</p>
<p>在设计组件时,应尽量减少优化的需求。应将状态和逻辑尽可能地放在那些需要使用它们的组件附近。只有当确实发现渲染过程存在问题时,才应该考虑使用<code>useCallback</code>、<code>useMemo</code>以及<code>React.memo</code>。</p>
<div class="crp_related "><h3>Related Posts:</h3><ul><li><a href="http://www.cheeli.com.cn/articles/how-to-design-a-type-safe-lazy-and-secure-plugin-architecture-in-react/"    ><img  width="150" height="150"  src="https://cdn.hashnode.com/uploads/covers/6979762ba2442d262dacf388/5b7ef89a-62ae-4ea0-97f5-34a2423650d3.png" class="crp_thumb crp_correctfirst" alt="如何在 React 中设计一种类型安全、高效且安全的插件架构" title="如何在 React 中设计一种类型安全、高效且安全的插件架构" /><span class="crp_title">如何在 React 中设计一种类型安全、高效且安全的插件架构</span></a></li><li><a href="http://www.cheeli.com.cn/articles/how-to-debug-react-state-updates-like-a-pro-without-polluting-production/"    ><img  width="150" height="150"  src="http://www.cheeli.com.cn/wp-content/plugins/contextual-related-posts/default.png" class="crp_thumb crp_default" alt="如何像专业人士一样调试 React 中的状态更新,同时避免影响生产环境的正常运行" title="如何像专业人士一样调试 React 中的状态更新,同时避免影响生产环境的正常运行" /><span class="crp_title">如何像专业人士一样调试 React 中的状态更新,同时避免影响生产环境的正常运行</span></a></li><li><a href="http://www.cheeli.com.cn/articles/the-modern-react-data-fetching-handbook-suspense-use-and-errorboundary-explained/"    ><img  width="150" height="150"  src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770089281941/9a0dbc5d-6b45-4813-9e62-8bc4cbad82ea.png" class="crp_thumb crp_correctfirst" alt="《现代 React 数据获取手册:悬停效果、使用方法以及错误处理机制详解》" title="《现代 React 数据获取手册:悬停效果、使用方法以及错误处理机制详解》" /><span class="crp_title">《现代 React 数据获取手册:悬停效果、使用方法以及错误处理机制详解》</span></a></li><li><a href="http://www.cheeli.com.cn/articles/warper-rust-powered-react-virtualisation-library/"    ><img  width="150" height="150"  src="http://www.cheeli.com.cn/wp-content/plugins/contextual-related-posts/default.png" class="crp_thumb crp_default" alt="Warper:由 Rust 编写、用于 React 虚拟化的库" title="Warper:由 Rust 编写、用于 React 虚拟化的库" /><span class="crp_title">Warper:由 Rust 编写、用于 React 虚拟化的库</span></a></li><li><a href="http://www.cheeli.com.cn/articles/how-to-build-a-full-stack-crud-app-with-react-aws-lambda-dynamodb-and-cognito-auth/"    ><img  width="150" height="150"  src="https://cdn.hashnode.com/uploads/covers/62d53ab5bc2c7a1dc672b04f/70486bdc-f272-45db-be30-f10752916546.png" class="crp_thumb crp_correctfirst" alt="如何使用 React、AWS Lambda、DynamoDB 以及 Cognito Auth 来构建一个功能完备的 CRUD 应用程序" title="如何使用 React、AWS Lambda、DynamoDB 以及 Cognito Auth 来构建一个功能完备的 CRUD 应用程序" /><span class="crp_title">如何使用 React、AWS Lambda、DynamoDB 以及 Cognito Auth…</span></a></li><li><a href="http://www.cheeli.com.cn/articles/how-to-build-responsive-and-accessible-ui-designs-with-react-and-semantic-html/"    ><img  width="150" height="150"  src="http://www.cheeli.com.cn/wp-content/plugins/contextual-related-posts/default.png" class="crp_thumb crp_default" alt="如何使用 React 和语义 HTML 来构建响应式且易于访问的用户界面设计" title="如何使用 React 和语义 HTML 来构建响应式且易于访问的用户界面设计" /><span class="crp_title">如何使用 React 和语义 HTML 来构建响应式且易于访问的用户界面设计</span></a></li></ul><div class="crp_clear"></div></div>                </div>
				<div class="entry-meta meta-tags">Tags: <span class="tag-links"><a href="http://www.cheeli.com.cn/articles/tag/%e4%bb%a3%e7%a0%81%e3%80%81%e7%bb%84%e4%bb%b6/" rel="tag">代码、组件</a></span></div>
</div>
			</div>
            
  <section id="comments">
	  <div class="comments-closed">
		  Comments are closed.	  </div>
  </section><!-- /#comments -->

        		
     </div><!-- /.col-md-12 -->                  	
    </div><!-- /.row -->

			</section>

	</div><!-- /.main -->    
         <aside class="sidebar side-right col-sm-4  th-widget-area" role="complementary">
	<section class="widget search-2 widget_search"><div class="widget-inner"><h3 class="widget-title">搜索</h3><form role="search" method="get" class="search-form form-inline" action="http://www.cheeli.com.cn/">
  <div class="input-group">
    <input type="search" value="" name="s" class="search-field form-control" placeholder="Search 上海软件外包公司-知力科技">
    <label class="hide">Search for:</label>
    <span class="input-group-btn">
      <button type="submit" class="search-submit btn btn-default">Search</button>
    </span>
  </div>
</form></div></section><section class="widget categories-2 widget_categories"><div class="widget-inner"><h3 class="widget-title">分类</h3>		<ul>
	<li class="cat-item cat-item-425"><a href="http://www.cheeli.com.cn/articles/category/iot/" >IoT</a>
</li>
	<li class="cat-item cat-item-64"><a href="http://www.cheeli.com.cn/articles/category/expertpoint/" >专家观点</a>
</li>
	<li class="cat-item cat-item-441"><a href="http://www.cheeli.com.cn/articles/category/%e4%ba%ba%e5%b7%a5%e6%99%ba%e8%83%bd/" >人工智能</a>
</li>
	<li class="cat-item cat-item-117"><a href="http://www.cheeli.com.cn/articles/category/%e5%a4%a7%e6%95%b0%e6%8d%ae/" >大数据</a>
</li>
	<li class="cat-item cat-item-476"><a href="http://www.cheeli.com.cn/articles/category/%e5%ae%89%e5%85%a8/" >安全</a>
</li>
	<li class="cat-item cat-item-495"><a href="http://www.cheeli.com.cn/articles/category/%e6%93%8d%e4%bd%9c%e7%b3%bb%e7%bb%9f/" >操作系统</a>
</li>
	<li class="cat-item cat-item-426"><a href="http://www.cheeli.com.cn/articles/category/%e6%95%85%e9%9a%9c%e9%a2%84%e8%ad%a6/" >故障预警</a>
</li>
	<li class="cat-item cat-item-469"><a href="http://www.cheeli.com.cn/articles/category/%e6%95%b0%e6%8d%ae%e5%ba%93/" >数据库</a>
</li>
	<li class="cat-item cat-item-477"><a href="http://www.cheeli.com.cn/articles/category/%e6%9e%b6%e6%9e%84/" >架构</a>
</li>
	<li class="cat-item cat-item-48"><a href="http://www.cheeli.com.cn/articles/category/hotnews/" >热点关注</a>
</li>
	<li class="cat-item cat-item-143"><a href="http://www.cheeli.com.cn/articles/category/%e7%a7%bb%e5%8a%a8/" >移动</a>
</li>
	<li class="cat-item cat-item-82"><a href="http://www.cheeli.com.cn/articles/category/%e8%bd%af%e4%bb%b6%e5%bc%80%e5%8f%91/" >软件开发</a>
</li>
	<li class="cat-item cat-item-1"><a href="http://www.cheeli.com.cn/articles/category/uncategorized/" >默认</a>
</li>
		</ul>
</div></section><section class="widget tag_cloud-4 widget_tag_cloud"><div class="widget-inner"><h3 class="widget-title">标签</h3><div class="tagcloud"><a href="http://www.cheeli.com.cn/articles/tag/ai/" class="tag-cloud-link tag-link-65 tag-link-position-1" style="font-size: 14.808219178082pt;" aria-label="AI (81个项目)">AI</a>
<a href="http://www.cheeli.com.cn/articles/tag/blank/" class="tag-cloud-link tag-link-193 tag-link-position-2" style="font-size: 9.5342465753425pt;" aria-label="blank (22个项目)">blank</a>
<a href="http://www.cheeli.com.cn/articles/tag/code/" class="tag-cloud-link tag-link-119 tag-link-position-3" style="font-size: 17.876712328767pt;" aria-label="code (169个项目)">code</a>
<a href="http://www.cheeli.com.cn/articles/tag/data/" class="tag-cloud-link tag-link-130 tag-link-position-4" style="font-size: 18.164383561644pt;" aria-label="data (183个项目)">data</a>
<a href="http://www.cheeli.com.cn/articles/tag/em/" class="tag-cloud-link tag-link-149 tag-link-position-5" style="font-size: 11.452054794521pt;" aria-label="em (35个项目)">em</a>
<a href="http://www.cheeli.com.cn/articles/tag/href/" class="tag-cloud-link tag-link-137 tag-link-position-6" style="font-size: 10.205479452055pt;" aria-label="href (26个项目)">href</a>
<a href="http://www.cheeli.com.cn/articles/tag/iot/" class="tag-cloud-link tag-link-513 tag-link-position-7" style="font-size: 10.397260273973pt;" aria-label="iot (27个项目)">iot</a>
<a href="http://www.cheeli.com.cn/articles/tag/java/" class="tag-cloud-link tag-link-100 tag-link-position-8" style="font-size: 8.958904109589pt;" aria-label="JAVA (19个项目)">JAVA</a>
<a href="http://www.cheeli.com.cn/articles/tag/kafka/" class="tag-cloud-link tag-link-237 tag-link-position-9" style="font-size: 10.205479452055pt;" aria-label="kafka (26个项目)">kafka</a>
<a href="http://www.cheeli.com.cn/articles/tag/li/" class="tag-cloud-link tag-link-225 tag-link-position-10" style="font-size: 14.808219178082pt;" aria-label="li (81个项目)">li</a>
<a href="http://www.cheeli.com.cn/articles/tag/line/" class="tag-cloud-link tag-link-180 tag-link-position-11" style="font-size: 8.2876712328767pt;" aria-label="line (16个项目)">line</a>
<a href="http://www.cheeli.com.cn/articles/tag/ltr/" class="tag-cloud-link tag-link-956 tag-link-position-12" style="font-size: 10.684931506849pt;" aria-label="ltr (29个项目)">ltr</a>
<a href="http://www.cheeli.com.cn/articles/tag/nbsp/" class="tag-cloud-link tag-link-439 tag-link-position-13" style="font-size: 11.260273972603pt;" aria-label="nbsp (34个项目)">nbsp</a>
<a href="http://www.cheeli.com.cn/articles/tag/python/" class="tag-cloud-link tag-link-133 tag-link-position-14" style="font-size: 9.7260273972603pt;" aria-label="python (23个项目)">python</a>
<a href="http://www.cheeli.com.cn/articles/tag/span/" class="tag-cloud-link tag-link-197 tag-link-position-15" style="font-size: 15.958904109589pt;" aria-label="span (107个项目)">span</a>
<a href="http://www.cheeli.com.cn/articles/tag/strong/" class="tag-cloud-link tag-link-116 tag-link-position-16" style="font-size: 20.753424657534pt;" aria-label="strong (340个项目)">strong</a>
<a href="http://www.cheeli.com.cn/articles/tag/variable/" class="tag-cloud-link tag-link-922 tag-link-position-17" style="font-size: 10.493150684932pt;" aria-label="variable (28个项目)">variable</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%b8%9a%e5%8a%a1/" class="tag-cloud-link tag-link-287 tag-link-position-18" style="font-size: 9.5342465753425pt;" aria-label="业务 (22个项目)">业务</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%b8%ad/" class="tag-cloud-link tag-link-178 tag-link-position-19" style="font-size: 12.219178082192pt;" aria-label="中 (43个项目)">中</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%ba%91/" class="tag-cloud-link tag-link-472 tag-link-position-20" style="font-size: 9.7260273972603pt;" aria-label="云 (23个项目)">云</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%ba%ba%e5%b7%a5%e6%99%ba%e8%83%bd/" class="tag-cloud-link tag-link-72 tag-link-position-21" style="font-size: 12.315068493151pt;" aria-label="人工智能 (44个项目)">人工智能</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%bb%a3%e7%a0%81/" class="tag-cloud-link tag-link-103 tag-link-position-22" style="font-size: 11.452054794521pt;" aria-label="代码 (35个项目)">代码</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%bb%a3%e7%a0%81%ef%bc%8c%e4%bc%9a%e7%9a%84%e3%80%82/" class="tag-cloud-link tag-link-1247 tag-link-position-23" style="font-size: 10.397260273973pt;" aria-label="代码,会的。 (27个项目)">代码,会的。</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%bc%81%e4%b8%9a/" class="tag-cloud-link tag-link-342 tag-link-position-24" style="font-size: 9.7260273972603pt;" aria-label="企业 (23个项目)">企业</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%bc%9a/" class="tag-cloud-link tag-link-227 tag-link-position-25" style="font-size: 12.027397260274pt;" aria-label="会 (41个项目)">会</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e4%bd%bf%e7%94%a8/" class="tag-cloud-link tag-link-156 tag-link-position-26" style="font-size: 12.794520547945pt;" aria-label="使用 (49个项目)">使用</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e5%85%ac%e5%8f%b8/" class="tag-cloud-link tag-link-210 tag-link-position-27" style="font-size: 9.1506849315068pt;" aria-label="公司 (20个项目)">公司</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e5%88%86%e6%9e%90/" class="tag-cloud-link tag-link-399 tag-link-position-28" style="font-size: 8.5753424657534pt;" aria-label="分析 (17个项目)">分析</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e5%9b%a2%e9%98%9f/" class="tag-cloud-link tag-link-164 tag-link-position-29" style="font-size: 8.7671232876712pt;" aria-label="团队 (18个项目)">团队</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e5%ad%a6%e4%b9%a0/" class="tag-cloud-link tag-link-566 tag-link-position-30" style="font-size: 8.5753424657534pt;" aria-label="学习 (17个项目)">学习</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e5%ae%89%e5%85%a8/" class="tag-cloud-link tag-link-462 tag-link-position-31" style="font-size: 11.739726027397pt;" aria-label="安全 (38个项目)">安全</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e5%b7%a5%e5%85%b7/" class="tag-cloud-link tag-link-422 tag-link-position-32" style="font-size: 8.2876712328767pt;" aria-label="工具 (16个项目)">工具</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%8a%80%e6%9c%af/" class="tag-cloud-link tag-link-78 tag-link-position-33" style="font-size: 11.931506849315pt;" aria-label="技术 (40个项目)">技术</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%95%b0%e6%8d%ae/" class="tag-cloud-link tag-link-87 tag-link-position-34" style="font-size: 22pt;" aria-label="数据 (460个项目)">数据</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%95%b0%e6%8d%ae%e5%ba%93/" class="tag-cloud-link tag-link-298 tag-link-position-35" style="font-size: 14.328767123288pt;" aria-label="数据库 (71个项目)">数据库</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%9c%8d%e5%8a%a1/" class="tag-cloud-link tag-link-194 tag-link-position-36" style="font-size: 12.123287671233pt;" aria-label="服务 (42个项目)">服务</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%9f%a5%e8%af%a2/" class="tag-cloud-link tag-link-115 tag-link-position-37" style="font-size: 8pt;" aria-label="查询 (15个项目)">查询</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%a8%a1%e5%9e%8b/" class="tag-cloud-link tag-link-123 tag-link-position-38" style="font-size: 13.657534246575pt;" aria-label="模型 (61个项目)">模型</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e6%b5%8b%e8%af%95/" class="tag-cloud-link tag-link-226 tag-link-position-39" style="font-size: 9.3424657534247pt;" aria-label="测试 (21个项目)">测试</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e7%94%a8%e6%88%b7/" class="tag-cloud-link tag-link-126 tag-link-position-40" style="font-size: 11.643835616438pt;" aria-label="用户 (37个项目)">用户</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e7%b3%bb%e7%bb%9f/" class="tag-cloud-link tag-link-202 tag-link-position-41" style="font-size: 11.931506849315pt;" aria-label="系统 (40个项目)">系统</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e7%bc%93%e5%ad%98/" class="tag-cloud-link tag-link-335 tag-link-position-42" style="font-size: 8.5753424657534pt;" aria-label="缓存 (17个项目)">缓存</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e7%bd%91%e7%bb%9c/" class="tag-cloud-link tag-link-311 tag-link-position-43" style="font-size: 8.7671232876712pt;" aria-label="网络 (18个项目)">网络</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e8%ae%be%e5%a4%87/" class="tag-cloud-link tag-link-451 tag-link-position-44" style="font-size: 10.397260273973pt;" aria-label="设备 (27个项目)">设备</a>
<a href="http://www.cheeli.com.cn/articles/tag/%e9%97%ae%e9%a2%98/" class="tag-cloud-link tag-link-247 tag-link-position-45" style="font-size: 9.1506849315068pt;" aria-label="问题 (20个项目)">问题</a></div>
</div></section>		<section class="widget recent-posts-2 widget_recent_entries"><div class="widget-inner">		<h3 class="widget-title">最新发布</h3>		<ul>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-use-dartexceptor-a-lighter-way-to-handle-errors-in-dart-3/">如何使用 DartExceptor:在 Dart 3 中处理错误的一种更简便的方法</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-schedule-jobs-in-postgresql-with-pg_cron/">如何在 PostgreSQL 中使用 pg_cron 来安排作业的执行</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-build-a-production-architecture-for-small-language-model-fleets/">如何为小型语言模型团队构建生产环境架构</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-avoid-overusing-usecallback-and-usememo-in-react/">如何在 React 中避免过度使用 useCallback 和 useMemo</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-handle-small-context-window-limits-in-rag-systems/">如何在RAG系统中应对小上下文窗口的限制</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/ai-paper-review-chain-of-thought-prompting-elicits-reasoning-in-large-language-models/">AI论文评审:通过引导思维链来激发大型语言模型中的推理能力</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-build-a-production-safe-agent-loop-from-exit-conditions-to-audit-trails/">如何构建一个能够确保生产环境安全运行的代理循环系统:从退出条件到审计追踪机制</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/the-2026-finops-roadmap-from-cost-blind-engineer-to-cloud-financial-manager/">2026年财务运营路线图:从盲目追求成本的工程师到具备云财务管理能力的专家</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/build-a-self-healing-ci-cd-pipeline-with-ai/">利用人工智能构建能够自我修复的持续集成/持续交付管道</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-run-private-text-to-speech-on-your-own-hardware-using-qvac/">如何使用 QVAC 在自己的硬件上运行私有的文本转语音系统</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/building-a-website-in-2026-what-matters-more-than-your-tech-stack/">在2026年搭建网站:除了技术选型之外,还有什么更重要的因素?</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/geopolitical-risk-isnt-one-thing-i-built-a-python-framework-to-prove-it/">地缘政治风险并非单一的概念——我开发了一个Python框架来证明这一点。</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-to-build-a-browser-based-pdf-crop-tool-using-javascript/">如何使用JavaScript构建基于浏览器的PDF裁剪工具</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/the-saga-pattern-in-node-js-how-to-roll-back-distributed-transactions-across-microservices/">Node.js中的 Saga模式:如何实现跨微服务的分布式事务回滚</a>
									</li>
											<li>
					<a href="http://www.cheeli.com.cn/articles/how-large-scale-platforms-handle-millions-of-daily-transactions/">大规模平台是如何处理每天数以百万计的交易量的?</a>
									</li>
					</ul>
		</div></section>
        		<section class="widget widget-social">
    		<div class="widget-inner">
        		                <h3 class="widget-title">分享至</h3>
                        			<div class="soc-widget">
        			<a target=_blank  href=