← 返回题库
前端核心·真题演练

前端真题 130 题 · React / Vue / Node

React 与 Vue 框架核心考点、Node.js 相关知识,80-108 题。

ReactVueNode.js框架

四、React

84. 合成事件(SyntheticEvent)

React 事件是跨浏览器一致的合成事件:包了一层原生事件,抹平浏览器差异。

核心机制:事件委托。React 17 之前把事件监听挂到 document,17 起挂到应用根容器(root container),通过冒泡捕获后统一分发。所有事件只在根部注册(节省监听器)。

事件池(旧版):React 16 复用事件对象(pooling),异步访问 event 前需 e.persist()React 17 已移除事件池

合成事件与原生事件执行顺序

  • 原生事件先于 React 合成事件执行(React 在根部处理是冒泡末端);
  • React 事件里 e.stopPropagation() 只能阻止合成事件冒泡,阻止不了原生事件继续传播到根部之前。

要点:事件用驼峰;委托层按 target 找到 fiber 触发 handler;e.nativeEvent 取原生事件;17+ 挂根容器让多 React 根/微前端共存不冲突。

考点:委托原理、事件池变化(17)、原生与合成执行顺序。


85. Virtual DOM

虚拟 DOM:用普通 JS 对象描述真实 DOM 结构({type, props, children})。

工作流程:render 生成 VDOM → 与上次 VDOM diff → 计算最小变更集 → 批量更新真实 DOM。

为什么引入

  1. 降低直接操作 DOM 的成本:真实 DOM 节点很重,频繁操作触发重排/重绘;VDOM 是纯 JS 计算,diff 后在批处理中一次性提交真实变更。注意:VDOM 不是绝对比手写 DOM 快,价值是"可维护性 + 更新可控 + 跨平台";
  2. 跨平台:同一套 VDOM 可渲染到 DOM / React Native / Canvas;
  3. 可预测、易测试。

diff 算法核心策略(React)

  • 同层比较:只比较同层节点;
  • key 优化:列表用稳定 key 复用节点(不用 index 的坑);
  • 类型不同直接替换子树。

考点:diff 的"三假设";为什么 Vue 模板编译能静态标记(比 React 运行时 diff 有编译期优化);Fiber 让 diff 可中断。

Virtual DOM 与 diff 图解

flowchart TD
    State["状态变更 setState / 响应式触发"] --> Render["执行 render()<br/>生成新的 VNode 树"]
    Render --> Diff["diff 比较<br/>新旧两棵 VNode 树"]
    Diff --> Same{"同层节点<br/>类型相同?"}
    Same -->|不同| Replace["<b>整棵替换</b><br/>卸载旧节点 + 挂载新节点"]
    Same -->|相同| Key{"key 是否匹配?"}
    Key -->|"匹配(同 key 同类型)"| Reuse["<b>复用真实 DOM 节点</b><br/>只更新变化的属性"]
    Key -->|无 key / key 变化| Reorder["按位置比较<br/>移动 / 新增 / 删除"]
    Reuse --> Patch["收集最小变更集"]
    Replace --> Patch
    Reorder --> Patch
    Patch --> Commit["批量提交到真实 DOM<br/>(避免频繁重排)"]

    style Diff fill:#ffe6cc,stroke:#d79b00
    style Reuse fill:#d5e8d4,stroke:#82b366

读图要点:diff 的三条假设——同层比较(不跨层级)、类型不同直接整棵替换靠 key 识别节点身份
高频追问:为什么 key 不能用数组下标——列表中间插入时下标全变,导致节点被误复用(表单项串值、动画跳变)。


86. setState 过程

React 的 state 更新机制:

  1. 调用 setState → 更新放入更新队列,标记组件待更新;
  2. 批处理:事件处理器/生命周期内的多次 setState 合并成一次更新(React 18 前仅事件内批处理;18 createRoot 全自动批处理);
  3. 合并后以最新 state 重新 render → 新 VDOM → diff 提交真实 DOM。

为什么"有时同步有时异步"

  • 生命周期/合成事件里:批处理 → 看起来"异步"(立即读是旧值);
  • setTimeout/Promise/原生事件(非 React 批处理上下文,18 前):同步生效;
  • 拿最新值:setState(updater, callback) 或 useEffect;函数式 setState(prev => ...)
// React 18 createRoot 后,所有场景都自动批处理

考点:队列合并、批处理边界变化(18 全自动)、函数式 updater 意义。


87. Fiber

Fiber 是 React 16 引入的协调引擎与数据结构,解决旧递归 diff "不可中断、可能卡顿"。

要解决的问题:旧算法同步递归整棵树,一次 render 占用主线程不释放 → 卡顿。

Fiber 架构核心

  1. Fiber 节点带链表指针(return/child/sibling),树变成可遍历链表
  2. 可中断/可恢复:更新拆成小任务单元,帧空闲时分片执行——时间切片
  3. 优先级(lane):紧急更新(输入)优先,高优打断低优;
  4. 双缓冲:内存构建 workInProgress 树,提交时切换 current 树;
  5. 渲染阶段(render,可中断) vs 提交阶段(commit,不可中断)

好处:高优先级交互不卡顿(concurrent)、可恢复、为 useTransition/useDeferredValue 铺路。

考点:链表化 + 可中断 + 优先级 + 双缓冲;render 可中断、commit 不可中断。

Fiber 架构图解

flowchart TD
    Trigger["触发更新"] --> Render["<b>Render 阶段(可中断)</b><br/>beginWork 构建 Fiber 树"]
    Render --> Q{"这一帧时间<br/>还够用吗?"}
    Q -->|够| Continue["继续下一个 Fiber 单元"]
    Continue --> Q
    Q -->|不够| Yield["让出主线程<br/>交还给浏览器渲染"]
    Yield --> Resume["下一帧空闲时<br/>从断点<b>恢复</b>"]
    Resume --> Q
    Q -->|整棵树完成| Commit["<b>Commit 阶段(不可中断)</b><br/>一次性把变更刷到真实 DOM"]
    Commit --> Done["更新完成"]

    style Render fill:#dae8fc,stroke:#6c8ebf
    style Yield fill:#fff2cc,stroke:#d6b656
    style Commit fill:#f8cecc,stroke:#b85450

读图要点:Fiber 把递归 diff 改成链表遍历child / sibling / return 三个指针),从而能随时暂停与恢复。
关键结论:render 阶段可中断,commit 阶段不可中断——因为 commit 会同时改真实 DOM,中途暂停会撕裂界面。


88. 高阶组件(HOC)

HOC:接收组件、返回新组件的函数(const Enhanced = withLog(Wrapped)),复用逻辑的经典模式。

function withLogging(Wrapped) {
  return class extends React.Component {
    componentDidMount() { console.log('mounted', Wrapped.name); }
    render() { return <Wrapped {...this.props} />; }
  };
}

作用:逻辑复用(鉴权/埋点/请求)、props 注入、渲染劫持(loading、条件渲染)。

问题:props 冲突;displayName;不要在 render 里创建 HOC;ref 不透传(forwardRef);静态方法丢失;多层嵌套地狱 → Hooks 已成首选复用方案

考点:HOC / render props / hooks 三方案比较;Vue 的 mixin 类比。


89. 错误处理(React 错误边界)

渲染期间的 JS 错误不处理会卸载整棵树。方案:

  1. 错误边界(Error Boundary):类组件实现 static getDerivedStateFromError(降级 UI)与 componentDidCatch(上报)。只能捕获渲染/生命周期/构造函数错误,捕获不了事件、异步、SSR、自身错误
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() { return { hasError: true }; }
  componentDidCatch(err, info) { reportError(err, info); }
  render() { return this.state.hasError ? <Fallback/> : this.props.children; }
}
  1. 事件错误 try/catch;Promise 用 .catch / unhandledrejection。

实践:全局边界 + 路由/模块局部边界;错误上报(sentry)。函数组件不能直接作错误边界。

考点:错误边界能力边界;Vue 对应 errorCaptured


90. Redux 核心原则

  1. 单一数据源:整个应用状态在一个 store 对象树里;
  2. State 只读:不能直接改 state,只能通过派发 action(纯对象 {type, payload});
  3. 纯函数 reducer(state, action) => newState 返回新对象、不可变更新。
store.dispatch({ type: 'ADD', payload: 1 });

配套:单向数据流(view → action → reducer → store → view);中间件处理副作用;Redux Toolkit。

考点:为什么 reducer 要纯(可预测、可回放、易测);不可变更新 vs 直接改的坑。


91. Redux 核心逻辑

从实现角度:

  1. createStore 生成 store,内部维护 currentState 与 listeners;
  2. dispatch(action):执行 reducer(state, action) 存新 state,通知所有订阅者;
  3. subscribe:注册监听(React-Redux 用它 + context 驱动更新);
  4. getState() 返回当前 state;
  5. combineReducers 按 key 合并 reducer;
  6. applyMiddleware 增强 dispatch——洋葱中间件链 (store) => next => action => ...(redux-thunk 让 dispatch 接收函数)。
function createStore(reducer, preloaded) {
  let state = preloaded, listeners = [];
  return {
    getState: () => state,
    dispatch: (action) => { state = reducer(state, action); listeners.forEach(l => l()); return action; },
    subscribe: (l) => { listeners.push(l); return () => (listeners = listeners.filter(x => x !== l)); },
  };
}

考点:单向数据流闭环;thunk 原理;useSelector 订阅与浅比较。


92. React 性能优化

按"减少渲染 + 降低单次成本 + 加载体积"组织:

  1. 减少不必要的 re-renderReact.memo / PureComponentuseCallback 稳定函数引用(配 memo);useMemo 缓存计算;稳定 key;状态合理拆分,避免整树刷新;Context 值变化导致所有消费组件重渲染(拆分 Provider / memo 消费)。
  2. 降低单次成本:虚拟列表(react-window)、图片懒加载。
  3. 并发特性(React 18)useTransition 非紧急更新降优;useDeferredValue;Suspense 懒加载。
  4. 加载体积:React.lazy + Suspense 路由级拆包、tree-shaking。
  5. 工具:Profiler API / DevTools Profiler 定位高耗时渲染。

经典反例:inline 回调导致 memo 失效(需 useCallback);Context 值没 memo 引发大面积刷新。


五、Vue

93. Vue 数据绑定原理

Vue 2:Object.defineProperty

  • 遍历 data 给每个属性定义 getter/setter;
  • getter 收集依赖(Dep 记录 Watcher),setter 派发更新
  • 每个组件一个 Watcher,渲染读取过的属性建立依赖;
  • 缺陷:数组索引/长度变更、新增/删除属性侦测不到 → 需 Vue.set/$set;深层对象递归劫持开销大。

Vue 3:Proxy

  • new Proxy(target, handler) 代理整个对象:get 收集依赖、set 触发更新
  • 惰性代理、可侦测增删属性、数组原生支持;
  • 依赖用 WeakMap 存 target→Map(key→Set(effect)),实现"哪个 key 变了通知哪个 effect"的精确追踪 + 惰性代理(不访问不收集),依赖更精确、开销更小

    ⚠️ 别答"可中断":Vue 3 的 track/trigger同步、不可中断的。"可中断/时间切片"是 React Fiber 的特性,两者常被混淆。

const deps = new WeakMap();  // target -> Map(key -> Set(effect))
function track(target, key) { /* 收集 activeEffect */ }
function trigger(target, key) { /* 触发 key 的 effect */ }

编译期优化(Vue 3):静态标记 patchFlag/静态提升,diff 只比较动态部分。

考点:defineProperty vs Proxy 对比表;依赖收集/派发闭环;Vue2 数组响应式 hack。


94. computed 和 watch

  • computed:基于响应式数据派生值,有缓存(依赖不变不重算),适合模板表达式;要求无副作用。
  • watch:侦听数据变化执行回调(异步/副作用场景),不缓存;可配置 deep/immediate;watchEffect 自动追踪。
const count = ref(1);
const double = computed(() => count.value * 2);   // 缓存
watch(count, (nv, ov) => console.log('变化', nv, ov), { deep: true });

对比:computed="派生 + 缓存",watch="观察变化 + 副作用";能不用 watch 就不用。


95. slot(插槽)

Vue 插槽做组件内容分发

  • 默认插槽 <slot/>
  • 具名插槽 <slot name="header"/> + <template v-slot:header>#header);
  • 作用域插槽:子组件暴露数据 <slot :item="item">,父用 v-slot="{ item }"——"子定数据、父定渲染"。

<main><slot :data="list" /></main>

<Card>
  <template #default="{ data }">{{ data }}</template>
</Card>

原理:插槽内容在父作用域编译(普通插槽);Vue3 插槽是函数(性能好)。React 类比:children / render props。


96. nextTick 原理

问题:Vue 数据更新异步(更新队列批处理),nextTick 在 DOM 更新完成后执行回调。

原理

  1. 改数据 → watcher 入异步更新队列(去重批处理);
  2. 队列 flush 在合适的宏/微任务时机统一执行;
  3. nextTick 回调在 flush 之后执行 → DOM 已更新。

实现:Vue2 微任务优先(Promise.then),降级 MutationObserver/setImmediate/setTimeout;Vue3 用 Promise,nextTick 返回 Promise 可 await。

this.count = 2;
await this.$nextTick();   // DOM 已更新

考点:为什么不能同步更新 DOM(批处理性能);与微任务的关系;与 React setState 批处理对比。


六、Node.js

97. Node 模块机制

  1. 模块加载三步:路径解析 → 加载 → 包装执行。每个文件包进:
(function (exports, require, module, __filename, __dirname) { /* code */ });
  1. module.exports 是真正导出;exports 只是别名——exports = {} 无效;
  2. require 缓存:首次加载缓存于 require.cache,再 require 返回缓存(单例);
  3. 查找规则:内置模块 → 路径 → node_modules 逐级向上 → package.json main / index.js;扩展名 .js/.json/.node 尝试;
  4. 循环依赖:require 运行时执行,A↔B 循环时拿到的是未完成执行的部分导出(可能 undefined)。

98. require 原理

requireModule._load

  1. resolve_resolveFilename 解析成绝对路径;
  2. 查缓存 Module._cache
  3. new Module 并缓存;
  4. 加载:按扩展名 .js(_compile)/ .json / .node;
  5. 编译执行_compile 用 vm 执行包装后的源码,require/exports 等由包装函数注入,返回 module.exports
function myRequire(id) {
  const path = Module._resolveFilename(id);
  if (Module._cache[path]) return Module._cache[path].exports;
  const module = Module._cache[path] = new Module(path);
  module.load(path);
  return module.exports;
}

99. Node 事件循环

libuv 六阶段:

timers → pending I/O → idle/prepare → poll → check(setImmediate) → close callbacks

关键

  • 每阶段间先清 process.nextTick 再清 Promise 微任务(nextTick 优先);
  • setImmediate(check)与 setTimeout(0)(timers)在主模块中顺序不定;在 I/O 回调内部 setImmediate 恒先于 setTimeout;
  • nextTick 不属于循环阶段,当前操作结束后立即执行。

优先级:nextTick > Promise > 宏任务

考点:画六阶段图;Node 与浏览器事件循环差异。

Node 事件循环六阶段图解

flowchart TD
    Timers["① timers<br/>setTimeout / setInterval 到期回调"] --> Pending["② pending callbacks<br/>被推迟的 I/O 回调"]
    Pending --> Idle["③ idle / prepare<br/>(Node 内部使用)"]
    Idle --> Poll["④ poll<br/><b>核心阶段</b><br/>等待 I/O 事件、执行 I/O 回调"]
    Poll --> Check["⑤ check<br/>setImmediate 回调"]
    Check --> Close["⑥ close callbacks<br/>socket 关闭等"]
    Close --> Timers

    Poll -.->|"每阶段之间都会清空"| NextTick["process.nextTick 队列<br/><b>优先级最高</b>"]
    NextTick -.-> Micro["Promise 微任务队列"]

    style Poll fill:#ffe6cc,stroke:#d79b00
    style NextTick fill:#f8cecc,stroke:#b85450

读图要点每个阶段之间都会先清空 nextTick 队列,再清空 Promise 微任务队列——所以 nextTick 优先于 Promise。
高频追问:setTimeout(fn, 0)setImmediate(fn) 谁先执行?——在 I/O 回调里 setImmediate(poll 阶段后直接进 check),在顶层代码里则不确定(取决于进程启动耗时)。


100. cluster 原理

利用多核的多进程模型:master fork 多个 worker 处理请求。

  • cluster.isPrimary 区分主/子进程角色(Node 16+ 的正式名称);旧名 cluster.isMaster 已废弃,仍可用但不要在新代码里写;
  • 共享端口:master 监听并把连接分发给 worker(RR 轮询或内核 SO_REUSEPORT 分发);
  • worker 间经 master 转发(IPC)通信;内存不共享 → Redis 等外部存储。
if (cluster.isPrimary) {
  for (let i = 0; i < os.cpus().length; i++) cluster.fork();
  cluster.on('exit', () => cluster.fork());   // 崩溃自动重启
} else {
  http.createServer((req, res) => res.end('ok')).listen(3000);
}

优势:多核、worker 隔离、自动重启。pm2 cluster 模式底层即 cluster。


101. 流机制(Stream)

Stream 把大数据分块处理(chunk),解决内存耗尽与等待延迟。

四种类型:Readable(读)、Writable(写)、Duplex(双工,socket)、Transform(转换,zlib/crypto)。

关键机制

  • 背压(backpressure):消费慢时暂停读取(highWaterMark 缓冲阈值,默认 64KB),避免内存无限增长;pipe 已内置处理;
  • 事件:data/end/error/finish。
fs.createReadStream('a.zip').pipe(fs.createWriteStream('b.zip'));   // 大文件内存占用稳定

102. pipe 原理

readable.pipe(writable)

  1. 切流动模式监听 data
  2. 每 chunk 调 writable.write;返回 false(缓冲满=背压)→ pause()
  3. 目标 drain 事件 → resume()
  4. enddest.end();双方 error 处理。
Readable.prototype.pipe = function (dest) {
  this.on('data', (chunk) => { if (!dest.write(chunk)) this.pause(); });
  dest.on('drain', () => this.resume());
  this.on('end', () => dest.end());
  return dest;
};

考点:pipe = "背压驱动的搬运工";现代推荐 stream.pipeline(统一错误管理)。


103. 守护进程(Daemon)

脱离终端、后台运行、不随会话退出而终止。核心(类 Unix):

  1. fork 子进程、父进程退出;
  2. 子进程 setsid() 脱离控制终端;
  3. chdir("/")、重定向标准 IO 到日志/dev/null、设 umask。

Node 做法:进程管理工具 pm2pm2 start 默认守护、自动重启、日志);系统级 systemd/launchd;或:

const child = spawn('node', ['server.js'], { detached: true, stdio: 'ignore' });
child.unref();    // 父进程退出不影响子进程

考点:"脱离控制终端 + 父退出不牵连";优雅停机(SIGTERM)。


104. Node 进程通信

  1. fork + IPCfork() 自动建 IPC 通道,child.send + process.on('message') 双向传(结构化克隆);
  2. spawn 的 stdio 管道传数据;
  3. cluster worker 经 master 转发(worker 间不直连);
  4. Redis/消息队列做共享状态(内存不共享);
  5. 信号 process.kill(pid, sig) 控制;
  6. socket/HTTP 互相调用。

105. Node 异常处理

  1. 同步 try/catch;
  2. error-first 回调 / EventEmitter 'error' 事件(无监听会崩);
  3. Promise .catch / async try-catch;
  4. 全局兜底:process.on('uncaughtException')'unhandledRejection'——建议仅记录后退出重启(进程状态可能已损坏),生产靠 pm2 拉起;
  5. express 错误中间件集中处理。
process.on('uncaughtException', (err) => { logger.error(err); process.exit(1); });

考点:区分三种异常路径;为何 uncaughtException 不建议"继续跑";"崩溃快速重启"优于"带病运行"。