@Summer 的 JavaScript / React 相关面试题(1)
如何在组件销毁的时候取消网络请求?
可以在组件的
beforeDestroy
或unmounted
生命周期钩子中调用abort()
方法来取消网络请求。requestAnimationFrame
是宏任务还是微任务?为什么?requestAnimationFrame
是宏任务。它会在浏览器下次重绘之前执行,因此它的回调函数会在所有微任务(如Promise
)之后执行。如何处理 React 中的
useState
闭包陷阱?提供一个具体场景。闭包陷阱是指在异步回调中使用
state
值时,获取到的可能是旧值。解决方案:jsxfunction Counter() { const [count, setCount] = useState(0); // 问题代码 useEffect(() => { const timer = setTimeout(() => { console.log(count); // 总是 0 setCount(count + 1); // 总是设置为 1 }, 2000); return () => clearTimeout(timer); }, []); // 空依赖数组,不会随 count 更新 // 解决方案1:添加依赖 useEffect(() => { const timer = setTimeout(() => { setCount(count + 1); // 使用最新的 count }, 2000); return () => clearTimeout(timer); }, [count]); // 解决方案2:使用函数式更新 useEffect(() => { const timer = setTimeout(() => { setCount(prev => prev + 1); // 不依赖闭包中的 count }, 2000); return () => clearTimeout(timer); }, []); }
解释下 React 中的代码分割(Code Splitting)及其实现方式,何时应该使用它?
代码分割是指将应用拆分成多个小块(chunks),按需加载,从而减小初始加载体积,提升性能。
实现方式:
- 使用动态
import()
语法 - 使用
React.lazy()
和Suspense
组合
jsx// 不使用代码分割 import HeavyComponent from './HeavyComponent'; // 使用代码分割 const HeavyComponent = React.lazy(() => import('./HeavyComponent')); function App() { return ( <React.Suspense fallback={<div>Loading...</div>}> <HeavyComponent /> </React.Suspense> ); }
应该在以下情况使用:
- 路由切换时加载不同页面组件
- 初次渲染不需要立即显示的大型组件
- 条件渲染的复杂功能(如管理员面板)
- 第三方库较大且不是立即需要时
- 使用动态
设计一个自定义 hook 处理无限滚动(infinite scroll),要考虑性能和用户体验。
jsxfunction useInfiniteScroll(fetchMore, options = {}) { const { threshold = 200, initialData = [], deps = [] } = options; const [data, setData] = useState(initialData); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const observerRef = useRef(null); const lastElementRef = useRef(null); const handleObserver = useCallback((entries) => { const [target] = entries; if (target.isIntersecting && hasMore && !loading) { setLoading(true); fetchMore().then((newItems) => { if (newItems.length === 0) { setHasMore(false); } else { setData(prev => [...prev, ...newItems]); } setLoading(false); }); } }, [fetchMore, hasMore, loading]); useEffect(() => { const observer = new IntersectionObserver(handleObserver, { root: null, rootMargin: '0px', threshold: 0.1 }); observerRef.current = observer; return () => { if (observerRef.current) { observerRef.current.disconnect(); } }; }, [handleObserver]); useEffect(() => { if (lastElementRef.current && observerRef.current) { observerRef.current.observe(lastElementRef.current); } return () => { if (lastElementRef.current && observerRef.current) { observerRef.current.unobserve(lastElementRef.current); } }; }, [data, ...deps]); return { data, loading, hasMore, lastElementRef }; }
使用示例:
jsxfunction ProductList() { const fetchMoreItems = async () => { // 获取下一页数据的API调用 const res = await api.getProducts(page); return res.products; }; const { data, loading, lastElementRef } = useInfiniteScroll(fetchMoreItems); return ( <div> {data.map((item, index) => ( <div key={item.id} ref={index === data.length - 1 ? lastElementRef : null} > {item.name} </div> ))} {loading && <div>Loading more...</div>} </div> ); }
解释虚拟列表(Virtual List)的工作原理及其实现思路,并说明它与传统渲染方式的区别。
虚拟列表是一种优化技术,只渲染用户视口中可见的部分元素,而不是渲染全部数据,适用于处理大量数据的列表。
工作原理:
- 计算可视区域高度
- 根据元素高度和可视区域,计算可视区域内可显示的元素数量
- 监听滚动事件,动态计算应该显示哪些元素
- 通过绝对定位或
transform
定位这些元素,并设置容器总高度与全部元素相等
简化实现思路:
jsxfunction VirtualList({ data, itemHeight, windowHeight }) { const [scrollTop, setScrollTop] = useState(0); // 计算起始索引和结束索引 const startIndex = Math.floor(scrollTop / itemHeight); const endIndex = Math.min( data.length - 1, Math.floor((scrollTop + windowHeight) / itemHeight) + 1 ); // 只渲染可见范围内的数据 const visibleData = data.slice(startIndex, endIndex + 1); const totalHeight = data.length * itemHeight; const handleScroll = (e) => { setScrollTop(e.target.scrollTop); }; return ( <div style={{ height: windowHeight, overflow: 'auto' }} onScroll={handleScroll} > <div style={{ height: totalHeight, position: 'relative' }}> {visibleData.map((item, index) => ( <div key={startIndex + index} style={{ position: 'absolute', top: (startIndex + index) * itemHeight, height: itemHeight }} > {item.content} </div> ))} </div> </div> ); }
与传统渲染区别:
- 传统方式:渲染全部元素,DOM 节点数量等于数据量
- 虚拟列表:只渲染可见元素,DOM 节点数量仅限于视口可见数量
- 优势:大幅减少DOM节点、提高渲染性能、减少内存占用、解决大数据卡顿问题
React 的并发模式(Concurrent Mode)是什么?它如何解决大型应用的性能问题?
React 并发模式是一种新的渲染机制,允许 React 中断渲染工作,处理更高优先级的任务,然后再回来完成之前的工作。
核心机制:
- 可中断渲染:React 可以开始渲染,暂停,稍后继续
- 优先级排序:不同的更新可以有不同的优先级
- 时间切片:将渲染工作分割成小块,分布在多个帧中执行
主要特性与API:
<Suspense>
:在数据加载时显示 fallback UIuseTransition
:标记低优先级更新,允许UI保持响应useDeferredValue
:延迟处理需要大量计算的值
示例:
jsxfunction SearchResults({ query }) { const [isPending, startTransition] = useTransition(); const [results, setResults] = useState([]); useEffect(() => { if (query === '') return setResults([]); // 将搜索标记为低优先级 startTransition(() => { // 复杂计算或API调用 const calculatedResults = performExpensiveSearch(query); setResults(calculatedResults); }); }, [query]); return ( <> {isPending ? <div>Loading results...</div> : null} <div style={{ opacity: isPending ? 0.5 : 1 }}> {results.map(result => ( <div key={result.id}>{result.text}</div> ))} </div> </> ); }
解决的问题:
- 避免由于大量计算导致的 UI 冻结
- 允许高优先级更新(点击、输入)打断低优先级渲染
- 避免加载状态闪烁(通过延迟显示loading状态)
- 保持旧UI可见直到新UI准备好,提升用户体验
描述 JavaScript 引擎的内存分配和垃圾回收机制,并解释内存泄漏的常见原因和如何避免。
JavaScript 引擎内存管理主要包括两个方面:内存分配和垃圾回收。
内存分配:
- 栈内存:存储原始类型(
Number
、Boolean
、String
等)和对象引用 - 堆内存:存储对象、函数等复杂数据类型
垃圾回收机制:
- 引用计数:计算对象的引用数量,当引用为0时回收(有循环引用问题)
- 标记-清除:从根对象(如全局对象)开始,标记所有可访问对象,未被标记的视为垃圾回收
- 分代回收:将对象分为"新生代"和"老生代",对不同代使用不同策略
常见内存泄漏原因及避免方法:
闭包不当使用
javascriptfunction createLeak() { const largeData = new Array(1000000).fill('x'); return function() { // 这个函数保持对largeData的引用,即使它不使用它 console.log('leak'); }; } const leak = createLeak(); // largeData将始终存在
避免:不再需要时将闭包置为
null
,或重构代码避免不必要闭包被遗忘的定时器
javascriptfunction startTimer() { const data = loadLargeData(); setInterval(() => { // 使用data doSomething(data); }, 1000); }
避免:组件卸载时清除定时器,使用
useEffect
返回清理函数DOM引用
javascriptconst elements = {}; function cacheElement() { elements.button = document.getElementById('button'); // 即使元素从DOM中移除,仍被缓存引用 }
避免:当元素被移除时,同时删除对应缓存引用
事件监听器未移除
javascriptfunction addListener() { const data = loadLargeData(); document.addEventListener('click', function() { // 使用data processData(data); }); }
避免:使用命名函数便于移除,或在组件销毁时调用
removeEventListener
WeakMap/WeakSet的使用场景
javascript// 不合适: 使用Map存储DOM引用 const cache = new Map(); function storeElement(element) { cache.set(element, someData); } // 正确: 使用WeakMap const cache = new WeakMap(); function storeElement(element) { cache.set(element, someData); // 当element被垃圾回收时,相关数据也会被回收 }
- 栈内存:存储原始类型(
设计一个轻量级状态管理解决方案,不依赖 Redux,但能处理复杂应用状态,并支持异步操作。
我们可以实现一个基于 React Context、
useReducer
和中间件的轻量级状态管理解决方案:jsx// createStore.js import { createContext, useContext, useReducer, useMemo } from 'react'; export function createStore(reducer, initialState, middlewares = []) { const StoreContext = createContext(); // 应用中间件 const applyMiddleware = (dispatch) => { // 从右到左组合中间件 return middlewares.reduceRight((acc, middleware) => { return middleware(acc); }, dispatch); }; const StoreProvider = ({ children }) => { const [state, baseDispatch] = useReducer(reducer, initialState); // 增强的dispatch支持中间件 const dispatch = useMemo(() => { return applyMiddleware((action) => { // 支持函数作为action (用于异步操作) if (typeof action === 'function') { return action(dispatch, () => state); } return baseDispatch(action); }); }, [state]); const store = useMemo(() => ({ state, dispatch }), [state]); return ( <StoreContext.Provider value={store}> {children} </StoreContext.Provider> ); }; const useStore = () => { const store = useContext(StoreContext); if (!store) { throw new Error('useStore must be used within a StoreProvider'); } return store; }; // 选择器hook,避免不必要的重渲染 const useSelector = (selector) => { const { state } = useStore(); return selector(state); }; const useDispatch = () => { const { dispatch } = useStore(); return dispatch; }; return { StoreProvider, useStore, useSelector, useDispatch }; } // 中间件示例 - 日志记录 export const loggerMiddleware = (next) => (action) => { console.log('dispatching', action); const result = next(action); console.log('next state', result); return result; }; // 异步操作中间件 export const thunkMiddleware = (next) => (action) => { if (typeof action === 'function') { return action(next); } return next(action); };
使用示例:
jsx// store.js const initialState = { counter: 0, todos: [] }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, counter: state.counter + 1 }; case 'DECREMENT': return { ...state, counter: state.counter - 1 }; case 'ADD_TODO': return { ...state, todos: [...state.todos, action.payload] }; default: return state; } } const { StoreProvider, useSelector, useDispatch } = createStore( reducer, initialState, [loggerMiddleware, thunkMiddleware] ); export { StoreProvider, useSelector, useDispatch }; // 异步action创建器 export const fetchTodos = () => async (dispatch) => { dispatch({ type: 'FETCH_TODOS_START' }); try { const response = await fetch('/api/todos'); const todos = await response.json(); dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: todos }); } catch (error) { dispatch({ type: 'FETCH_TODOS_ERROR', payload: error }); } }; // 使用 function App() { return ( <StoreProvider> <Counter /> <TodoList /> </StoreProvider> ); } function Counter() { const count = useSelector(state => state.counter); const dispatch = useDispatch(); return ( <div> Count: {count} <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button> </div> ); } function TodoList() { const todos = useSelector(state => state.todos); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchTodos()); }, []); // ... }
实现一个高性能的自动补全(Autocomplete)组件,需要处理防抖、异步请求、键盘导航和无障碍访问。
jsximport { useState, useEffect, useRef, useCallback } from 'react'; function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(timer); }; }, [value, delay]); return debouncedValue; } function Autocomplete({ fetchSuggestions, onSelect, placeholder = '搜索...' }) { const [input, setInput] = useState(''); const [suggestions, setSuggestions] = useState([]); const [loading, setLoading] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const [showSuggestions, setShowSuggestions] = useState(false); const containerRef = useRef(null); const inputRef = useRef(null); const suggestionRefs = useRef([]); // 防抖处理输入 const debouncedInput = useDebounce(input, 300); // 获取建议结果 useEffect(() => { const fetchData = async () => { if (!debouncedInput.trim()) { setSuggestions([]); return; } setLoading(true); try { // 使用缓存提高性能 const results = await fetchSuggestions(debouncedInput); setSuggestions(results); setShowSuggestions(true); } catch (error) { console.error('Error fetching suggestions:', error); } finally { setLoading(false); } }; fetchData(); }, [debouncedInput, fetchSuggestions]); // 键盘导航 const handleKeyDown = useCallback((e) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); setActiveIndex((prev) => prev < suggestions.length - 1 ? prev + 1 : prev ); break; case 'ArrowUp': e.preventDefault(); setActiveIndex((prev) => (prev > 0 ? prev - 1 : 0)); break; case 'Enter': if (activeIndex >= 0 && suggestions[activeIndex]) { handleSelect(suggestions[activeIndex]); } break; case 'Escape': setShowSuggestions(false); setActiveIndex(-1); break; default: break; } }, [activeIndex, suggestions]); // 当选中项变化时,滚动到可见区域 useEffect(() => { if (activeIndex >= 0 && suggestionRefs.current[activeIndex]) { suggestionRefs.current[activeIndex].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }, [activeIndex]); // 点击外部关闭建议 useEffect(() => { const handleOutsideClick = (e) => { if (containerRef.current && !containerRef.current.contains(e.target)) { setShowSuggestions(false); } }; document.addEventListener('click', handleOutsideClick); return () => { document.removeEventListener('click', handleOutsideClick); }; }, []); const handleInputChange = (e) => { setInput(e.target.value); setActiveIndex(-1); }; const handleSelect = (item) => { setInput(item.title || item.name || item.text); setShowSuggestions(false); if (onSelect) { onSelect(item); } inputRef.current.focus(); }; // 高亮匹配文本 const highlightMatch = (text) => { if (!input) return text; const regex = new RegExp(`(${input})`, 'gi'); const parts = text.split(regex); return parts.map((part, i) => regex.test(part) ? <mark key={i}>{part}</mark> : part ); }; return ( <div className="autocomplete-container" ref={containerRef}> <div className="input-wrapper"> <input ref={inputRef} type="text" value={input} onChange={handleInputChange} onKeyDown={handleKeyDown} onFocus={() => setShowSuggestions(!!suggestions.length)} placeholder={placeholder} aria-label="Search" aria-autocomplete="list" aria-controls="suggestions-list" aria-activedescendant={activeIndex >= 0 ? `suggestion-${activeIndex}` : ''} /> {loading && <span className="loader" aria-hidden="true" />} </div> {showSuggestions && suggestions.length > 0 && ( <ul id="suggestions-list" className="suggestions-list" role="listbox" > {suggestions.map((item, index) => { const text = item.title || item.name || item.text; return ( <li key={item.id || index} ref={(el) => { suggestionRefs.current[index] = el; }} id={`suggestion-${index}`} role="option" aria-selected={activeIndex === index} className={`suggestion-item ${activeIndex === index ? 'active' : ''}`} onClick={() => handleSelect(item)} onMouseEnter={() => setActiveIndex(index)} > {highlightMatch(text)} {item.description && ( <div className="item-description">{item.description}</div> )} </li> ); })} </ul> )} {showSuggestions && suggestions.length === 0 && !loading && input && ( <div className="no-results">无匹配结果</div> )} </div> ); } // 使用示例 function SearchPage() { const fetchSuggestions = async (query) => { // 实现带有缓存的API请求 const cache = fetchSuggestions.cache || (fetchSuggestions.cache = {}); if (cache[query]) { return cache[query]; } const response = await fetch(`/api/search?q=${query}`); const data = await response.json(); cache[query] = data.results; return data.results; }; const handleSelect = (item) => { console.log('Selected:', item); }; return ( <div className="search-page"> <h1>搜索</h1> <Autocomplete fetchSuggestions={fetchSuggestions} onSelect={handleSelect} placeholder="搜索产品、文章..." /> </div> ); }
性能优化点:
- 使用防抖避免频繁API请求
- 实现请求缓存减少重复请求
- 使用虚拟列表处理大量搜索结果
- 使用
React.memo
避免不必要的重渲染 - 只有当显示建议时才添加全局事件监听
无障碍功能:
- 添加适当的ARIA属性支持屏幕阅读器
- 键盘导航支持(上下箭头、Enter、Escape)
- 焦点管理和视觉提示
- 语义化 HTML 结构
React Server Components 和传统客户端组件有什么区别?在什么场景下使用 RSC 更有优势?
React Server Components (RSC) 与客户端组件的区别
基本区别:
特性 Server Components Client Components 执行环境 服务器端 浏览器端 交互性 不支持状态和事件处理 完全支持 React 状态和事件 访问资源 可直接访问服务器资源(数据库、文件系统等) 只能通过 API 调用访问服务器资源 包大小影响 不增加客户端包体积 增加 JavaScript 包体积 代码可见性 代码不发送到客户端 代码发送到浏览器 导入依赖 服务器端导入不影响客户端 所有依赖都增加客户端包体积 RSC 的优势场景:
数据获取密集型组件
jsx// Server Component async function ProductDetails({ id }) { // 直接访问数据库,无需API中间层 const product = await db.products.findUnique({ where: { id } }); const relatedProducts = await db.products.findMany({ where: { category: product.category }, take: 5 }); const reviews = await db.reviews.findMany({ where: { productId: id } }); return ( <div> <h1>{product.name}</h1> <ProductInfo product={product} /> <Reviews reviews={reviews} /> <RelatedProducts products={relatedProducts} /> </div> ); }
减少包体积的场景
jsx// Server Component import { marked } from 'marked'; // 大型依赖库 import sanitizeHtml from 'sanitize-html'; // 安全处理 export default function MarkdownRenderer({ content }) { // 在服务器端处理,这些库不会添加到客户端包中 const html = sanitizeHtml(marked(content)); return <div dangerouslySetInnerHTML={{ __html: html }} />; }
访问敏感数据/API 的组件
jsx// Server Component import { getSecretApiKey } from '../secrets'; // 敏感信息 async function AdminDashboard() { // API密钥不会暴露给客户端 const apiKey = getSecretApiKey(); const adminData = await fetch('https://api.example.com/admin', { headers: { Authorization: `Bearer ${apiKey}` } }).then(r => r.json()); return <AdminPanel data={adminData} />; }
文件系统访问
jsx// Server Component import fs from 'fs/promises'; import path from 'path'; async function SiteMap() { // 直接读取服务器文件系统 const sitemap = await fs.readFile( path.join(process.cwd(), 'public', 'sitemap.xml'), 'utf8' ); return <pre>{sitemap}</pre>; }
混合使用模式(服务端渲染框架)
jsx// Server Component import db from '../db'; import { ClientCounter } from './ClientCounter'; // 标记为客户端组件 export default async function Page() { // 数据获取在服务器端 const products = await db.products.findMany(); return ( <div> {/* 静态内容在服务器渲染 */} <h1>产品列表 ({products.length})</h1> <ProductGrid products={products} /> {/* 交互组件在客户端渲染 */} <ClientCounter /> </div> ); } // Client Component (需要在文件顶部添加 'use client' 指令) 'use client' import { useState } from 'react'; export function ClientCounter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}> 点击次数: {count} </button> ); }
总结:RSC 最适合用于数据获取逻辑、大型依赖使用、静态内容渲染、访问服务器端专有资源的场景,而交互逻辑应当保留在客户端组件中。最佳实践是将应用分层,让服务器组件负责数据和结构,客户端组件负责交互。