# 2026 前端面试题大全:最新面经与框架真题
这份 2026 前端面试题与面经按技术方向持续更新,每道题都提供 30 秒速记、真实示例代码、可交互 DEMO 和面试官追问,适合校招、社招以及中高级前端面试前集中复习。
2026 年的前端面试没有放弃 JavaScript、CSS、浏览器和网络基础,但更强调把原理放进真实项目:排查 React 闭包与渲染性能、解释 Vue 3 响应式、选择 Webpack 或 Vite、划分 Next.js Server Component,以及处理大模型流式输出和审查 AI 生成代码。
# 2026 前端面试题分类
- 基础核心:JavaScript 与浏览器、CSS 与现代布局、TypeScript、浏览器与网络进阶
- 主流框架:React 19 与 Next.js、Vue 3、Angular、Svelte 与 Nuxt
- 工程化:Webpack、Vite、Monorepo 与包工程、测试与质量
- 渲染与架构:服务端渲染与同构、微前端与模块联邦、性能与安全
- 跨端开发:Electron、React Native、Flutter、小程序、Taro 与跨端
- AI 与服务端:AI 前端高频题、AI 应用工程、Node.js 与服务端 JavaScript
如果复习时间有限,可以先按前端面试题复习路线补基础,再用本页做 2026 高频场景自测;临近面试时再结合真实公司前端面经模拟追问。
# JavaScript 与浏览器
# 微任务为什么可能让页面“卡死”?
30 秒速记
- 一个任务结束后,会处理可执行微任务
- 微任务可以追加微任务,检查点会继续处理
- 持续追加可能饿死渲染、输入与定时器
await Promise.resolve()仍然只让给微任务- 长工作分片,并让出到后续任务或工作线程
微任务把页面拖住,往往是因为队列一直清不空。 每个回调可能只做一点事,却又立即追加下一项,浏览器就迟迟没机会处理输入、定时器和绘制。把循环改成不断等待已成功的期约也可能发生同样问题,因为仍在微任务之间接力。长计算要分批设上限,并适时让出到后续任务;适合独立计算的工作,可以移到工作线程。
面试官追问
追问 1循环里加了 await Promise.resolve(),为什么按钮还是点不动?
它只让出当前异步续体,后续又进入微任务。队列持续有新任务时,输入和绘制仍可能等不到机会。
追问 2每处理一百项用定时器续跑,就一定流畅吗?
不保证,一百项的耗时可能差异很大。可以按实际耗时预算切片,并保留取消条件。
追问 3用 requestAnimationFrame 处理大量计算合适吗?
它在绘制前运行,回调里做太多工作反而会拖慢这一帧。适合短小的视觉更新,重计算仍要拆分或转移。
追问 4怎么确认是微任务饿死,而不是某个单次长函数?
录制性能轨迹,看是否连续执行期约或微任务回调,中间长期没有绘制与输入处理。再追踪是谁持续创建下一项。
# React 中的闭包为什么会读到旧状态?
30 秒速记
- 每次渲染是一份状态快照,回调保留那次闭包
- 长期定时器与订阅最容易读到旧快照
- 只依赖前值:
setCount(c => c + 1) - 需要重新同步的值保留依赖,不随意省略
- 支持时用
useEffectEvent分离副作用里的事件逻辑
旧闭包不是状态没更新,而是回调仍在读取创建它时那次渲染的变量。 空依赖的副作用注册一个长期定时器后,后续渲染不会自动替换这个回调。只需要根据前值更新时,函数式更新最直接;需要随参数重新连接时,就保留依赖并重新同步。确实只想在副作用事件触发时读取最新值,可以按版本使用 useEffectEvent,不能借它隐藏真正的依赖。
计数器只需要前值,函数式更新最直接;更新函数在 React 处理状态队列时接收前一个待处理状态,因此 Effect 不必读取 count,也不会每次重建定时器。
import { useEffect, useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = window.setInterval(() => {
setCount((current) => current + 1)
}, 1000)
return () => window.clearInterval(timer)
}, [])
return <output>{count}</output>
}
不要机械地把每个值塞进依赖数组。先判断 Effect 是否应该随它重新同步;如果只是回调内部要读最新值,拆分事件逻辑比反复退订、重订更准确。
面试官追问
追问 1计数器一直从零加到一,哪种改法最直接?
把 setCount(count + 1) 改成 setCount(c => c + 1)。更新函数读取队列中的前一个状态,不依赖旧回调捕获的数字。
追问 2聊天室房间变了,能用最新值读取来避免重新连接吗?
不行,房间标识变化本来就应该重新同步连接。它应是副作用依赖,读取最新值不能替代重新订阅。
追问 3只改变通知主题,不想断开聊天室,怎么拆?
把连接依赖保留为房间标识,通知逻辑在副作用事件触发时读最新主题。支持 useEffectEvent 的版本可表达这种区别。
追问 4用 ref 保存最新值,就能删除所有依赖吗?
不能,引用更新不触发重新渲染或重新同步。先判断值变化是否应改变外部连接,再决定状态、依赖和引用各自的职责。
# 如何实现可取消、不会串结果的搜索防抖?
30 秒速记
- 防抖控制何时发请求,序号控制谁能提交结果
- 输入变化立即清定时器、失效旧序号并尝试取消
- 每次请求绑定自己的
AbortController - 返回数据后再检查身份,统一提交界面
- 卸载清理;空输入和组合输入单独处理
防抖只减少请求次数,防止串结果还要在提交结果前检查请求是否过期。 输入一变,就清理待触发的定时器、让旧请求序号失效,并尝试取消旧操作;新请求返回后,只有序号仍匹配才能更新界面。取消能减少浪费,但不能作为唯一保障。搜索函数最好只返回数据,由经过检查的出口提交结果,卸载时再清理定时器与请求。
export function createSearch<T>(
search: (q: string, signal: AbortSignal) => Promise<T>,
onResult: (value: T) => void,
onError: (error: unknown) => void,
) {
let timer: ReturnType<typeof setTimeout> | undefined
let controller: AbortController | undefined
let version = 0
function cancel() {
version++ // 输入一变就让旧结果失效,不等新请求发出
if (timer !== undefined) clearTimeout(timer)
timer = undefined
controller?.abort()
controller = undefined
}
function run(query: string) {
cancel()
const q = query.trim()
if (!q) return // 调用方同步清空搜索结果
const currentVersion = version
const currentController = new AbortController()
controller = currentController
timer = setTimeout(async () => {
timer = undefined
try {
const value = await search(q, currentController.signal)
if (currentVersion === version && !currentController.signal.aborted) {
onResult(value)
}
} catch (error) {
if (currentVersion === version && !currentController.signal.aborted) {
onError(error)
}
} finally {
if (controller === currentController) controller = undefined
}
}, 300)
}
return { run, cancel } // 输入时 run,组件卸载时 cancel
}
空关键词时调用方同步清空结果;组合输入期间暂停提交,结束后再调用 run。搜索函数只返回数据,提交界面统一放在通过序号检查的 onResult 中;abort 用来减少无用工作,版本检查负责阻止旧结果覆盖新输入。调用方提供明确的 onError,避免在定时器的异步回调中抛出未处理拒绝。
面试官追问
追问 1先搜短词再搜长词,旧请求最后回来,怎么挡住?
每次输入递增版本号,提交结果前比较本次版本与当前版本。不匹配就丢弃,不能只比较请求是否成功。
