目 录CONTENT

文章目录

React(十六)自定义Hooks

React(十六)自定义 Hooks 完全指南

目录

  1. 为什么需要自定义 Hooks?
  2. 自定义 Hooks 的定义规范
  3. 基础使用实战:三个递进式示例
    • 3.1 useToggle:开关状态切换
    • 3.2 useLocalStorageState:持久化存储状态
    • 3.3 useFetch:通用数据请求
  4. React 19 专属注意事项
  5. 通用核心注意事项
  6. 常见误区解析:10 个高频问题
  7. 课堂练习
  8. 总结

1. 为什么需要自定义 Hooks?

1.1 生活中类比:通用充电头

想象你买了一部手机、一部平板、一个蓝牙耳机。它们都需要充电,但每款的充电线接口可能不同。于是厂家发明了通用充电头:Type-C、Lightning、Micro-USB 都能插,你不需要为每个设备单独设计一套充电逻辑。

在 React 里,自定义 Hooks 就像这个通用充电头

每个组件都可能需要:

  • 管理开关状态(显示/隐藏弹窗、展开/折叠菜单);
  • 把状态保存到 localStorage
  • 发起接口请求并处理 loading 和 error;
  • 监听窗口大小变化;
  • 监听鼠标位置、键盘事件。

如果每个组件都自己写一遍,代码会大量重复。自定义 Hooks 就是把这些通用的状态逻辑抽出来,做成可以复用的函数。

1.2 Class 组件时代的逻辑复用痛点

在 React 16 之前,Class 组件复用状态逻辑主要有三种方式:

方案 优点 缺点
mixin 简单直接 命名冲突、来源不清晰、难以维护
高阶组件(HOC) 可以包裹组件 嵌套地狱、props 名称冲突、调试困难
render props 灵活性高 代码嵌套层数多、可读性差

这些方案都有同一个问题:逻辑复用不是真正地把逻辑抽出来,而是在组件结构上层层包装

1.3 自定义 Hooks 是 React 19 推荐的标准方案

React Hooks 出现之后,React 官方推荐用自定义 Hooks来复用状态逻辑:

  • 它是真正抽离逻辑,不改变组件结构;
  • 它是函数式、可组合的,多个 Hook 可以像搭积木一样组合;
  • 它是React 19 官方认可的唯一标准复用方案

一句话:自定义 Hooks 让你把"组件里重复的状态逻辑"变成一个可复用的函数,谁想用就调用它。


2. 自定义 Hooks 的定义规范

2.1 命名规则:必须以 use 开头

React 19 对自定义 Hooks 的命名有强制要求

  • 必须以 use 开头;
  • 后面接大驼峰命名的功能名;
  • React 会凭借这个前缀识别它是不是 Hook,从而启用相关规则检查。

合法命名示例

useToggle;
useLocalStorageState;
useFetch;
useWindowResize;
useMousePosition;

非法命名示例

命名 问题
localStorageState 缺少 use 前缀,React 不会识别为 Hook
useLocalstorageState 第二个单词首字母小写,不符合大驼峰规范
use_local_storage 使用下划线,不符合 JavaScript 命名习惯
useToggle ✅ 正确

如果你的自定义 Hook 不叫 use 开头,React 的 ESLint 插件不会把它当作 Hook 检查,Hook 调用规则(比如不能放在 if 里)就可能失效。

2.2 核心定义原则:只能在 Hook 或组件里调用其他 Hook

自定义 Hook 内部可以调用:

  • React 原生 Hook(如 useStateuseEffectuseContextuseRefuseMemouseCallback);
  • React 19 新 Hook(如 useuseTransitionuseActionStateuseOptimistic);
  • 其他自定义 Hook。

禁止在普通函数、类组件、事件处理函数、循环、条件判断中调用 Hook。

2.3 为什么调用顺序必须固定?

React 内部维护了一个调用顺序链表。每次组件渲染时,React 会按顺序记录每个 Hook 的状态。

如果你在 iffor 里调用 Hook,React 就无法保证每次渲染的 Hook 顺序一致,状态就会错位。

2.4 标准语法结构模板

// 自定义 Hook 标准模板
import { useState, useEffect } from "react";

// 1. 函数名以 use 开头,后面大驼峰
function useCustomHook(initialValue) {
  // 2. 内部可以调用其他 Hook
  const [state, setState] = useState(initialValue);

  useEffect(() => {
    // 副作用逻辑
  }, []);

  // 3. 返回组件需要的状态、函数、计算值
  return [state, setState];
}

export default useCustomHook;

和普通函数的本质区别

特性 普通函数 自定义 Hook
能否调用 React Hook ❌ 不能 ✅ 能
命名要求 无强制要求 必须以 use 开头
调用位置限制 任意位置 只能在组件顶层或 Hook 顶层
是否有内部状态 没有 通过 Hook 拥有独立状态

3. 基础使用实战:三个递进式示例

3.1 useToggle:开关状态切换 Hook

场景

很多组件都需要一个布尔值开关:

  • 弹窗的显示/隐藏;
  • 菜单的展开/折叠;
  • 主题的深色/浅色切换。

自定义 Hook 代码

import { useState, useCallback } from "react";

/**
 * useToggle:通用的布尔值开关 Hook
 * @param {boolean} defaultValue - 默认值,默认为 false
 * @returns {[boolean, { toggle, setTrue, setFalse }]} - 当前状态 + 操作函数
 */
function useToggle(defaultValue = false) {
  // 1. 内部使用 useState 管理布尔值
  const [value, setValue] = useState(defaultValue);

  // 2. 用 useCallback 缓存函数,避免每次渲染都创建新函数
  const toggle = useCallback(() => {
    setValue((prev) => !prev);
  }, []);

  const setTrue = useCallback(() => {
    setValue(true);
  }, []);

  const setFalse = useCallback(() => {
    setValue(false);
  }, []);

  // 3. 返回状态和操作函数
  return [value, { toggle, setTrue, setFalse }];
}

export default useToggle;

在两个组件中复用

import useToggle from "./useToggle";

function ModalDemo() {
  // 组件 A 使用 useToggle,拥有自己的状态
  const [isVisible, { toggle, setTrue, setFalse }] = useToggle(false);

  return (
    <div>
      <button onClick={setTrue}>打开弹窗</button>
      {isVisible && (
        <div className="modal">
          <p>这是一个弹窗</p>
          <button onClick={setFalse}>关闭</button>
          <button onClick={toggle}>切换</button>
        </div>
      )}
    </div>
  );
}

function MenuDemo() {
  // 组件 B 也使用 useToggle,状态完全独立
  const [isOpen, { toggle }] = useToggle(true);

  return (
    <div>
      <button onClick={toggle}>{isOpen ? "折叠菜单" : "展开菜单"}</button>
      {isOpen && (
        <ul>
          <li>菜单项</li>
        </ul>
      )}
    </div>
  );
}

function App() {
  return (
    <div>
      <ModalDemo />
      <MenuDemo />
    </div>
  );
}

export default App;

代码运行效果

  • 点击"打开弹窗",弹窗显示;点击"关闭",弹窗消失。
  • 点击"折叠菜单",菜单收起;再次点击,菜单展开。
  • 两个组件互不影响,各自管理自己的开关状态。

抽离前后代码量对比

方式 每个组件需要写的代码
不抽离 useState(false) + 自己写 toggle / setTrue / setFalse
抽离成 useToggle 一行 useToggle(false) 即可

3.2 useLocalStorageState:持久化存储状态 Hook

场景

用户偏好设置(比如主题、语言、字号)需要在页面刷新后依然保留。localStorage 是浏览器提供的本地存储方案,自定义 Hook 可以把它和 React 状态整合起来。

自定义 Hook 代码

import { useState, useEffect, useCallback } from "react";

/**
 * useLocalStorageState:状态与 localStorage 双向同步的 Hook
 * @param {string} key - 存储在 localStorage 中的键名
 * @param {*} defaultValue - 默认值
 * @returns {[any, Function]} - 当前值 + 设置值函数
 */
function useLocalStorageState(key, defaultValue) {
  // 1. 初始化状态:尝试从 localStorage 读取,读取失败则使用默认值
  const [value, setValue] = useState(() => {
    try {
      const storedValue = window.localStorage.getItem(key);
      // 如果有存储值,解析 JSON;否则返回默认值
      return storedValue ? JSON.parse(storedValue) : defaultValue;
    } catch (error) {
      // 捕获解析异常(比如存储值被破坏)
      console.error(`读取 localStorage 失败,key: ${key}`, error);
      return defaultValue;
    }
  });

  // 2. 设置值时同步更新 localStorage
  const setStoredValue = useCallback(
    (newValue) => {
      try {
        // 如果传入的是函数,先执行函数拿到新值
        const valueToStore =
          newValue instanceof Function ? newValue(value) : newValue;

        setValue(valueToStore);
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      } catch (error) {
        console.error(`写入 localStorage 失败,key: ${key}`, error);
      }
    },
    [key, value],
  );

  // 3. 监听其他标签页或脚本修改 localStorage 时同步更新状态
  useEffect(() => {
    const handleStorageChange = (event) => {
      if (event.key === key) {
        try {
          setValue(event.newValue ? JSON.parse(event.newValue) : defaultValue);
        } catch (error) {
          console.error(`同步 localStorage 失败,key: ${key}`, error);
        }
      }
    };

    window.addEventListener("storage", handleStorageChange);

    return () => {
      window.removeEventListener("storage", handleStorageChange);
    };
  }, [key, defaultValue]);

  return [value, setStoredValue];
}

export default useLocalStorageState;

在用户偏好设置组件中使用

import useLocalStorageState from "./useLocalStorageState";

function ThemeSettings() {
  // 使用自定义 Hook,页面刷新后主题偏好自动恢复
  const [theme, setTheme] = useLocalStorageState("theme", "light");

  return (
    <div
      style={{
        background: theme === "dark" ? "#333" : "#fff",
        color: theme === "dark" ? "#fff" : "#333",
      }}
    >
      <p>当前主题:{theme}</p>
      <button onClick={() => setTheme("light")}>浅色</button>
      <button onClick={() => setTheme("dark")}>深色</button>
    </div>
  );
}

function LanguageSettings() {
  // 另一个组件也复用同一个 Hook,key 不同,互不干扰
  const [language, setLanguage] = useLocalStorageState("language", "zh");

  return (
    <div>
      <p>当前语言:{language}</p>
      <button onClick={() => setLanguage("zh")}>中文</button>
      <button onClick={() => setLanguage("en")}>英文</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <ThemeSettings />
      <LanguageSettings />
    </div>
  );
}

export default App;

代码运行效果

  • 选择"深色"主题,刷新页面后仍然是深色;
  • 切换语言为英文,刷新页面后仍然是英文;
  • 打开多个浏览器标签页,在一个标签页修改主题,另一个标签页也会同步(通过 storage 事件)。

JSON 序列化异常处理说明

localStorage 只能存储字符串,所以复杂数据需要用 JSON.stringify 转换。但实际使用中可能遇到:

  • 存储值被用户手动修改成非法 JSON;
  • 数据本身包含循环引用,无法序列化。

上面的代码通过 try/catch 捕获这些异常,确保应用不会因为 localStorage 出错而崩溃。


3.3 useFetch:通用数据请求 Hook

场景

几乎每个页面都需要请求接口:

  • 列表页获取数据列表;
  • 详情页获取单条数据;
  • 需要处理 loading、error、手动刷新。

自定义 Hook 代码(React 19 兼容)

import { useState, useEffect, useCallback, useTransition } from "react";

/**
 * useFetch:通用数据请求 Hook
 * @param {string} url - 请求地址
 * @param {object} options - fetch 配置(method、headers、body 等)
 * @returns {object} - { data, loading, error, refresh }
 */
function useFetch(url, options = {}) {
  // 1. 定义状态
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // 2. React 19 的 useTransition:让 loading 过渡更平滑,避免阻塞 UI
  const [isPending, startTransition] = useTransition();

  // 3. 请求逻辑封装在 useCallback 中,方便复用和刷新
  const fetchData = useCallback(
    async (signal) => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, { ...options, signal });

        if (!response.ok) {
          throw new Error(`请求失败,状态码:${response.status}`);
        }

        const result = await response.json();

        // 使用 startTransition 让状态更新有过渡效果
        startTransition(() => {
          setData(result);
        });
      } catch (err) {
        // 如果是主动取消请求,不设置为错误
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    },
    [url, options],
  );

  // 4. 手动刷新函数
  const refresh = useCallback(() => {
    fetchData();
  }, [fetchData]);

  // 5. 组件挂载或 url/options 变化时发起请求
  useEffect(() => {
    // 创建 AbortController,用于取消请求
    const controller = new AbortController();

    fetchData(controller.signal);

    // 组件卸载或依赖变化时取消请求,避免竞态问题
    return () => {
      controller.abort();
    };
  }, [fetchData]);

  return {
    data,
    loading,
    error,
    refresh,
    isPending, // React 19 过渡状态
  };
}

export default useFetch;

在列表页和详情页中复用

import useFetch from './useFetch';

// 列表页
function UserList() {
  const { data, loading, error, refresh } = useFetch(
    'https://jsonplaceholder.typicode.com/users'
  );

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了:{error}</button>;

  return (
    <div>
      <button onClick={refresh}>刷新列表</button>
      <ul>
        {data?.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

// 详情页
function UserDetail({ userId }) {
  const { data, loading, error } = useFetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );

  if (loading) return <p>加载中...</p>;
  if (error) return <p>出错了:{error}</p>;

  return (
    <div>
      <h1>{data?.name}</h1>
      <p>邮箱:{data?.email}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <UserList />
      <UserDetail userId={1} />
    </div>
  );
}

export default App;

代码运行效果

  • 页面加载时自动请求数据;
  • 请求中显示"加载中...";
  • 请求失败显示错误信息;
  • 点击"刷新列表"重新请求;
  • 组件卸载或地址变化时,旧请求会被取消,避免竞态。

竞态问题说明

什么是竞态?

比如你快速切换两个用户详情,A 请求发出后,B 请求也发出。如果 A 响应慢、B 响应快,最后 A 的响应可能会覆盖 B 的结果,导致页面显示错误。

解决方案:使用 AbortController

  • 每次发起请求时创建新的 AbortController
  • 组件卸载或依赖变化时,调用 controller.abort() 取消旧请求;
  • 被取消的请求会抛出 AbortError,我们忽略这种错误。

4. React 19 专属注意事项

4.1 React 19 中自定义 Hook 的自动批处理

在 React 18 中,多个 setState 同时调用会被自动批处理,只触发一次渲染。React 19 进一步强化了这一点,在更多场景下(包括事件处理函数、setTimeout、Promise 等)都会自动批处理。

自定义 Hook 内部如果有多个 setState,你不用担心重复渲染,React 19 会自动帮你合并。

4.2 React 19 的 use 新 Hook 对自定义 Hook 的影响

React 19 引入了新的 use Hook,用于读取 Promise、Context 等值。在自定义 Hook 中,你仍然可以沿用 useState + useEffect 的方式,但也可以结合 use 来简化异步逻辑。

// React 19 新特性:用 use 读取 Promise
import { use } from "react";

function useUserData(promise) {
  // use 会阻塞渲染,直到 Promise 完成
  const data = use(promise);
  return data;
}

注意:useawait 不同,它只能在组件或 Hook 中调用,且要求传入 Promise。

4.3 useTransition 在自定义 Hook 中的使用

React 19 中 useTransition 更稳定,适合在自定义 Hook 中处理非紧急状态更新:

import { useState, useTransition } from "react";

function useSlowData() {
  const [data, setData] = useState(null);
  const [isPending, startTransition] = useTransition();

  const updateData = (newData) => {
    startTransition(() => {
      setData(newData);
    });
  };

  return { data, isPending, updateData };
}

5. 通用核心注意事项

5.1 调用规则红线:只能在顶层调用

❌ 错误:在 if 中调用 Hook

function useWrongHook(value) {
  if (value) {
    // ❌ 错误:Hook 在条件中调用
    const [state, setState] = useState(0);
  }
  return state;
}

❌ 错误:在循环中调用 Hook

function useWrongHook(items) {
  // ❌ 错误:Hook 在循环中调用
  for (let i = 0; i < items.length; i++) {
    useEffect(() => {}, []);
  }
}

✅ 正确:始终在最顶层调用

function useRightHook(value) {
  // ✅ 正确:在 Hook 的最顶层调用
  const [state, setState] = useState(0);

  useEffect(() => {
    if (value) {
      setState(value);
    }
  }, [value]);

  return state;
}

5.2 状态隔离原则:每个组件独立

不同组件调用同一个自定义 Hook 时,它们的 state 是完全独立的。

import useToggle from "./useToggle";

function ComponentA() {
  const [on] = useToggle(false);
  return <p>A: {on ? "开" : "关"}</p>;
}

function ComponentB() {
  const [on] = useToggle(false);
  return <p>B: {on ? "开" : "关"}</p>;
}

A 和 B 的状态互不影响。React 把每个 Hook 调用和具体组件实例绑定,不会因为用了同一个 Hook 就共享状态。

5.3 依赖项处理:必须完整

自定义 Hook 内部使用 useEffect / useMemo / useCallback 时,依赖项必须写完整。

❌ 错误:依赖项遗漏

function useCountMessage(count) {
  useEffect(() => {
    console.log("count:", count);
  }, []); // ❌ 漏了 count
}

✅ 正确:补全依赖项

function useCountMessage(count) {
  useEffect(() => {
    console.log("count:", count);
  }, [count]); // ✅ 依赖项完整
}

推荐安装 eslint-plugin-react-hooks 并开启 react-hooks/exhaustive-deps 规则,自动检查依赖项是否遗漏。

5.4 命名规范严谨性

如果你写了一个函数不叫 use 开头,但内部调用了 Hook,React 不会报错,但 ESLint 不会按 Hook 规则检查它,容易导致错误。

// ❌ 不推荐:虽然能运行,但失去了 Hook 规则保护
function localStorageState(key) {
  const [value, setValue] = useState(() => {
    return localStorage.getItem(key);
  });
  return [value, setValue];
}

5.5 竞态问题处理:必须用 AbortController

数据请求类自定义 Hook 必须使用 AbortController 处理竞态。详细实现参考上面的 useFetch 示例。

5.6 TypeScript 类型安全

如果项目使用 TypeScript,自定义 Hook 应该加泛型约束。

useFetch 的 TypeScript 版本

import { useState, useEffect, useCallback } from "react";

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refresh: () => void;
}

function useFetch<T = any>(
  url: string,
  options?: RequestInit,
): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(
    async (signal?: AbortSignal) => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, { ...options, signal });
        if (!response.ok) {
          throw new Error(`请求失败:${response.status}`);
        }
        const result = (await response.json()) as T;
        setData(result);
      } catch (err: any) {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    },
    [url, options],
  );

  const refresh = useCallback(() => fetchData(), [fetchData]);

  useEffect(() => {
    const controller = new AbortController();
    fetchData(controller.signal);
    return () => controller.abort();
  }, [fetchData]);

  return { data, loading, error, refresh };
}

export default useFetch;

使用方式:

interface User {
  id: number;
  name: string;
  email: string;
}

const { data } = useFetch<User>("/api/user/1");
// data 的类型是 User | null

6. 常见误区解析:10 个高频问题

Q1:自定义 Hook 和普通函数到底有什么区别?

  • 自定义 Hook 可以调用 React 的 Hook;普通函数不能;
  • 自定义 Hook 必须以 use 开头;普通函数没有命名限制;
  • 自定义 Hook 内部有独立的状态(通过 useState 等实现);普通函数没有。

Q2:能不能在自定义 Hook 里调用第三方库的 Hook?

:可以。只要第三方库的 Hook 是合法的 React Hook(比如 useQuery from React Query),就可以在自定义 Hook 中调用。这也是常见的组合方式。

Q3:多个自定义 Hook 之间怎么传递状态?

:一个自定义 Hook 可以调用另一个自定义 Hook。状态在每个调用它的组件中独立存在。如果要跨组件共享状态,需要结合 useContext 或全局状态管理库。

Q4:自定义 Hook 会让不同组件共享状态吗?

:不会。每个组件调用自定义 Hook 时,React 都会为该组件创建独立的 Hook 状态。就像每个手机都插同一个充电头,但电池是各自独立的。

Q5:自定义 Hook 里面可以写 UI 吗?

:不可以。自定义 Hook 只能返回数据、函数、计算结果,不能直接返回 JSX。UI 应该放在组件里。

Q6:自定义 Hook 的参数可以是什么?

:任意类型。可以是初始值、配置对象、回调函数、Promise 等。但要确保参数在组件渲染过程中保持稳定,避免不必要的副作用执行。

Q7:为什么自定义 Hook 必须以 use 开头?

:React 和 ESLint 通过 use 前缀识别一个函数是不是 Hook,从而启用 Hook 规则检查。如果不用 use 开头,React 不会报错,但 lint 不会检查调用位置是否正确。

Q8:自定义 Hook 内部可以调用 useEffect 吗?

:可以,而且非常常见。但需要注意依赖项的完整性,以及副作用的清理函数(比如取消订阅、取消请求)。

Q9:自定义 Hook 有性能问题吗?

:本身不会带来性能问题。但如果内部大量使用 useEffect 且依赖项设置不当,可能导致无限渲染或重复请求。需要合理使用 useMemouseCallback

Q10:React 19 和 React 18 的自定义 Hook 有什么主要区别?

:自定义 Hook 的基本写法一致。React 19 增强了自动批处理、引入了 useuseActionState 等新 Hook,可以在自定义 Hook 中组合使用。


7. 课堂练习

练习 1:实现 useCounter 计数器 Hook

要求:

  • 支持加、减、重置功能;
  • 支持设置步长;
  • 支持最小值和最大值限制。

参考答案核心框架

import { useState, useCallback } from "react";

function useCounter(initialValue = 0, options = {}) {
  const { step = 1, min = -Infinity, max = Infinity } = options;
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => {
    setCount((prev) => Math.min(prev + step, max));
  }, [step, max]);

  const decrement = useCallback(() => {
    setCount((prev) => Math.max(prev - step, min));
  }, [step, min]);

  const reset = useCallback(() => {
    setCount(initialValue);
  }, [initialValue]);

  return { count, increment, decrement, reset };
}

export default useCounter;

练习 2:实现 useWindowResize 监听窗口尺寸 Hook

要求:

  • 监听浏览器窗口的 resize 事件;
  • 返回当前窗口的宽度和高度;
  • 组件卸载时自动移除事件监听。

参考答案核心框架

import { useState, useEffect } from "react";

function useWindowResize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return size;
}

export default useWindowResize;

8. 总结

要点 内容
命名 必须以 use 开头,后接大驼峰功能名
调用位置 只能在组件或自定义 Hook 的最顶层调用
内部可调用 React 原生 Hook、其他自定义 Hook、第三方 Hook
状态隔离 不同组件调用同一个 Hook,状态相互独立
依赖项 使用 useEffect/useMemo/useCallback 时必须完整
竞态处理 数据请求类 Hook 使用 AbortController
React 19 自动批处理更强,可结合 useuseTransition 等新 Hook

💡 核心原则:自定义 Hooks 是把组件中重复的状态逻辑抽出来,变成可复用的函数。它不改组件结构,只提供逻辑。用好自定义 Hooks,能让你的代码像搭积木一样清晰、可维护。

0
博主关闭了当前页面的评论