前端真题 130 题 · React / Vue / Node
React 与 Vue 框架核心考点、Node.js 相关知识,80-108 题。
四、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。
为什么引入:
- 降低直接操作 DOM 的成本:真实 DOM 节点很重,频繁操作触发重排/重绘;VDOM 是纯 JS 计算,diff 后在批处理中一次性提交真实变更。注意:VDOM 不是绝对比手写 DOM 快,价值是"可维护性 + 更新可控 + 跨平台";
- 跨平台:同一套 VDOM 可渲染到 DOM / React Native / Canvas;
- 可预测、易测试。
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 更新机制:
- 调用 setState → 更新放入更新队列,标记组件待更新;
- 批处理:事件处理器/生命周期内的多次 setState 合并成一次更新(React 18 前仅事件内批处理;18
createRoot全自动批处理); - 合并后以最新 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 架构核心:
- Fiber 节点带链表指针(return/child/sibling),树变成可遍历链表;
- 可中断/可恢复:更新拆成小任务单元,帧空闲时分片执行——时间切片;
- 优先级(lane):紧急更新(输入)优先,高优打断低优;
- 双缓冲:内存构建 workInProgress 树,提交时切换 current 树;
- 渲染阶段(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 错误不处理会卸载整棵树。方案:
- 错误边界(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; }
}
- 事件错误 try/catch;Promise 用 .catch / unhandledrejection。
实践:全局边界 + 路由/模块局部边界;错误上报(sentry)。函数组件不能直接作错误边界。
考点:错误边界能力边界;Vue 对应 errorCaptured。
90. Redux 核心原则
- 单一数据源:整个应用状态在一个 store 对象树里;
- State 只读:不能直接改 state,只能通过派发 action(纯对象
{type, payload}); - 纯函数 reducer:
(state, action) => newState返回新对象、不可变更新。
store.dispatch({ type: 'ADD', payload: 1 });
配套:单向数据流(view → action → reducer → store → view);中间件处理副作用;Redux Toolkit。
考点:为什么 reducer 要纯(可预测、可回放、易测);不可变更新 vs 直接改的坑。
91. Redux 核心逻辑
从实现角度:
- createStore 生成 store,内部维护 currentState 与 listeners;
- dispatch(action):执行
reducer(state, action)存新 state,通知所有订阅者; - subscribe:注册监听(React-Redux 用它 + context 驱动更新);
- getState() 返回当前 state;
- combineReducers 按 key 合并 reducer;
- 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 性能优化
按"减少渲染 + 降低单次成本 + 加载体积"组织:
- 减少不必要的 re-render:
React.memo/PureComponent;useCallback稳定函数引用(配 memo);useMemo缓存计算;稳定 key;状态合理拆分,避免整树刷新;Context 值变化导致所有消费组件重渲染(拆分 Provider / memo 消费)。 - 降低单次成本:虚拟列表(react-window)、图片懒加载。
- 并发特性(React 18):
useTransition非紧急更新降优;useDeferredValue;Suspense 懒加载。 - 加载体积:React.lazy + Suspense 路由级拆包、tree-shaking。
- 工具: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 更新完成后执行回调。
原理:
- 改数据 → watcher 入异步更新队列(去重批处理);
- 队列 flush 在合适的宏/微任务时机统一执行;
- 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 模块机制
- 模块加载三步:路径解析 → 加载 → 包装执行。每个文件包进:
(function (exports, require, module, __filename, __dirname) { /* code */ });
module.exports是真正导出;exports只是别名——exports = {}无效;- require 缓存:首次加载缓存于
require.cache,再 require 返回缓存(单例); - 查找规则:内置模块 → 路径 → node_modules 逐级向上 → package.json main / index.js;扩展名 .js/.json/.node 尝试;
- 循环依赖:require 运行时执行,A↔B 循环时拿到的是未完成执行的部分导出(可能 undefined)。
98. require 原理
require → Module._load:
- resolve:
_resolveFilename解析成绝对路径; - 查缓存
Module._cache; - new Module 并缓存;
- 加载:按扩展名 .js(
_compile)/ .json / .node; - 编译执行:
_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):
- 切流动模式监听
data; - 每 chunk 调
writable.write;返回 false(缓冲满=背压)→pause(); - 目标
drain事件 →resume(); - 源
end→dest.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):
- fork 子进程、父进程退出;
- 子进程 setsid() 脱离控制终端;
- chdir("/")、重定向标准 IO 到日志/dev/null、设 umask。
Node 做法:进程管理工具 pm2(pm2 start 默认守护、自动重启、日志);系统级 systemd/launchd;或:
const child = spawn('node', ['server.js'], { detached: true, stdio: 'ignore' });
child.unref(); // 父进程退出不影响子进程
考点:"脱离控制终端 + 父退出不牵连";优雅停机(SIGTERM)。
104. Node 进程通信
- fork + IPC:
fork()自动建 IPC 通道,child.send+process.on('message')双向传(结构化克隆); - spawn 的 stdio 管道传数据;
- cluster worker 经 master 转发(worker 间不直连);
- Redis/消息队列做共享状态(内存不共享);
- 信号
process.kill(pid, sig)控制; - socket/HTTP 互相调用。
105. Node 异常处理
- 同步 try/catch;
- error-first 回调 / EventEmitter
'error'事件(无监听会崩); - Promise .catch / async try-catch;
- 全局兜底:
process.on('uncaughtException')与'unhandledRejection'——建议仅记录后退出重启(进程状态可能已损坏),生产靠 pm2 拉起; - express 错误中间件集中处理。
process.on('uncaughtException', (err) => { logger.error(err); process.exit(1); });
考点:区分三种异常路径;为何 uncaughtException 不建议"继续跑";"崩溃快速重启"优于"带病运行"。