前端框架的面试往往不仅仅考察API的熟练度,更看重对底层运转逻辑的理解、架构演进背后的权衡(Trade-offs)以及在复杂场景下的工程化思考。
以下为你梳理的 React 与 Vue 面试核心题库。这些问题不仅覆盖了高频场景,且深入到框架的核心设计原理。
React 核心面试题与深度解析
1. React 的渲染机制是怎样的?Fiber 架构解决了什么问题?
解析:
这道题考察对 React 底层运作的宏观认知。
在 React 16 之前,React 的渲染过程是同步的。当组件树极其庞大时,虚拟 DOM 的 Diff 运算会长时间占用主线程,导致浏览器无法及时响应用户的输入或动画渲染,产生卡顿。
Fiber 架构本质上实现了时间分片(Time Slicing)和可中断渲染。
-
将耗时的渲染任务拆分成多个小的工作单元(Fiber 节点)。
-
利用浏览器的空闲时间(
requestIdleCallback的 polyfill 实现)来执行这些单元。 -
如果执行过程中有更高优先级的任务(如用户输入),React 会暂停当前渲染,优先处理高优先级任务,之后再恢复或重新计算。
代码延伸(Fiber 节点的数据结构简述):
JavaScript
// Fiber 节点是一个链表结构,记录了组件的状态、父子和兄弟关系
function FiberNode(tag, pendingProps, key, mode) {
// 实例相关
this.tag = tag;
this.key = key;
this.stateNode = null; // 对应的真实 DOM 或组件实例
// 树形结构,用于遍历
this.return = null; // 父节点
this.child = null; // 第一个子节点
this.sibling = null; // 下一个兄弟节点
// 副作用与更新
this.pendingProps = pendingProps;
this.memoizedState = null;
this.updateQueue = null;
}
2. 请解释 React Hooks 中的“闭包陷阱”(Stale Closures),并说明如何解决?
解析:
这是实际业务中最容易踩坑的场景。Hooks 的运行高度依赖 JavaScript 的闭包机制。当我们在 useEffect 或定时器中引用了某个 state 时,如果依赖数组设置不当,闭包中捕获的将是“历史版本”的 state,而非最新值。
错误示例(闭包陷阱):
JavaScript
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// 这里的 count 永远是初次渲染时的 0
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 依赖数组为空,effect 只执行一次
return <div>{count}</div>;
}
解决方案:
解决此问题的核心在于打破陈旧的作用域,或者让 React 知道你需要最新的状态。
-
方案一:使用函数式更新(推荐)。
setCount接收一个函数,该函数的参数总是最新的 state。 -
方案二:利用
useRef。useRef的引用在整个组件生命周期内保持不变,可以用来存储最新值。
JavaScript
// 函数式更新解决
setCount(prevCount => prevCount + 1);
// useRef 解决(适用于需要在回调中读取最新状态但不触发渲染的场景)
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
3. React 的性能优化手段有哪些?何时应该避免使用 useMemo/useCallback?
解析:
很多开发者习惯性地为所有函数和计算结果包裹 useMemo 和 useCallback,这反而会导致性能下降。
-
React.memo:用于拦截父组件渲染引发的子组件不必要的渲染。进行的是浅比较(Shallow Compare)。 -
useMemo:缓存昂贵的计算结果。 -
useCallback:缓存函数引用,通常配合React.memo使用,防止子组件因每次接收新的函数引用而重新渲染。
反直觉考量(边界条件):
缓存机制本身是有成本的(依赖数组的对比、闭包的创建、内存占用)。如果计算过程非常简单(例如简单的加减法或少量的数组映射),使用 useMemo 的开销往往大于重新计算的开销。只有当计算复杂度极高,或该变量作为其他深层级 Hook 的依赖时,缓存才具有正向收益。
Vue 核心面试题与深度解析
1. Vue 2 和 Vue 3 的响应式原理有何本质区别?为什么要做出这种改变?
解析:
这是 Vue 最核心的基础题,涉及底层 API 的变更及随之带来的架构红利。
-
Vue 2(Object.defineProperty):
通过递归遍历对象的所有属性,将其转化为 getter/setter 来实现拦截。
缺陷:必须在初始化时知道属性的存在,无法原生监听到对象属性的添加和删除(需要
$set、$delete);无法完美监听数组的索引修改和长度变化(需要重写数组原型方法);深层对象初始化时递归开销大。 -
Vue 3(Proxy):
使用 ES6 的
Proxy直接代理整个对象,而非修改对象属性。优势:原生支持监听属性的新增和删除;原生支持监听数组变化;支持 Map、Set 等数据结构;惰性响应(只有在访问到深层属性时,才会将其转化为响应式,极大提升了初始渲染性能)。
代码演示(简易版 Proxy 响应式):
JavaScript
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖 (Track)
console.log(`读取了属性:${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 触发更新 (Trigger)
console.log(`修改了属性:${key},新值为:${value}`);
return Reflect.set(target, key, value, receiver);
}
});
}
const state = reactive({ a: 1, b: [1, 2, 3] });
state.a = 2; // 触发 set
state.b.push(4); // 触发 get 和 set,无需特殊处理数组
2. 为什么 Vue 3 要引入 Composition API(组合式 API)?它比 Options API 好在哪里?
解析:
单纯回答“为了代码组织更好”是不够的,需要从代码复用和逻辑解耦的层面深入。
在 Vue 2 的 Options API(data, methods, computed, watch)中,当一个组件极其复杂时,同一个业务逻辑的代码会被打散在不同的选项中(碎片化)。这就导致阅读和重构代码时,必须在文件上下反复跳跃。
Composition API 允许我们基于逻辑关注点来组织代码。更重要的是,它提供了一种极其优雅的逻辑提取与复用方式(Custom Hooks / Composables),彻底解决了 Vue 2 中 mixin 带来的命名冲突和数据来源不清晰的问题。
代码比对(逻辑复用):
JavaScript
// Vue 3 Composable 示例:提取鼠标位置追踪逻辑
import { ref, onMounted, onUnmounted } from 'vue';
export function useMousePosition() {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// 在组件中极度清爽地调用
// setup() { const { x, y } = useMousePosition(); return { x, y } }
3. Vue 3 在编译层面做了哪些优化(Compiler Optimizations)?
解析:
Vue 3 的性能飞跃不仅来自于响应式系统的重写,更来自于编译器在构建时所做的静态分析。Vue 突破了传统 Virtual DOM 的全量对比瓶颈。
-
静态提升(Static Hoisting): Vue 会将不包含任何动态绑定的静态节点在编译阶段提升到渲染函数外部,每次渲染直接复用,避免重新创建对象。
-
补丁标记(Patch Flags): 在动态节点上标记出哪些属性是动态的(如只绑定了 class,或只绑定了 text)。在更新时,Diff 算法只需针对带有特定 Flag 的属性进行比对,极大地减少了无用功。
-
Block Tree: 将模板结构打平,所有的动态节点被收集到一个扁平的数组中。更新时只遍历这个数组,使得 DOM 更新的复杂度与动态节点的数量成正比,而非与整个模板的节点总量成正比。
这两个框架的考察虽然侧重点不同(React 偏向不可变数据与函数式思维,Vue 偏向响应式与模板编译),但最终都在解决同一个核心问题:如何在保证开发者心智负担可控的前提下,以最小的代价将状态映射到 UI 上。
你希望我接着为你详细梳理前端工程化构建(如 Webpack/Vite)的面试题,还是深入其中一个框架的状态管理(Redux vs Pinia)的最佳实践?