React 核心知识点详解
本文档详细解答 React 面试中的核心问题,涵盖 useEffect、async/await、防抖函数等知识点。
1. React中,防抖函数、业务函数、useCallback的包裹顺序应该是什么样子,为什么
正确的包裹顺序
scss
// ❌ 错误顺序
const debouncedFn = debounce(callback, 300);
const handler = useCallback(debouncedFn, [deps]);
// ✅ 正确顺序
const handler = useCallback(
debounce((value) => {
// 业务逻辑
}, 300),
[deps] // 依赖项
);
// ✅ 或者更清晰的方式
const handler = useMemo(
() => debounce((value) => {
// 业务逻辑
}, 300),
[deps]
);
为什么这样包裹?
原因分析:
- useCallback 的作用:缓存函数引用,避免每次渲染创建新函数
- debounce 的作用:包装函数,返回一个防抖后的新函数
- 执行时机问题:
-
- 如果
debounce在useCallback外面,每次渲染都会执行debounce,导致防抖函数被重复创建 - 这样会失去防抖效果,因为每次都是新的防抖实例
- 如果
最佳实践
scss
function SearchComponent() {
const [query, setQuery] = useState('');
// ✅ 使用 useMemo 缓存防抖函数(推荐)
const debouncedSearch = useMemo(
() => debounce((value) => {
console.log('搜索:', value);
}, 300),
[] // 空依赖,整个组件生命周期只创建一次
);
// ✅ 或者使用 useCallback
const handleSearch = useCallback(
debounce((value) => {
console.log('搜索:', value);
}, 300),
[]
);
// ✅ 如果需要访问最新的 state,使用 ref
const queryRef = useRef(query);
queryRef.current = query;
const debouncedSearch = useMemo(
() => debounce((value) => {
console.log('搜索:', queryRef.current, value);
}, 300),
[]
);
return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}
清理防抖函数
scss
useEffect(() => {
return () => {
debouncedSearch.cancel(); // 组件卸载时取消 pending 的防抖调用
};
}, [debouncedSearch]);
2. useEffect的第二个参数的用法和使用场景
三种用法
javascript
// 1. 不传第二个参数 - 每次渲染后都执行
useEffect(() => {
console.log('每次渲染后执行');
});
// 2. 传入空数组 [] - 仅在挂载时执行一次
useEffect(() => {
console.log('组件挂载时执行一次');
return () => console.log('组件卸载时执行');
}, []);
// 3. 传入依赖数组 [dep1, dep2] - 仅在依赖变化时执行
useEffect(() => {
console.log('count 变化了:', count);
}, [count]);
使用场景详解
场景 1:数据获取(挂载时)
scss
useEffect(() => {
fetchUserData();
}, []); // 空数组 = 仅挂载时执行
场景 2:响应状态变化
ini
useEffect(() => {
document.title = `点击了 ${count} 次`;
}, [count]); // count 变化时更新标题
场景 3:监听多个依赖
scss
useEffect(() => {
fetchSearchResults(query, page);
}, [query, page]); // query 或 page 变化时重新搜索
场景 4:监听 props 变化
scss
function UserProfile({ userId }) {
useEffect(() => {
fetchUser(userId);
}, [userId]); // userId 变化时重新获取用户信息
}
场景 5:订阅/取消订阅
scss
useEffect(() => {
const subscription = someObservable.subscribe(handler);
return () => {
subscription.unsubscribe(); // 清理订阅
};
}, []); // 空数组表示仅在挂载/卸载时执行
依赖项的注意事项
scss
// ❌ 错误:遗漏依赖
useEffect(() => {
fetchData(userId, filter); // filter 变化时不会重新执行
}, [userId]); // 缺少 filter
// ✅ 正确:包含所有依赖
useEffect(() => {
fetchData(userId, filter);
}, [userId, filter]);
// ✅ 使用 ESLint 插件自动检测
// npm install eslint-plugin-react-hooks --save-dev
3. useEffect中回调函数中的return出去的是什么,有什么作用
返回值:清理函数
useEffect 返回的函数被称为 **清理函数(cleanup function) **,用于清理副作用。
执行时机
javascript
useEffect(() => {
console.log('1. 副作用执行');
return () => {
console.log('2. 清理函数执行');
};
}, [count]);
执行顺序:
- 组件挂载 → 执行副作用函数
- 依赖变化 → 先执行清理函数 → 再执行新的副作用函数
- 组件卸载 → 执行清理函数
典型应用场景
场景 1:取消订阅
javascript
useEffect(() => {
const subscription = eventEmitter.subscribe((data) => {
console.log('收到数据:', data);
});
return () => {
subscription.unsubscribe(); // 清理订阅,防止内存泄漏
};
}, []);
场景 2:清除定时器
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('定时执行');
}, 1000);
return () => {
clearInterval(timer); // 清除定时器
};
}, []);
场景 3:取消网络请求
ini
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data));
return () => {
controller.abort(); // 取消请求
};
}, [url]);
场景 4:事件监听器清理
javascript
useEffect(() => {
const handleResize = () => {
console.log('窗口大小变化');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
4. 在两次更新中,useEffect第一个参数,第一个参数中的返回值,第二个参数的执行时机是什么样子
执行顺序图解
假设组件经历以下生命周期:
- 挂载
- 更新(count 从 0 → 1)
- 更新(count 从 1 → 2)
- 卸载
javascript
useEffect(() => {
console.log('A. 副作用执行');
return () => {
console.log('B. 清理函数执行');
};
}, [count]);
执行顺序
less
【第一次挂载】
→ A. 副作用执行
【第一次更新 (count: 0 → 1)】
→ B. 清理函数执行 (清理上一次的副作用)
→ A. 副作用执行 (执行新的副作用)
【第二次更新 (count: 1 → 2)】
→ B. 清理函数执行 (清理上一次的副作用)
→ A. 副作用执行 (执行新的副作用)
【卸载】
→ B. 清理函数执行 (最后清理)
详细示例
ini
function Timer({ count }) {
useEffect(() => {
console.log(`[${count}] 副作用: 创建定时器`);
const timer = setInterval(() => {
console.log(`[${count}] 定时器运行中...`);
}, 1000);
return () => {
console.log(`[${count}] 清理: 清除定时器`);
clearInterval(timer);
};
}, [count]);
return <div>{count}</div>;
}
/* 执行日志:
[0] 副作用: 创建定时器
[0] 定时器运行中...
[0] 定时器运行中...
[更新: count = 1]
[0] 清理: 清除定时器 ← 先清理旧的
[1] 副作用: 创建定时器 ← 再创建新的
[1] 定时器运行中...
[更新: count = 2]
[1] 清理: 清除定时器
[2] 副作用: 创建定时器
[卸载]
[2] 清理: 清除定时器
*/
关键点总结
- 清理函数在下次副作用执行前调用(不是在下次渲染前)
- 清理函数能读取到上一次渲染的 props 和 state(闭包)
- 挂载时不执行清理函数,卸载时执行清理函数
5. useEffect中怎么使用async
❌ 错误写法
scss
// 错误:useEffect 回调不能是 async 函数
useEffect(async () => {
const data = await fetchData(); // ❌ 这样写会报警告
setData(data);
}, []);
// 原因:useEffect 期望返回清理函数或 undefined
// async 函数返回 Promise,这会导致问题
✅ 正确写法 1:立即执行函数(IIFE)
scss
useEffect(() => {
(async () => {
try {
const data = await fetchData();
setData(data);
} catch (error) {
console.error('获取数据失败:', error);
}
})();
}, []);
✅ 正确写法 2:在内部定义 async 函数并调用
scss
useEffect(() => {
const loadData = async () => {
try {
const data = await fetchData();
setData(data);
} catch (error) {
console.error(error);
}
};
loadData();
}, []);
✅ 正确写法 3:使用 .then() 链式调用
ini
useEffect(() => {
fetchData()
.then(data => setData(data))
.catch(error => console.error(error));
}, []);
✅ 完整示例(带清理功能)
ini
useEffect(() => {
let isMounted = true; // 标记组件是否挂载
const controller = new AbortController();
const loadData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal
});
const data = await response.json();
if (isMounted) { // 只有组件还在时才更新状态
setData(data);
}
} catch (error) {
if (error.name !== 'AbortError' && isMounted) {
console.error('请求失败:', error);
}
}
};
loadData();
return () => {
isMounted = false; // 标记组件已卸载
controller.abort(); // 取消请求
};
}, [url]);
6. async...await语法,如果函数不用async标识,内部直接使用await,会有什么后果
❌ 错误示例
csharp
// ❌ 错误:非 async 函数中使用 await
function fetchData() {
const response = await fetch('/api/data'); // SyntaxError
return response.json();
}
// 报错:Uncaught SyntaxError: await is only valid in async functions
会产生什么后果?
- **语法错误(SyntaxError) **:JavaScript 引擎会抛出错误
- 代码无法执行:整个函数都不会运行
- 影响范围:如果这个函数被导出,会导致整个模块加载失败
为什么必须用 async 标识?
javascript
// ✅ 正确写法
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
// 等价于
function fetchData() {
return fetch('/api/data')
.then(response => response.json());
}
原因:
await会暂停函数执行,等待 Promise resolve- 只有
async函数才能以这种方式暂停执行 async函数总是返回 Promise,即使没有显式 return
深入理解 async/await 转换
javascript
async function example() {
console.log('开始');
const result = await someAsyncOperation();
console.log('结果:', result);
return result;
}
// Babel 转换后(简化版):
function example() {
return new Promise((resolve, reject) => {
console.log('开始');
someAsyncOperation()
.then(result => {
console.log('结果:', result);
resolve(result);
})
.catch(reject);
});
}
特殊场景:顶层 await(Top-level await)
javascript
// ✅ 在 ES2022+ 的模块中,支持顶层 await
// 无需包裹在 async 函数中
const response = await fetch('/api/data'); // ✅ 合法
const data = await response.json();
export default data;
注意:
- 仅支持在 ES Module(.mjs 或 package.json 中 type: "module")中使用
- CommonJS 模块不支持顶层 await
7. const a = 1, a.toString()的结果是什么,为什么
结果
vbscript
const a = 1;
a.toString(); // "1"(字符串 "1")
为什么是这个结果?
原因分析
- 原始值没有方法:
-
1是原始值(primitive value),不是对象,理论上没有.toString()方法- 但 JavaScript 允许在原始值上调用方法
- **自动装箱(Autoboxing) **:
-
- 当在原始值上调用方法时,JavaScript 会自动将其转换为对应的包装对象
1→new Number(1)→ 调用Number.prototype.toString()- 调用完成后,临时对象被销毁
- Number.prototype.toString() 的行为:
-
- 将数字转换为字符串表示
1→"1"
执行过程详解
vbscript
const a = 1;
a.toString();
/* 内部执行过程:
1. 访问 a.toString
→ JavaScript 发现 a 是原始值
→ 创建临时对象:new Number(1)
2. 调用 toString 方法
→ Number.prototype.toString.call(new Number(1))
3. 返回结果
→ "1"(字符串)
4. 临时对象被垃圾回收
*/
验证自动装箱
arduino
const a = 1;
// 尝试添加属性
a.customProp = 'test'; // ✅ 不报错,但无效
console.log(a.customProp); // undefined
// 原因:
// 1. a.customProp = 'test' 时,创建了临时 Number 对象
// 2. 属性被添加到临时对象上
// 3. 临时对象立即被销毁
// 4. 下次访问 a.customProp 时,又创建了新的临时对象
// 5. 新对象上没有 customProp,所以是 undefined
不同类型的 toString()
scss
// Number
(1).toString(); // "1"
(1.5).toString(); // "1.5"
(10).toString(2); // "1010" (二进制)
(10).toString(16); // "a" (十六进制)
// String
"hello".toString(); // "hello"(原样返回)
// Boolean
true.toString(); // "true"
false.toString(); // "false"
// null 和 undefined(没有包装对象)
null.toString(); // ❌ TypeError
undefined.toString(); // ❌ TypeError
原始值 vs 包装对象
less
// 原始值
const a = 1;
typeof a; // "number"
// 包装对象
const b = new Number(1);
typeof b; // "object"
b.valueOf(); // 1(获取原始值)
// 相等性比较
a == b; // true(类型转换后比较)
a === b; // false(类型不同)
// typeof
typeof a.toString(); // "string"(返回的是字符串)
typeof b.toString(); // "string"
为什么这样设计?
JavaScript 借鉴了 Java 的设计,为了让原始值也能像对象一样使用方法:
- 优点:代码简洁,不需要手动创建包装对象
- 缺点:可能让人误以为原始值也是对象
性能优化:
- 自动装箱只发生在访问属性/方法时
- 不访问属性时,仍然是高效的原始值存储
总结
这 7 个问题涵盖了 React 的核心机制和 JavaScript 的基础原理:
- useCallback + debounce:理解包裹顺序,避免重复创建
- useEffect 依赖数组:掌握三种用法场景
- 清理函数:理解副作用清理的重要性
- 执行时机:深入理解 React 的生命周期
- async in useEffect:正确处理异步操作
- async/await 语法:理解 JavaScript 的异步机制
- 自动装箱:理解 JavaScript 的类型系统
建议结合实际项目多加练习,这些知识点在面试和实际开发中都非常重要!
文档创建时间:2026-04-17创建者:OpenClaw AI Assistant