自定义Hooks设计(如何封装AI搜索组件?)
我们先来系统性地拆解不同场景下自定义 Hooks 的设计思路,并结合你提到的点,细化核心代码的每一行关键操作及其设计考量。这将帮助你建立对不同逻辑单元封装的感觉,并理解代码背后的"为什么"。
🧠 一、自定义 Hooks 设计核心原则回顾 (面试高频点)
在深入具体 Hook 之前,牢记这些贯穿始终的原则至关重要:
- 单一职责原则 (Single Responsibility Principle - SRP): 一个 Hook 只解决一个特定的问题或封装一个独立的逻辑单元。避免"瑞士军刀"式的 Hook。
- 为什么? 提高复用性、可测试性、可维护性。修改一个功能不会意外影响其他功能。
- 明确输入输出 (Clear Input & Output): Hook 的参数(输入)和返回值(输出)必须清晰、简洁、有描述性。
- 为什么? 使用者能快速理解如何调用这个 Hook 以及它能提供什么,降低认知负担。
- 命名规范 (Naming Convention): 必须以
use
开头(如useFetch
,useWindowSize
),采用驼峰命名法,准确描述功能。- 为什么? 这是 React 的约定,便于识别是 Hook,工具(如 ESLint 插件)也能据此检查规则。
- 性能优化意识 (Performance Awareness): 合理使用
useMemo
,useCallback
,useRef
避免不必要的计算和重渲染。谨慎处理依赖数组和副作用。- 为什么? Hook 在每次组件渲染时都会执行,不当设计会导致性能瓶颈。
- 副作用管理 (Side Effect Management): 使用
useEffect
(React) 或onMounted
/onUnmounted
(Vue) 等处理副作用(API 请求、事件监听、定时器),并确保清理 (return
清理函数)。- 为什么? 防止内存泄漏、无效更新和状态不一致。
- 遵循 Hooks 规则 (Rules of Hooks): 只在函数组件/其他 Hook 的最顶层调用 Hook,不能在条件、循环或嵌套函数中调用。
- 为什么? React 依赖 Hook 的调用顺序来正确管理状态。违反规则会导致难以追踪的 bug。
🔧 二、不同场景 Hook 设计思路与代码深度拆解
我们选择四种最常见且具有代表性的场景,详细分析其设计思路并逐行解析核心代码。
🚀 1. 数据请求 Hook (e.g., useFetch
/ useAsync
)
-
设计思路:
- 输入 (Input): 请求的
url
(必选),可选的配置项(如method
,headers
,initialData
, 依赖项数组dependencies
用于触发重新请求)。 - 输出 (Output): 请求状态 (
loading
,error
,data
),可能包含一个手动触发重新请求的函数 (refetch
)。 - 核心逻辑: 使用
useState
管理data
,loading
,error
状态。使用useEffect
(依赖url
和可能的dependencies
)发起异步请求。在请求开始/成功/失败时更新状态。提供清理(如取消请求)选项。考虑防抖/节流(可选项,或由调用方控制依赖触发频率)。 - 关键考量点: 请求取消、避免竞态条件、缓存策略(可扩展)。
- 输入 (Input): 请求的
-
核心代码实现与逐行解析 (React 示例):
javascript
import { useState, useEffect } from 'react';
function useFetch(url, options = {}, dependencies = []) {
// 状态管理:清晰定义 Hook 管理的核心状态
const [data, setData] = useState(options.initialData || null);
const [loading, setLoading] = useState(true); // 初始状态为加载中
const [error, setError] = useState(null);
// 使用 ref 存储可取消的请求控制器 (AbortController)
const abortControllerRef = useRef(new AbortController());
// 核心副作用:执行数据获取
useEffect(() => {
// 1. 重置状态 (为新的请求做准备,避免旧数据残留)
setLoading(true);
setError(null);
// 2. 创建新的 AbortController 并存储到 ref (确保每次请求使用新的控制器)
const abortController = new AbortController();
abortControllerRef.current = abortController;
// 3. 定义异步请求函数 (封装请求逻辑)
const fetchData = async () => {
try {
// 4. 发起请求:传入 signal 用于取消
const signal = abortController.signal;
const response = await fetch(url, { ...options, signal });
// 5. 检查 HTTP 状态码 (非 2xx 响应也需要处理为错误)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 6. 解析响应数据
const result = await response.json();
// 7. 检查请求是否已被取消 (避免在已卸载的组件上设置状态)
if (!signal.aborted) {
setData(result);
}
} catch (err) {
// 8. 错误处理:仅处理非取消错误
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
// 9. 无论成功/失败/取消,只要未被取消,都结束加载状态
if (!abortController.signal.aborted) {
setLoading(false);
}
}
};
fetchData(); // 执行请求
// 10. 清理函数:组件卸载或依赖变化导致重新请求前,取消进行中的请求
return () => {
abortController.abort(); // 关键!取消当前请求
};
// 11. 依赖数组:url 和传入的 dependencies 变化时重新执行 effect
}, [url, ...dependencies]); // 依赖项明确
// 12. (可选) 提供手动重新请求的函数
const refetch = () => {
// 可以通过改变一个无关的状态 (如 forceUpdate) 或直接触发 useEffect 依赖变化实现。
// 更优雅的方式是利用依赖数组:这里假设调用 refetch 会改变 dependencies 中的某个值(由调用方控制)。
// 或者,可以在这里重置状态并重新执行 fetchData 逻辑,但需要处理好取消和状态重置。
console.warn('Manual refetch triggered. Implementation depends on how dependencies are managed.');
};
// 13. 明确输出:状态 + 可选操作
return { data, loading, error, refetch };
}
📝 2. 表单管理 Hook (e.g., useForm
/ useFormState
)
-
设计思路:
- 输入 (Input): 表单的初始值 (
initialValues
),可选的验证规则 (validationSchema
或validate
函数)。 - 输出 (Output): 表单数据对象 (
values
),表单字段变更处理器 (handleChange
),表单提交处理器 (handleSubmit
),重置表单函数 (resetForm
),错误状态对象 (errors
),表单整体状态 (isValid
,isSubmitting
等)。 - 核心逻辑: 使用
useState
或useReducer
(适合复杂表单) 管理整个表单的values
和errors
状态。提供通用的handleChange
函数(通常利用name
属性)更新对应字段的值。在提交时执行验证(同步或异步)并处理提交逻辑。提供重置功能。 - 关键考量点: 性能(避免大型表单频繁更新导致的性能问题)、验证策略(同步/异步、何时触发)、与 UI 库集成、嵌套字段处理。
- 输入 (Input): 表单的初始值 (
-
核心代码实现与逐行解析 (React useState 基础版):
javascript
import { useState, useCallback } from 'react';
function useForm(initialValues = {}, validate) {
// 1. 状态管理:表单值 (values) 和错误信息 (errors)
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
// 2. 通用字段变更处理器:利用字段的 `name` 属性进行更新
const handleChange = useCallback((event) => {
const { name, value, type, checked } = event.target;
// 3. 处理 checkbox 的特殊情况 (checked 属性)
const val = type === 'checkbox' ? checked : value;
// 4. 更新状态:基于字段名 (name) 设置对应的值
setValues(prevValues => ({
...prevValues, // 展开旧状态 (不可变性)
[name]: val // 更新目标字段
}));
// 5. (可选) 即时验证:如果提供了 validate 函数,并在变更时需验证
if (validate) {
// 6. 通常只验证当前变更的字段 (避免全表单验证开销)
setErrors(prevErrors => ({
...prevErrors,
[name]: validate(name, val, values) // 调用验证函数获取当前字段错误
}));
}
}, [validate]); // 依赖 validate (如果它变化)
// 7. 表单提交处理器
const handleSubmit = useCallback((onSubmit) => (event) => {
event.preventDefault(); // 阻止默认表单提交行为
setIsSubmitting(true);
// 8. 提交前进行全表单验证 (如果有验证函数)
if (validate) {
const newErrors = {};
let isValid = true;
// 9. 遍历所有字段进行验证
Object.keys(values).forEach(name => {
const error = validate(name, values[name], values);
if (error) {
newErrors[name] = error;
isValid = false;
}
});
setErrors(newErrors);
// 10. 如果验证不通过,阻止提交
if (!isValid) {
setIsSubmitting(false);
return;
}
}
// 11. 调用外部传入的提交函数 (onSubmit),并传入当前表单值
onSubmit(values);
setIsSubmitting(false); // 假设提交是同步的,异步提交需在 .then/.catch 中处理
}, [values, validate]); // 依赖 values 和 validate
// 12. 重置表单函数
const resetForm = useCallback(() => {
setValues(initialValues);
setErrors({});
setIsSubmitting(false);
}, [initialValues]); // 依赖 initialValues
// 13. 计算表单整体是否有效 (可选,根据 errors 对象计算)
const isValid = Object.keys(errors).length === 0;
// 14. 明确输出:表单核心状态、操作函数、辅助状态
return {
values, // 表单数据
errors, // 字段错误信息
isSubmitting, // 是否正在提交
isValid, // 表单整体是否有效 (基于当前 errors)
handleChange, // 字段变更处理器
handleSubmit, // 表单提交处理器 (高阶函数,需传入实际提交逻辑)
resetForm, // 重置表单函数
setValues, // (谨慎暴露) 直接设置 values (用于复杂情况如级联更新)
};
}
🎯 3. 事件监听/浏览器 API Hook (e.g., useWindowSize
, useMousePosition
, useKeyPress
)
-
设计思路:
- 输入 (Input): 通常不需要复杂输入,可能需要配置项(如监听哪个
key
的useKeyPress
,节流间隔)。 - 输出 (Output): 监听的目标值(如
{ width, height }
,{ x, y }
,keyPressed
)。 - 核心逻辑: 使用
useState
存储监听的值。在useEffect
中添加事件监听器(resize
,mousemove
,keydown/keyup
)。在事件处理函数中更新状态。在清理函数中移除事件监听器。必须考虑性能! (使用节流throttle
或防抖debounce
限制高频事件更新频率)。 - 关键考量点: 性能优化(节流/防抖)、SSR 兼容性(避免在 Node 环境访问
window
)、清理的重要性。
- 输入 (Input): 通常不需要复杂输入,可能需要配置项(如监听哪个
-
核心代码实现与逐行解析 (React
useWindowSize
带节流):
javascript
import { useState, useEffect } from 'react';
// 15. 节流函数工具 (避免高频更新)
function throttle(func, wait) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= wait) {
func.apply(this, args);
lastTime = now;
}
};
}
function useWindowSize(throttleWait = 100) { // 默认 100ms 节流
// 16. 状态管理:窗口尺寸
const [size, setSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0, // SSR 兼容
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
// 17. SSR 检查:只在客户端执行
if (typeof window === 'undefined') {
return;
}
// 18. 定义处理窗口大小变化的函数 (应用节流)
const handleResize = throttle(() => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, throttleWait);
// 19. 立即获取一次初始尺寸 (确保首次渲染准确)
handleResize();
// 20. 添加事件监听器 (监听 resize 事件)
window.addEventListener('resize', handleResize);
// 21. 清理函数:组件卸载时移除监听器 (并取消可能的节流等待)
return () => {
window.removeEventListener('resize', handleResize);
// 如果节流实现有定时器,这里也需要 clearTimeout
};
}, [throttleWait]); // 依赖 throttleWait (如果变化,需要重新创建 throttle)
// 22. 输出窗口尺寸对象
return size;
}
⚙️ 4. 复杂状态/Reducer Hook (e.g., useReducer
封装购物车、复杂配置)
-
设计思路:
- 输入 (Input): 初始状态 (
initialState
),Reducer 函数 (reducer
),可选的初始化函数 (init
函数)。 - 输出 (Output): 当前状态 (
state
),分发action
的函数 (dispatch
),有时会封装特定的action creators
(函数返回action
对象)。 - 核心逻辑: 直接使用 React 内置的
useReducer
Hook。定义清晰的reducer
函数处理不同的action.type
。状态更新遵循不可变性原则(返回新对象/数组)。适合管理包含多个子值、逻辑复杂的 state,或者下一个 state 依赖于之前的 state 的场景。 - 关键考量点: 清晰定义
action
类型、reducer
的纯净性、避免深拷贝性能问题(使用 Immer 等库可简化不可变更新)、何时选择useReducer
替代useState
(状态逻辑复杂、有深层更新、需要可预测的状态变更)。
- 输入 (Input): 初始状态 (
-
核心代码实现与逐行解析 (React 基础
useReducer
示例 - 计数器增强版):
javascript
import { useReducer } from 'react';
// 23. 定义 Action 类型 (使用常量或 TypeScript 类型)
const ACTIONS = {
INCREMENT: 'increment',
DECREMENT: 'decrement',
SET: 'set',
RESET: 'reset',
};
// 24. Reducer 函数:纯函数,接收当前 state 和 action,返回新 state
function counterReducer(state, action) {
switch (action.type) {
case ACTIONS.INCREMENT:
return { count: state.count + 1 }; // 不可变更新:返回新对象
case ACTIONS.DECREMENT:
return { count: state.count - 1 };
case ACTIONS.SET:
// 25. action 携带 payload (要设置的值)
return { count: action.payload };
case ACTIONS.RESET:
// 26. 重置到初始状态 (假设 initialState 是 { count: 0 })
return { count: 0 };
default:
// 27. 未知 action 类型:返回当前 state (或抛出错误)
return state;
}
}
// 28. (可选) Action Creators:封装创建 action 对象的函数
function increment() {
return { type: ACTIONS.INCREMENT };
}
function decrement() {
return { type: ACTIONS.DECREMENT };
}
function setCount(value) {
return { type: ACTIONS.SET, payload: value };
}
function resetCount() {
return { type: ACTIONS.RESET };
}
function useEnhancedCounter(initialCount = 0) {
// 29. 使用 useReducer:传入 reducer 函数和初始状态
const [state, dispatch] = useReducer(counterReducer, { count: initialCount });
// 30. 封装更语义化的操作函数 (内部调用 dispatch)
const incrementCounter = () => dispatch(increment()); // 使用 action creator
const decrementCounter = () => dispatch(decrement());
const setCounter = (value) => dispatch(setCount(value));
const resetCounter = () => dispatch(resetCount());
// 31. 输出:当前状态 + 操作函数
return {
count: state.count, // 暴露具体的状态值
increment: incrementCounter,
decrement: decrementCounter,
set: setCounter,
reset: resetCounter,
};
}
📊 三、设计思路对比与选择
特征 | 数据请求 Hook (useFetch ) |
表单管理 Hook (useForm ) |
事件监听 Hook (useWindowSize ) |
复杂状态 Hook (useEnhancedCounter ) |
---|---|---|---|---|
核心状态管理 | data , loading , error |
values , errors , isSubmitting |
size (或其他目标值) |
state (通常是一个对象) |
主要副作用 | API 调用 (fetch /axios ) |
表单提交逻辑 | 事件监听 (resize 等) |
无 (Reducer 是纯函数) |
关键优化点 | 请求取消、竞态处理、缓存 | 字段级更新、验证触发时机 | 节流/防抖 (必须!) | 不可变更新、避免深拷贝开销 |
清理需求 | 高 (取消请求) | 低 (通常无持续副作用) | 高 (移除事件监听器) | 无 |
输入复杂度 | 中 (URL, options, deps) | 中/高 (initialValues, 验证规则) | 低 (可能配置节流间隔) | 中 (Reducer, initialState) |
输出复杂度 | 中 (状态 + refetch ) |
高 (值、处理器、错误、状态) | 低 (目标值) | 中 (状态 + dispatch/封装函数) |
适用场景 | 任何异步数据获取 | 表单交互 | 响应视窗变化、鼠标/键盘事件 | 购物车、复杂配置、多步骤流程 |
💎 四、总结与面试要点
- 理解原则是根基: 面试中阐述自定义 Hook 设计时,务必清晰传达 单一职责、明确输入输出、命名规范、性能意识、副作用管理、遵循规则 这些核心原则。
- 场景化设计思维: 不要试图设计一个"万能" Hook。根据要解决的具体问题类型 (数据获取、表单、事件、复杂状态)选择合适的设计模式和核心 API (
useState
,useEffect
,useReducer
,useCallback
,useMemo
,useRef
)。 - 代码细节体现功力:
- 状态初始化: 考虑
null
vs 空数组/对象,SSR 兼容性。 - 依赖数组: 精确管理,避免遗漏导致陈旧闭包,避免冗余导致无效重运行。
- 清理函数: 绝对必要 对于请求、事件监听、定时器。这是防止内存泄漏和错误的关键。
- 性能优化: 高频事件用节流/防抖 ;大型状态更新用不可变数据+结构共享 或
useImmer
;避免在渲染中做高开销计算用useMemo
;避免回调函数频繁重建用useCallback
(配合稳定依赖)。 - 错误处理: 健壮处理异步错误(API 失败、网络错误),区分可取消错误(
AbortError
)。 - 类型安全 (TS): 强烈推荐使用 TypeScript 定义 Hook 的输入参数、状态类型、返回值类型,极大提升可维护性和使用体验。
- 状态初始化: 考虑
- 权衡与取舍: 例如,
useForm
是选择简单的useState
还是更强大的useReducer
?即时验证还是提交时验证?暴露setValues
还是只提供handleChange
?根据业务复杂度和团队约定做决定。 - 测试性: 设计 Hook 时就考虑如何测试。逻辑与 UI 分离的自定义 Hook 天然更易于进行单元测试。
通过理解这些不同场景的设计思路和代码细节,你应该能更系统地构思如何封装自己的 Hook,并在面试中清晰地阐述你的设计决策和实现考量。记住,好的自定义 Hook 是逻辑清晰、接口友好、性能稳健、易于维护和测试的独立单元。
以下是针对封装 AI 搜索组件的自定义 Hook 设计思路及实现详解,结合前端面试的技术要点展开:
🔍 一、设计核心原则
- 逻辑与UI分离
- 将搜索的数据管理(请求、状态、错误处理)与UI渲染解耦,通过 Hook 封装通用逻辑,组件仅负责展示。
- 输入输出明确
- 输入:搜索 API 函数、初始参数、防抖配置等。
- 输出 :搜索状态(
loading
/error
/data
)、触发搜索的函数、参数更新方法。
- 性能优化
- 内置防抖(
debounce
)减少高频请求,依赖项精准控制重请求条件。
- 内置防抖(
🛠️ 二、Hook 结构设计(useAISearch
)
javascript
import { useState, useEffect, useCallback, useRef } from 'react';
function useAISearch({ api, initialQuery = '', debounceTime = 300 }) {
const [query, setQuery] = useState(initialQuery);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const timerRef = useRef();
// 防抖触发搜索
const triggerSearch = useCallback(() => {
if (!query.trim()) {
setResults([]);
return;
}
setLoading(true);
setError(null);
api(query)
.then((data) => setResults(data))
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [api, query]);
// 防抖逻辑
useEffect(() => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(triggerSearch, debounceTime);
return () => clearTimeout(timerRef.current);
}, [query, debounceTime, triggerSearch]);
// 暴露给组件的接口
return {
query,
setQuery, // 更新搜索词
results, // 搜索结果
loading, // 加载状态
error, // 错误信息
search: triggerSearch, // 手动触发搜索(如按钮点击)
};
}
⚙️ 三、关键实现细节
- 防抖控制
- 使用
useRef
存储计时器,避免setTimeout
闭包问题,debounceTime
参数支持动态调整。
- 使用
- 异步安全处理
- 请求前后更新
loading
状态,错误捕获后重置状态并提示,避免组件崩溃。
- 请求前后更新
- 条件请求
- 空搜索词自动清空结果,避免无效请求。
- 内存管理
- 清理计时器(
clearTimeout
)防止组件卸载后执行无效更新。
- 清理计时器(
💡 四、组件集成示例
jsx
function AISearchComponent() {
const { query, setQuery, results, loading, error, search } = useAISearch({
api: fetchSearchResults, // 传入搜索API函数
debounceTime: 500,
});
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="输入关键词..."
/>
<button onClick={search}>搜索</button>
{loading && <p>搜索中...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
</div>
);
}
🚀 五、面试技术亮点
- 自定义 Hook 设计范式
- 遵循单一职责原则(仅处理搜索逻辑)和明确输入输出(参数/返回值清晰)。
- 性能优化实践
- 防抖减少请求次数、
useCallback
避免函数重复创建、条件渲染控制。
- 防抖减少请求次数、
- 错误边界处理
- 异步请求的
try/catch
封装,提升用户体验。
- 异步请求的
- 扩展性
- 支持自定义防抖时间、API 替换,适配不同场景(如实时搜索/按钮触发)。
🔮 六、进阶优化方向
- 缓存策略 :结合
useMemo
缓存历史结果,减少重复请求。 - 取消请求 :集成
AbortController
终止进行中的请求。 - 类型安全:使用 TypeScript 定义 API 响应格式,增强代码可靠性。
💎 总结 :封装
useAISearch
的核心在于逻辑复用 与性能可控。通过分离数据流与UI,开发者可快速构建高性能搜索组件,同时保持代码可维护性。此设计模式体现了 React Hooks 的核心价值------将复杂逻辑抽象为可插拔单元。
以下是5个面试高频自定义Hook实现题目,结合大厂真题和原理深度整理,附核心思路与实现要点,助你高效备战:
🔥 高频自定义Hooks实现题目
1. 防抖搜索Hook(useDebounceSearch
)
题目描述 :封装带防抖的搜索Hook,减少高频请求
核心思路:
javascript
function useDebounceSearch(api, delay = 300) {
const [results, setResults] = useState([]);
const timerRef = useRef();
const search = useCallback((query) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(async () => {
const data = await api(query);
setResults(data);
}, delay);
}, [api, delay]);
return { results, search };
}
关键考点:
- 防抖逻辑(
setTimeout
+clearTimeout
) useRef
保存计时器避免闭包陷阱- 依赖项处理(
api
变化需重建函数)
2. 强制刷新Hook(useForceUpdate
)
题目描述 :不修改state的前提下强制组件重新渲染
核心思路:
javascript
const useForceUpdate = () => {
const [_, forceUpdate] = useReducer(x => x + 1, 0);
return useCallback(() => forceUpdate(), []);
};
关键考点:
- 利用
useReducer
的不可变更新触发渲染 - 返回稳定函数引用(
useCallback
空依赖) - 原理:状态计数变化引发组件更新
3. 异步请求Hook(useFetch
)
题目描述 :封装带加载/错误状态的数据请求逻辑
核心思路:
javascript
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const res = await fetch(url, { signal: controller.signal });
setData(await res.json());
} catch (err) {
if (err.name !== 'AbortError') setError(err);
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort(); // 取消未完成请求
}, [url]);
return { data, loading, error };
}
关键考点:
- 请求取消(
AbortController
避免竞态) - 错误边界处理(区分取消错误与真实错误)
- 依赖数组控制请求触发时机
4. Redux状态管理Hook(useReducer
进阶)
题目描述 :用自定义Hook实现Redux核心功能(状态+dispatch)
核心思路:
javascript
const useCustomRedux = (reducer, initialState) => {
const [state, dispatch] = useReducer(reducer, initialState);
// 封装action creators
const bindActionCreators = (actions) =>
Object.keys(actions).reduce((acc, key) => ({
...acc,
[key]: (...args) => dispatch(actions[key](...args))
}), {});
return { state, ...bindActionCreators(actions) };
};
关键考点:
useReducer
管理复杂状态逻辑- Action Creators封装(避免手动写
dispatch
) - 对比Redux:缺少中间件/时间旅行但轻量
5. 响应式窗口尺寸Hook(useWindowSize
)
题目描述 :实时获取浏览器窗口尺寸变化
核心思路:
javascript
function useWindowSize() {
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;
}
关键考点:
- 事件监听与清理(
useEffect
清理函数) - 节流优化(高频事件需加
throttle
) - SSR兼容(
typeof window
检测)
💡 扩展练习建议
usePrevious
:保存上一次的state值(useRef
追踪历史值)useLocalStorage
:同步localStorage与组件状态(JSON.stringify/parse
处理)useClickOutside
:点击组件外部时触发回调(document.addEventListener
+ref
判断)useHover
:检测元素悬停状态(mouseenter/mouseleave
事件绑定)
⚠️ 面试核心考察点
- 设计原则 :单一职责、输入输出明确、命名规范(
useXxx
) - 性能意识 :合理使用
useMemo/useCallback
、避免无效渲染 - 边界处理:竞态取消、错误捕获、SSR兼容
- 原理深度:闭包陷阱、依赖数组作用、Hooks调用规则
💪 练习建议:手写实现后对比ahooks源码(github.com/alibaba/hoo...),学习工业级Hook的异常处理与扩展性设计。