Skip to content

@Summer 的 JavaScript / React 相关面试题(1)

  1. 如何在组件销毁的时候取消网络请求?

    可以在组件的 beforeDestroyunmounted 生命周期钩子中调用 abort() 方法来取消网络请求。

  2. requestAnimationFrame 是宏任务还是微任务?为什么?

    requestAnimationFrame 是宏任务。它会在浏览器下次重绘之前执行,因此它的回调函数会在所有微任务(如 Promise)之后执行。

  3. 如何处理 React 中的 useState 闭包陷阱?提供一个具体场景。

    闭包陷阱是指在异步回调中使用 state 值时,获取到的可能是旧值。解决方案:

    jsx
    function 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);
      }, []);
    }
  4. 解释下 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>
      );
    }

    应该在以下情况使用:

    • 路由切换时加载不同页面组件
    • 初次渲染不需要立即显示的大型组件
    • 条件渲染的复杂功能(如管理员面板)
    • 第三方库较大且不是立即需要时
  5. 设计一个自定义 hook 处理无限滚动(infinite scroll),要考虑性能和用户体验。

    jsx
    function 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
      };
    }

    使用示例:

    jsx
    function 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>
      );
    }
  6. 解释虚拟列表(Virtual List)的工作原理及其实现思路,并说明它与传统渲染方式的区别。

    虚拟列表是一种优化技术,只渲染用户视口中可见的部分元素,而不是渲染全部数据,适用于处理大量数据的列表。

    工作原理:

    1. 计算可视区域高度
    2. 根据元素高度和可视区域,计算可视区域内可显示的元素数量
    3. 监听滚动事件,动态计算应该显示哪些元素
    4. 通过绝对定位或 transform 定位这些元素,并设置容器总高度与全部元素相等

    简化实现思路:

    jsx
    function 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节点、提高渲染性能、减少内存占用、解决大数据卡顿问题
  7. React 的并发模式(Concurrent Mode)是什么?它如何解决大型应用的性能问题?

    React 并发模式是一种新的渲染机制,允许 React 中断渲染工作,处理更高优先级的任务,然后再回来完成之前的工作。

    核心机制:

    • 可中断渲染:React 可以开始渲染,暂停,稍后继续
    • 优先级排序:不同的更新可以有不同的优先级
    • 时间切片:将渲染工作分割成小块,分布在多个帧中执行

    主要特性与API:

    1. <Suspense>:在数据加载时显示 fallback UI
    2. useTransition:标记低优先级更新,允许UI保持响应
    3. useDeferredValue:延迟处理需要大量计算的值

    示例:

    jsx
    function 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准备好,提升用户体验
  8. 描述 JavaScript 引擎的内存分配和垃圾回收机制,并解释内存泄漏的常见原因和如何避免。

    JavaScript 引擎内存管理主要包括两个方面:内存分配和垃圾回收。

    内存分配:

    • 栈内存:存储原始类型(NumberBooleanString 等)和对象引用
    • 堆内存:存储对象、函数等复杂数据类型

    垃圾回收机制:

    1. 引用计数:计算对象的引用数量,当引用为0时回收(有循环引用问题)
    2. 标记-清除:从根对象(如全局对象)开始,标记所有可访问对象,未被标记的视为垃圾回收
    3. 分代回收:将对象分为"新生代"和"老生代",对不同代使用不同策略

    常见内存泄漏原因及避免方法:

    1. 闭包不当使用

      javascript
      function createLeak() {
        const largeData = new Array(1000000).fill('x');
        return function() {
          // 这个函数保持对largeData的引用,即使它不使用它
          console.log('leak');
        };
      }
      const leak = createLeak(); // largeData将始终存在

      避免:不再需要时将闭包置为 null,或重构代码避免不必要闭包

    2. 被遗忘的定时器

      javascript
      function startTimer() {
        const data = loadLargeData();
        setInterval(() => {
          // 使用data
          doSomething(data);
        }, 1000);
      }

      避免:组件卸载时清除定时器,使用 useEffect 返回清理函数

    3. DOM引用

      javascript
      const elements = {};
      function cacheElement() {
        elements.button = document.getElementById('button');
        // 即使元素从DOM中移除,仍被缓存引用
      }

      避免:当元素被移除时,同时删除对应缓存引用

    4. 事件监听器未移除

      javascript
      function addListener() {
        const data = loadLargeData();
        document.addEventListener('click', function() {
          // 使用data
          processData(data);
        });
      }

      避免:使用命名函数便于移除,或在组件销毁时调用 removeEventListener

    5. 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被垃圾回收时,相关数据也会被回收
      }
  9. 设计一个轻量级状态管理解决方案,不依赖 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());
      }, []);
      
      // ...
    }
  10. 实现一个高性能的自动补全(Autocomplete)组件,需要处理防抖、异步请求、键盘导航和无障碍访问。

    jsx
    import { 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 结构
  11. React Server Components 和传统客户端组件有什么区别?在什么场景下使用 RSC 更有优势?

    React Server Components (RSC) 与客户端组件的区别

    基本区别:

    特性Server ComponentsClient Components
    执行环境服务器端浏览器端
    交互性不支持状态和事件处理完全支持 React 状态和事件
    访问资源可直接访问服务器资源(数据库、文件系统等)只能通过 API 调用访问服务器资源
    包大小影响不增加客户端包体积增加 JavaScript 包体积
    代码可见性代码不发送到客户端代码发送到浏览器
    导入依赖服务器端导入不影响客户端所有依赖都增加客户端包体积

    RSC 的优势场景:

    1. 数据获取密集型组件

      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>
        );
      }
    2. 减少包体积的场景

      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 }} />;
      }
    3. 访问敏感数据/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} />;
      }
    4. 文件系统访问

      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>;
      }
    5. 混合使用模式(服务端渲染框架)

      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 最适合用于数据获取逻辑、大型依赖使用、静态内容渲染、访问服务器端专有资源的场景,而交互逻辑应当保留在客户端组件中。最佳实践是将应用分层,让服务器组件负责数据和结构,客户端组件负责交互。