综合场景面试题
前端面试题 - 综合场景篇
本章节收录前端综合场景面试题,重点考察候选人的知识体系整合能力和实际问题解决能力。涵盖从URL输入到页面加载、性能优化实战、业务场景实现等高频面试题目。
📊 数据统计
| 统计项 | 数值 |
|---|---|
| 总题数 | 45道 |
| 基础题 | 12道 (26.7%) |
| 中级题 | 23道 (51.1%) |
| 高级题 | 10道 (22.2%) |
📚 目录
一、从URL到页面加载
1. 从输入URL到页面加载完成,经历了哪些过程?
难度:⭐⭐⭐(高级)
标签:#浏览器 #网络 #渲染 #高频
问题描述:
详细描述用户在浏览器地址栏输入URL并按下回车,到页面完全展示出来的整个过程。
参考答案要点:
整个过程可分为网络阶段、解析阶段、渲染阶段三个大阶段:
网络阶段
-
URL解析
- 协议解析(http/https)
- 域名解析
- 路径和参数解析
- HSTS检查(强制HTTPS)
-
DNS解析(域名→IP)
- 浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS → 根域名服务器 → 顶级域名服务器 → 权威域名服务器
- DNS预解析优化:
<link rel="dns-prefetch" href="//example.com">
-
建立TCP连接(三次握手)
- 客户端发送SYN(seq=x)
- 服务端返回SYN+ACK(seq=y, ack=x+1)
- 客户端发送ACK(ack=y+1)
- HTTPS还需TLS握手(交换加密算法、证书验证、密钥协商)
-
发送HTTP请求
- 构建请求报文(请求行、请求头、请求体)
- 发送请求到服务器
-
服务器处理并返回响应
- 服务器接收请求
- 后端处理(路由、数据库查询等)
- 返回HTTP响应(状态码、响应头、响应体)
解析阶段
-
浏览器接收响应
- 解析响应头(Content-Type、Content-Length等)
- 根据Content-Type决定处理方式
-
解析HTML构建DOM树
- 词法分析(Token化)
- 语法分析(构建DOM树)
- 遇到
<script>会阻塞解析(除非有defer/async) - 遇到
<link>会并行请求CSS
-
解析CSS构建CSSOM树
- 解析CSS文件和
<style>标签 - 构建CSSOM树
- CSS解析不会阻塞HTML解析,但会阻塞渲染
- 解析CSS文件和
渲染阶段
-
构建渲染树(Render Tree)
- 合并DOM树和CSSOM树
- 排除
display: none的节点 - 计算每个节点的样式
-
布局(Layout/Reflow)
- 计算每个元素的位置和大小
- 生成布局树(Layout Tree)
-
绘制(Paint)
- 将渲染树转换为屏幕上的像素
- 分层绘制(背景、文字、边框等)
-
合成(Composite)
- 将多个图层合成最终页面
- GPU加速合成
-
断开连接(四次挥手,可选)
- Connection: keep-alive保持长连接
- 或四次挥手断开TCP连接
关键优化点:
- DNS预解析
- TCP长连接
- 资源预加载(preload/prefetch)
- 减少阻塞资源
- 启用HTTP/2或HTTP/3
2. DNS解析的详细过程是怎样的?
难度:⭐⭐(中级)
标签:#DNS #网络
问题描述:
详细描述DNS解析的完整流程,包括缓存查询顺序和递归查询过程。
参考答案要点:
DNS查询顺序(从近到远)
- 浏览器缓存(Chrome约1分钟,TTL控制)
- 操作系统缓存(hosts文件 + DNS客户端服务)
- 路由器缓存(本地DNS服务器)
- ISP DNS服务器(运营商DNS,如114.114.114.114)
- 根域名服务器(全球13组,返回顶级域名服务器地址)
- 顶级域名服务器(如.com、.cn,返回权威DNS服务器)
- 权威域名服务器(域名注册商提供,返回最终IP)
DNS解析类型
- 递归查询:客户端→本地DNS→根→顶级→权威,层层返回
- 迭代查询:本地DNS分别查询根、顶级、权威
DNS优化
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com" />
<!-- 预连接(DNS+TCP+TLS) -->
<link rel="preconnect" href="https://cdn.example.com" />3. TCP三次握手和四次挥手的详细过程?
难度:⭐⭐⭐(高级)
标签:#TCP #网络
问题描述:
详细解释TCP三次握手建立连接和四次挥手断开连接的过程,以及为什么是三次和四次。
参考答案要点:
三次握手(建立连接)
客户端 服务端
| |
|-------- SYN,seq=x ----->| (同步序列号)
| |
|<-- SYN,seq=y,ACK=x+1 ---| (同步+确认)
| |
|-------- ACK,y+1 ------->| (确认)
| |- 第一次握手:客户端发送SYN包(seq=x),进入SYN_SENT状态
- 第二次握手:服务端收到SYN,发送SYN+ACK包(seq=y, ack=x+1),进入SYN_RECV状态
- 第三次握手:客户端收到SYN+ACK,发送ACK包(ack=y+1),双方进入ESTABLISHED状态
为什么是三次?
- 防止历史重复连接初始化(已失效的连接请求报文突然到达)
- 同步双方初始序列号
- 确认双方收发能力正常
四次挥手(断开连接)
客户端 服务端
| |
|-------- FIN ------------>| (我要关闭)
| |
|<-------- ACK ------------| (知道了)
| |
|<-------- FIN ------------| (我也要关闭)
| |
|-------- ACK ------------>| (知道了)
| |- 第一次挥手:主动方发送FIN包,进入FIN_WAIT_1状态
- 第二次挥手:被动方收到FIN,发送ACK包,进入CLOSE_WAIT状态
- 第三次挥手:被动方发送FIN包,进入LAST_ACK状态
- 第四次挥手:主动方收到FIN,发送ACK包,进入TIME_WAIT状态(等待2MSL后关闭)
为什么是四次?
- TCP全双工通信,每个方向需要单独关闭
- 被动方收到FIN后,可能还有数据要发送
TIME_WAIT作用:
- 确保最后一个ACK能被对方收到(可重传)
- 防止已失效的连接请求报文出现在本连接中
4. HTTPS的握手过程是怎样的?
难度:⭐⭐⭐(高级)
标签:#HTTPS #TLS #安全
问题描述:
详细描述HTTPS建立安全连接的TLS握手过程。
参考答案要点:
TLS 1.2握手过程(2-RTT)
-
Client Hello
- 客户端支持的TLS版本
- 支持的加密算法套件(Cipher Suites)
- 客户端随机数(Client Random)
- 支持的压缩方法
-
Server Hello
- 确认的TLS版本
- 选定的加密算法套件
- 服务端随机数(Server Random)
- 服务器证书(含公钥)
- 可选:请求客户端证书
-
客户端验证与密钥交换
- 验证服务器证书(CA签名、有效期、域名匹配)
- 生成预主密钥(Pre-Master Secret)
- 用服务器公钥加密预主密钥发送
- 发送Change Cipher Spec(之后加密通信)
- 发送Finished消息(验证握手完整性)
-
服务端确认
- 用私钥解密预主密钥
- 发送Change Cipher Spec
- 发送Finished消息
-
生成会话密钥
- 客户端随机数 + 服务端随机数 + 预主密钥 → 主密钥
- 主密钥派生出对称加密密钥、MAC密钥、IV
TLS 1.3优化(1-RTT或0-RTT)
- 简化握手流程
- 支持0-RTT恢复(基于PSK)
- 移除过时算法
5. 浏览器如何解析和渲染页面?
难度:⭐⭐⭐(高级)
标签:#浏览器 #渲染 #DOM #CSSOM
问题描述:
详细描述浏览器从接收HTML到渲染页面的完整过程。
参考答案要点:
渲染流程
HTML → DOM Tree
↓
CSS → CSSOM Tree
↓
Render Tree(渲染树)
↓
Layout(布局/重排)
↓
Paint(绘制/重绘)
↓
Composite(合成)-
构建DOM树
- 词法分析:将HTML字符串转换为Token
- 语法分析:根据Token构建DOM节点
- 遇到
<script>阻塞解析(除非defer/async) - 遇到
<link>并行加载CSS
-
构建CSSOM树
- 解析CSS文件和
<style>标签 - CSSOM是样式对象的树形结构
- 样式计算(Computed Style)
- 解析CSS文件和
-
构建渲染树
- 合并DOM和CSSOM
- 只包含可见节点(排除
display: none) - 计算每个节点的最终样式
-
布局(Layout/Reflow)
- 计算每个元素的几何信息(位置、大小)
- 生成布局树(Layout Tree)
- 百分比、auto等值转为绝对像素
-
绘制(Paint)
- 将渲染树转为屏幕像素
- 分层绘制(背景、文字、边框等)
- 生成绘制记录(Paint Records)
-
合成(Composite)
- 将页面分层(Layer)
- GPU合成图层(Composite Layers)
- 最终显示到屏幕
关键概念
- Reflow(重排):布局或几何属性变化,重新计算布局
- Repaint(重绘):外观变化但不影响布局,重新绘制
- Composite(合成):transform/opacity变化,直接合成
性能损耗:Reflow > Repaint > Composite
二、浏览器渲染原理
6. 什么是重排(Reflow)和重绘(Repaint)?如何优化?
难度:⭐⭐⭐(高级)
标签:#性能优化 #渲染 #重排 #重绘
问题描述:
解释重排和重绘的区别,列举触发条件,并提供优化方案。
参考答案要点:
重排(Reflow/Layout)
定义:当元素的几何属性(位置、大小)发生变化,浏览器需要重新计算布局。
触发条件:
- 修改元素尺寸(width/height/padding/margin)
- 修改元素位置(top/left/right/bottom)
- 改变显示属性(display: none/block)
- 修改内容(文本、图片大小变化)
- 修改字体大小
- 激活CSS伪类(:hover)
- 查询布局属性(offsetHeight/scrollTop/clientWidth等)
- 浏览器窗口resize
影响范围:
- 局部重排:修改元素不影响其他元素布局
- 全局重排:修改元素影响整个文档布局
重绘(Repaint/Paint)
定义:当元素的外观属性变化但不影响布局时,浏览器重新绘制元素。
触发条件:
- 修改颜色(color/background-color)
- 修改背景(background-image/background-position)
- 修改边框样式(border-color)
- 修改阴影(box-shadow)
- 修改透明度(opacity)
- 修改文字装饰(text-decoration)
优化策略
-
减少Reflow次数
JAVASCRIPT// ❌ 坏做法:多次触发Reflow const el = document.getElementById("box") el.style.width = "100px" console.log(el.offsetHeight) // 强制Reflow el.style.height = "200px" // ✅ 好做法:批量修改 const el = document.getElementById("box") el.classList.add("new-size") // 一次性修改 -
使用DocumentFragment
JAVASCRIPT// ✅ 批量DOM操作 const fragment = document.createDocumentFragment() for (let i = 0; i < 1000; i++) { const li = document.createElement("li") fragment.appendChild(li) } ul.appendChild(fragment) // 只触发一次Reflow -
离线修改
JAVASCRIPT// ✅ 先隐藏,修改完再显示 el.style.display = "none" // ... 大量修改 ... el.style.display = "block" // 只触发2次Reflow -
使用transform和opacity
CSS/* ✅ GPU加速,不触发Reflow/Repaint */ .animated { transform: translateX(100px); opacity: 0.5; will-change: transform, opacity; } -
避免table布局
- table布局需要多次计算,任何单元格变化都会导致整个table重排
-
缓存布局属性
JAVASCRIPT// ❌ 多次查询触发Reflow for (let i = 0; i < len; i++) { arr[i] = el.offsetHeight } // ✅ 缓存后使用 const height = el.offsetHeight for (let i = 0; i < len; i++) { arr[i] = height } -
使用requestAnimationFrame
JAVASCRIPT// ✅ 与渲染帧同步 function animate() { requestAnimationFrame(() => { // 修改样式 updateStyle() }) }
7. 什么是合成层(Composite Layer)?如何创建?
难度:⭐⭐⭐(高级)
标签:#渲染 #GPU #合成层
问题描述:
解释浏览器合成层的概念,以及如何手动创建合成层优化性能。
参考答案要点:
合成层概念
浏览器渲染时,会将页面分为多个图层(Layer),每个图层独立绘制,最后GPU合成。合成层的变化不会影响其他图层,避免重排和重绘。
创建合成层的条件
自动创建:
- 3D变换(transform: translate3d/rotate3d)
- opacity动画
- transform动画
- will-change: transform/opacity
- video/canvas/iframe元素
- position: fixed
- 重叠在合成层上的元素
手动创建:
.gpu-layer {
transform: translateZ(0);
/* 或 */
transform: translate3d(0, 0, 0);
/* 或 */
will-change: transform;
}合成层优化
优点:
- 避免重排和重绘
- GPU加速,适合动画
- 减少主线程压力
缺点:
- 占用更多内存
- 过多图层导致合成开销
- 可能引发性能问题
最佳实践:
- 只为需要动画的元素创建合成层
- 动画结束后移除will-change
- 避免过多图层(建议不超过100个)
8. script标签的defer和async有什么区别?
难度:⭐⭐(中级)
标签:#浏览器 #JavaScript #性能
问题描述:
详细解释script标签的defer和async属性的区别,以及使用场景。
参考答案要点:
默认行为(无defer/async)
- 遇到
<script>立即下载并执行 - 阻塞HTML解析
- 阻塞DOM构建
- 阻塞后续资源下载
async属性
<script async src="script.js"></script>- 下载:不阻塞HTML解析(并行下载)
- 执行:下载完成后立即执行,阻塞HTML解析
- 顺序:不保证执行顺序(谁先下载完谁先执行)
- 适用:独立脚本,不依赖其他脚本(如统计代码、广告)
defer属性
<script defer src="script.js"></script>- 下载:不阻塞HTML解析(并行下载)
- 执行:HTML解析完成后、DOMContentLoaded事件前执行
- 顺序:保证执行顺序(按出现顺序)
- 适用:依赖DOM或其他脚本的脚本
对比总结
| 特性 | 无属性 | async | defer |
|---|---|---|---|
| 下载阻塞 | 是 | 否 | 否 |
| 执行阻塞 | 是 | 是(下载完) | 否 |
| 执行时机 | 立即 | 下载完 | DOM构建后 |
| 执行顺序 | 按顺序 | 不保证 | 按顺序 |
| DOM就绪 | 否 | 否 | 是 |
最佳实践
<!-- 首屏关键JS -->
<script src="critical.js"></script>
<!-- 非关键JS -->
<script defer src="app.js"></script>
<!-- 独立第三方脚本 -->
<script async src="analytics.js"></script>9. 什么是Critical Rendering Path(关键渲染路径)?如何优化?
难度:⭐⭐⭐(高级)
标签:#性能优化 #渲染
问题描述:
解释关键渲染路径的概念,以及如何优化以提升首屏加载速度。
参考答案要点:
关键渲染路径定义
浏览器将HTML、CSS、JavaScript转换为屏幕上像素所经历的步骤序列。优化目标是最小化首屏渲染时间。
关键资源
- 关键资源:阻塞首次渲染的资源
- 关键路径长度:关键资源数量和大小
- 关键字节数:关键资源的总大小
优化策略
-
最小化关键资源
- 内联关键CSS(
<style>标签) - 延迟加载非关键CSS(
media="print"+onload) - 异步加载非关键JS(defer/async)
- 内联关键CSS(
-
优化关键资源大小
- 压缩(Gzip/Brotli)
- 代码分割(Code Splitting)
- Tree Shaking
-
优化加载顺序
HTML<!-- 1. 关键CSS内联 --> <style> /* 首屏CSS */ </style> <!-- 2. 预加载关键资源 --> <link rel="preload" href="critical.js" as="script" /> <!-- 3. 异步加载非关键CSS --> <link rel="preload" href="non-critical.css" as="style" onload="this.rel='stylesheet'" /> <!-- 4. defer加载JS --> <script defer src="app.js"></script> -
使用Preload/Prefetch
HTML<!-- 预加载当前页面关键资源 --> <link rel="preload" href="font.woff2" as="font" crossorigin /> <!-- 预获取下一页面资源 --> <link rel="prefetch" href="next-page.js" /> <!-- DNS预解析 --> <link rel="dns-prefetch" href="//api.example.com" /> <!-- 预连接 --> <link rel="preconnect" href="https://cdn.example.com" />
三、性能优化实战
10. 如何统计全站每个静态资源的加载耗时?
难度:⭐⭐⭐(高级)
标签:#性能监控 #Performance API
问题描述:
设计一个方案,统计网站所有静态资源(JS、CSS、图片等)的加载耗时。
参考答案要点:
方案一:Performance API
// 使用PerformanceObserver监听资源加载
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === "resource") {
console.log({
name: entry.name, // 资源URL
type: entry.initiatorType, // 资源类型(script/link/img)
duration: entry.duration, // 加载耗时
startTime: entry.startTime,
// 详细时间分解
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
request: entry.responseStart - entry.requestStart,
response: entry.responseEnd - entry.responseStart,
})
}
}
})
observer.observe({ entryTypes: ["resource"] })方案二:Performance Timeline
// 获取所有资源加载数据
window.addEventListener("load", () => {
const resources = performance.getEntriesByType("resource")
const data = resources.map((r) => ({
name: r.name,
type: r.initiatorType,
duration: r.duration,
size: r.transferSize, // 传输大小
decodedSize: r.decodedBodySize,
}))
// 上报到监控系统
reportToServer(data)
})方案三:结合错误监控
// 监控资源加载失败
window.addEventListener(
"error",
(e) => {
if (e.target.tagName) {
console.error("资源加载失败:", {
url: e.target.src || e.target.href,
type: e.target.tagName,
})
}
},
true
)优化建议
- 缓存策略:分析缓存命中率,优化Cache-Control
- CDN优化:分析不同CDN节点性能
- 资源合并:减少HTTP请求数
- 预加载:识别关键资源进行preload
- 懒加载:非首屏资源懒加载
11. 如何实现前端性能监控SDK?
难度:⭐⭐⭐(高级)
标签:#性能监控 #SDK设计
问题描述:
设计一个前端性能监控SDK,采集Web Vitals等关键性能指标。
参考答案要点:
核心指标
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.init()
}
init() {
this.observeNavigation()
this.observePaint()
this.observeLCP()
this.observeCLS()
this.observeFID()
this.observeResources()
}
// 导航计时
observeNavigation() {
window.addEventListener("load", () => {
setTimeout(() => {
const nav = performance.getEntriesByType("navigation")[0]
this.metrics.navigation = {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
ttfb: nav.responseStart - nav.requestStart,
domReady: nav.domContentLoadedEventEnd - nav.startTime,
loadComplete: nav.loadEventEnd - nav.startTime,
}
}, 0)
})
}
// 首次绘制
observePaint() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === "first-contentful-paint") {
this.metrics.fcp = entry.startTime
}
if (entry.name === "first-paint") {
this.metrics.fp = entry.startTime
}
}
})
observer.observe({ entryTypes: ["paint"] })
}
// 最大内容绘制
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.lcp = lastEntry.startTime
})
observer.observe({ entryTypes: ["largest-contentful-paint"] })
}
// 累积布局偏移
observeCLS() {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
this.metrics.cls = clsValue
})
observer.observe({ entryTypes: ["layout-shift"] })
}
// 首次输入延迟
observeFID() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.metrics.fid = entry.processingStart - entry.startTime
}
})
observer.observe({ entryTypes: ["first-input"] })
}
// 上报数据
report() {
const data = {
url: location.href,
timestamp: Date.now(),
userAgent: navigator.userAgent,
metrics: this.metrics,
}
// 使用sendBeacon确保数据发送
navigator.sendBeacon("/api/performance", JSON.stringify(data))
}
}
// 使用
const monitor = new PerformanceMonitor()
window.addEventListener("beforeunload", () => monitor.report())关键指标说明
| 指标 | 全称 | 说明 | 目标值 |
|---|---|---|---|
| FP | First Paint | 首次绘制 | < 1.8s |
| FCP | First Contentful Paint | 首次内容绘制 | < 1.8s |
| LCP | Largest Contentful Paint | 最大内容绘制 | < 2.5s |
| FID | First Input Delay | 首次输入延迟 | < 100ms |
| CLS | Cumulative Layout Shift | 累积布局偏移 | < 0.1 |
| TTFB | Time to First Byte | 首字节时间 | < 600ms |
12. 如何实现图片懒加载?
难度:⭐⭐(中级)
标签:#性能优化 #懒加载
问题描述:
实现一个图片懒加载方案,当图片进入视口时才加载。
参考答案要点:
方案一:Intersection Observer API(推荐)
function lazyLoadImages() {
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src // 从data-src加载真实图片
img.classList.remove("lazy")
observer.unobserve(img)
}
})
},
{
rootMargin: "50px 0px", // 提前50px开始加载
threshold: 0.01,
}
)
document.querySelectorAll("img.lazy").forEach((img) => {
imageObserver.observe(img)
})
}
// HTML
// <img class="lazy" data-src="real-image.jpg" src="placeholder.jpg">方案二:原生loading属性(最简单)
<img src="image.jpg" loading="lazy" alt="描述" />loading="lazy":延迟加载loading="eager":立即加载(默认)
方案三:scroll事件(兼容性最好)
function lazyLoad() {
const images = document.querySelectorAll("img[data-src]")
images.forEach((img) => {
const rect = img.getBoundingClientRect()
const isInViewport = rect.top < window.innerHeight && rect.bottom > 0
if (isInViewport) {
img.src = img.dataset.src
img.removeAttribute("data-src")
}
})
}
// 节流优化
const throttledLazyLoad = throttle(lazyLoad, 200)
window.addEventListener("scroll", throttledLazyLoad)
window.addEventListener("resize", throttledLazyLoad)优化技巧
- 占位图:使用低质量占位图或骨架屏
- 预加载可视区外:rootMargin提前加载
- 响应式图片:使用srcset
HTML
<img data-srcset="small.jpg 300w, large.jpg 800w" loading="lazy" />
13. 如何实现无限滚动加载?
难度:⭐⭐(中级)
标签:#交互 #性能
问题描述:
实现一个无限滚动加载方案,当滚动到底部时自动加载更多内容。
参考答案要点:
class InfiniteScroll {
constructor(options) {
this.container = options.container
this.loadMore = options.loadMore
this.threshold = options.threshold || 100
this.isLoading = false
this.hasMore = true
this.init()
}
init() {
// 使用Intersection Observer监听底部元素
const sentinel = document.createElement("div")
sentinel.className = "scroll-sentinel"
this.container.appendChild(sentinel)
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
this.load()
}
})
},
{
rootMargin: `${this.threshold}px`,
}
)
observer.observe(sentinel)
}
async load() {
this.isLoading = true
this.showLoading()
try {
const result = await this.loadMore()
if (!result.hasMore) {
this.hasMore = false
this.showNoMore()
}
} catch (error) {
this.showError()
} finally {
this.isLoading = false
this.hideLoading()
}
}
showLoading() {
// 显示加载中提示
}
hideLoading() {
// 隐藏加载中提示
}
showNoMore() {
// 显示没有更多数据
}
showError() {
// 显示加载失败,支持重试
}
}
// 使用
const infiniteScroll = new InfiniteScroll({
container: document.querySelector(".list-container"),
threshold: 200,
async loadMore() {
const data = await fetch(`/api/list?page=${++this.page}`).then((r) =>
r.json()
)
renderItems(data.items)
return { hasMore: data.hasMore }
},
})优化建议
- 防抖/节流:避免频繁触发加载
- 加载状态:防止重复请求
- 错误处理:失败时支持重试
- 结束状态:无更多数据时提示
- 虚拟列表:大量数据时使用虚拟滚动
14. 如何防止前端重复请求?
难度:⭐⭐(中级)
标签:#网络 #性能
问题描述:
设计一个方案,防止用户快速点击按钮导致的重复请求。
参考答案要点:
方案一:请求锁(推荐)
class RequestLock {
constructor() {
this.pendingRequests = new Map()
}
async request(key, requestFn) {
// 如果有进行中的请求,返回该请求的Promise
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key)
}
// 创建新请求
const promise = requestFn().finally(() => {
this.pendingRequests.delete(key)
})
this.pendingRequests.set(key, promise)
return promise
}
}
// 使用
const requestLock = new RequestLock()
async function submitForm(data) {
return requestLock.request("submitForm", () => {
return fetch("/api/submit", {
method: "POST",
body: JSON.stringify(data),
})
})
}方案二:AbortController取消重复请求
class RequestManager {
constructor() {
this.controllers = new Map()
}
async request(key, requestFn) {
// 取消之前的请求
if (this.controllers.has(key)) {
this.controllers.get(key).abort()
}
const controller = new AbortController()
this.controllers.set(key, controller)
try {
const result = await requestFn(controller.signal)
return result
} finally {
this.controllers.delete(key)
}
}
}
// 使用
const manager = new RequestManager()
function search(keyword) {
return manager.request("search", (signal) => {
return fetch(`/api/search?q=${keyword}`, { signal })
})
}方案三:防抖(搜索场景)
import { debounce } from "lodash"
const debouncedSearch = debounce((keyword) => {
fetch(`/api/search?q=${keyword}`)
.then((r) => r.json())
.then((data) => renderResults(data))
}, 300)
// 输入时调用
input.addEventListener("input", (e) => {
debouncedSearch(e.target.value)
})方案四:按钮状态控制
async function handleSubmit() {
const btn = document.querySelector(".submit-btn")
if (btn.disabled) return
btn.disabled = true
btn.textContent = "提交中..."
try {
await submitForm()
showSuccess()
} catch (error) {
showError()
} finally {
btn.disabled = false
btn.textContent = "提交"
}
}四、业务场景实现
15. 如何实现一个购物车功能?
难度:⭐⭐⭐(高级)
标签:#业务场景 #状态管理
问题描述:
设计一个购物车系统,包括添加商品、修改数量、计算价格、持久化存储等功能。
参考答案要点:
数据结构设计
interface CartItem {
skuId: string; // SKU唯一标识
productId: string; // 商品ID
name: string; // 商品名称
price: number; // 单价(分)
quantity: number; // 数量
selected: boolean; // 是否选中
image: string; // 商品图片
stock: number; // 库存
attributes: Record<string, string>; // 规格属性
}
interface CartState {
items: CartItem[];
isLoading: boolean;
}核心功能实现
class ShoppingCart {
constructor() {
this.items = this.loadFromStorage() || []
}
// 添加商品
addItem(product) {
const existingItem = this.items.find((item) => item.skuId === product.skuId)
if (existingItem) {
// 已存在,增加数量
existingItem.quantity += product.quantity
// 检查库存
if (existingItem.quantity > product.stock) {
existingItem.quantity = product.stock
throw new Error("超出库存限制")
}
} else {
// 新商品
this.items.push({
...product,
selected: true,
})
}
this.saveToStorage()
this.syncToServer()
}
// 更新数量
updateQuantity(skuId, quantity) {
const item = this.items.find((item) => item.skuId === skuId)
if (!item) return
if (quantity <= 0) {
this.removeItem(skuId)
} else {
item.quantity = Math.min(quantity, item.stock)
}
this.saveToStorage()
this.syncToServer()
}
// 删除商品
removeItem(skuId) {
this.items = this.items.filter((item) => item.skuId !== skuId)
this.saveToStorage()
this.syncToServer()
}
// 切换选中状态
toggleSelect(skuId) {
const item = this.items.find((item) => item.skuId === skuId)
if (item) {
item.selected = !item.selected
this.saveToStorage()
}
}
// 全选/取消全选
toggleSelectAll(selected) {
this.items.forEach((item) => {
item.selected = selected
})
this.saveToStorage()
}
// 计算总价
getTotalPrice() {
return this.items
.filter((item) => item.selected)
.reduce((total, item) => {
return total + item.price * item.quantity
}, 0)
}
// 计算总数量
getTotalCount() {
return this.items
.filter((item) => item.selected)
.reduce((count, item) => count + item.quantity, 0)
}
// 本地存储
saveToStorage() {
localStorage.setItem("cart", JSON.stringify(this.items))
}
loadFromStorage() {
const data = localStorage.getItem("cart")
return data ? JSON.parse(data) : null
}
// 同步到服务端(登录状态)
async syncToServer() {
if (this.isLoggedIn()) {
await fetch("/api/cart/sync", {
method: "POST",
body: JSON.stringify(this.items),
})
}
}
// 合并本地和云端购物车(登录时)
async mergeCart(serverCart) {
const localCart = this.items
// 合并策略:以本地为准,数量累加
const merged = [...localCart]
serverCart.forEach((serverItem) => {
const localItem = merged.find((item) => item.skuId === serverItem.skuId)
if (localItem) {
localItem.quantity += serverItem.quantity
} else {
merged.push(serverItem)
}
})
this.items = merged
this.saveToStorage()
}
}未登录 vs 登录状态
| 场景 | 存储方式 | 同步策略 |
|---|---|---|
| 未登录 | localStorage | 登录时合并到服务端 |
| 已登录 | localStorage + 服务端 | 实时同步,多端同步 |
16. 如何实现用户登录流程?
难度:⭐⭐(中级)
标签:#业务场景 #安全
问题描述:
设计一个完整的用户登录流程,包括token管理、登录状态保持、单点登录等。
参考答案要点:
登录流程
用户输入账号密码
↓
前端验证格式
↓
发送登录请求
↓
服务端验证 → 生成Token
↓
存储Token(Cookie/LocalStorage)
↓
获取用户信息
↓
跳转首页Token管理
class AuthManager {
constructor() {
this.token = null
this.refreshToken = null
this.user = null
}
// 登录
async login(credentials) {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(credentials),
})
const { token, refreshToken, user } = await response.json()
this.setToken(token)
this.setRefreshToken(refreshToken)
this.setUser(user)
// 启动自动刷新
this.startRefreshTimer()
return user
}
// 设置Token
setToken(token) {
this.token = token
// 方案1:localStorage(易受XSS攻击)
localStorage.setItem("token", token)
// 方案2:Cookie(HttpOnly,防XSS)
// document.cookie = `token=${token}; HttpOnly; Secure; SameSite=Strict`;
}
// 获取Token
getToken() {
return this.token || localStorage.getItem("token")
}
// 刷新Token
async refreshToken() {
try {
const response = await fetch("/api/refresh", {
method: "POST",
headers: {
Authorization: `Bearer ${this.refreshToken}`,
},
})
const { token } = await response.json()
this.setToken(token)
return token
} catch (error) {
// 刷新失败,退出登录
this.logout()
throw error
}
}
// 自动刷新
startRefreshTimer() {
// Token过期前5分钟刷新
const refreshTime = (getTokenExpiry(this.token) - 300) * 1000
setTimeout(() => {
this.refreshToken()
}, refreshTime)
}
// 退出登录
logout() {
this.token = null
this.refreshToken = null
this.user = null
localStorage.removeItem("token")
localStorage.removeItem("refreshToken")
// 通知服务端(可选)
fetch("/api/logout", { method: "POST" })
}
// 检查登录状态
isAuthenticated() {
return !!this.getToken() && !isTokenExpired(this.getToken())
}
}请求拦截器(自动携带Token)
// axios拦截器
axios.interceptors.request.use((config) => {
const token = authManager.getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器(Token过期自动刷新)
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const newToken = await authManager.refreshToken()
originalRequest.headers.Authorization = `Bearer ${newToken}`
return axios(originalRequest)
} catch (refreshError) {
// 刷新失败,跳转登录
window.location.href = "/login"
return Promise.reject(refreshError)
}
}
return Promise.reject(error)
}
)17. 如何实现前端路由?
难度:⭐⭐⭐(高级)
标签:#路由 #原理
问题描述:
实现一个简单的前端路由系统,支持hash模式和history模式。
参考答案要点:
Hash模式实现
class HashRouter {
constructor() {
this.routes = new Map()
this.currentPath = ""
// 监听hash变化
window.addEventListener("hashchange", () => {
this.render()
})
// 初始化
window.addEventListener("load", () => {
this.render()
})
}
// 注册路由
route(path, handler) {
this.routes.set(path, handler)
}
// 导航
navigate(path) {
window.location.hash = path
}
// 渲染
render() {
const hash = window.location.hash.slice(1) || "/"
this.currentPath = hash
const handler = this.routes.get(hash)
if (handler) {
handler()
} else {
// 404处理
const notFound = this.routes.get("/404")
if (notFound) notFound()
}
}
// 前进/后退
back() {
window.history.back()
}
forward() {
window.history.forward()
}
}
// 使用
const router = new HashRouter()
router.route("/", () => {
document.getElementById("app").innerHTML = "<h1>首页</h1>"
})
router.route("/about", () => {
document.getElementById("app").innerHTML = "<h1>关于</h1>"
})
// 导航
router.navigate("/about")History模式实现
class HistoryRouter {
constructor() {
this.routes = new Map()
// 监听popstate事件(前进/后退)
window.addEventListener("popstate", () => {
this.render()
})
}
// 注册路由
route(path, handler) {
this.routes.set(path, handler)
}
// 导航(不刷新页面)
navigate(path) {
history.pushState({}, "", path)
this.render()
}
// 替换当前历史记录
replace(path) {
history.replaceState({}, "", path)
this.render()
}
// 渲染
render() {
const path = window.location.pathname
const handler = this.routes.get(path)
if (handler) {
handler()
} else {
const notFound = this.routes.get("/404")
if (notFound) notFound()
}
}
// 前进/后退
back() {
window.history.back()
}
forward() {
window.history.forward()
}
go(n) {
window.history.go(n)
}
}
// 使用
const router = new HistoryRouter()
router.route("/", () => {
document.getElementById("app").innerHTML = "<h1>首页</h1>"
})
router.route("/user/:id", (params) => {
document.getElementById("app").innerHTML = `<h1>用户 ${params.id}</h1>`
})
// 需要服务端配合,所有路由指向index.html对比
| 特性 | Hash模式 | History模式 |
|---|---|---|
| URL格式 | /#/path | /path |
| 服务端支持 | 不需要 | 需要(所有路由指向index.html) |
| 兼容性 | IE8+ | IE10+ |
| SEO | 较差 | 较好 |
18. 如何实现一个虚拟列表?
难度:⭐⭐⭐(高级)
标签:#性能优化 #大数据
问题描述:
实现一个虚拟列表,只渲染可视区域内的元素,优化大量数据的渲染性能。
参考答案要点:
<template>
<div class="virtual-list" ref="container" @scroll="handleScroll">
<!-- 撑开滚动区域 -->
<div class="list-phantom" :style="{ height: totalHeight + 'px' }"></div>
<!-- 可视区域 -->
<div
class="list-content"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="list-item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.text }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: Array, // 所有数据
itemHeight: Number, // 每项高度
buffer: {
// 缓冲区数量
type: Number,
default: 5,
},
},
data() {
return {
startIndex: 0, // 可视区起始索引
endIndex: 0, // 可视区结束索引
offsetY: 0, // 偏移量
containerHeight: 0, // 容器高度
}
},
computed: {
// 可视区数据
visibleItems() {
return this.items.slice(this.startIndex, this.endIndex)
},
// 总高度
totalHeight() {
return this.items.length * this.itemHeight
},
},
mounted() {
this.containerHeight = this.$refs.container.clientHeight
this.updateVisibleRange()
},
methods: {
handleScroll() {
const scrollTop = this.$refs.container.scrollTop
this.updateVisibleRange(scrollTop)
},
updateVisibleRange(scrollTop = 0) {
// 计算可视区起始索引
this.startIndex = Math.floor(scrollTop / this.itemHeight)
// 计算可视区能显示的数量
const visibleCount = Math.ceil(this.containerHeight / this.itemHeight)
// 加上缓冲区
this.startIndex = Math.max(0, this.startIndex - this.buffer)
this.endIndex = Math.min(
this.items.length,
this.startIndex + visibleCount + this.buffer * 2
)
// 计算偏移量(让内容在正确位置显示)
this.offsetY = this.startIndex * this.itemHeight
},
},
}
</script>
<style scoped>
.virtual-list {
position: relative;
height: 400px;
overflow-y: auto;
}
.list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.list-item {
box-sizing: border-box;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 0 16px;
}
</style>优化版本(支持不定高度)
class DynamicVirtualList {
constructor(options) {
this.items = options.items
this.container = options.container
this.estimatedHeight = options.estimatedHeight || 50
// 缓存每项的实际高度
this.heightCache = new Map()
// 计算累计高度
this.cumulativeHeights = this.calculateCumulativeHeights()
this.init()
}
// 二分查找起始索引
findStartIndex(scrollTop) {
let left = 0
let right = this.cumulativeHeights.length - 1
while (left < right) {
const mid = Math.floor((left + right) / 2)
if (this.cumulativeHeights[mid] < scrollTop) {
left = mid + 1
} else {
right = mid
}
}
return left
}
// 更新某项高度
updateItemHeight(index, height) {
this.heightCache.set(index, height)
this.cumulativeHeights = this.calculateCumulativeHeights()
}
calculateCumulativeHeights() {
const heights = []
let sum = 0
for (let i = 0; i < this.items.length; i++) {
heights.push(sum)
sum += this.heightCache.get(i) || this.estimatedHeight
}
return heights
}
}五、安全与监控
19. 如何防范XSS攻击?
难度:⭐⭐⭐(高级)
标签:#安全 #XSS
问题描述:
详细描述XSS攻击的类型和防范措施。
参考答案要点:
XSS类型
-
存储型XSS
- 恶意脚本存储在服务端(数据库)
- 所有访问该页面的用户都会执行
- 危害最大
-
反射型XSS
- 恶意脚本在URL参数中
- 需要诱骗用户点击链接
- 一次性攻击
-
DOM型XSS
- 通过JavaScript修改DOM时触发
- 不经过服务端
防范措施
-
输入过滤
JAVASCRIPT// 转义特殊字符 function escapeHtml(str) { return str .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'") } -
输出编码
JAVASCRIPT// 根据输出位置选择编码方式 // HTML内容 element.textContent = userInput // HTML属性 element.setAttribute("data-value", userInput) // JavaScript const script = document.createElement("script") script.textContent = JSON.stringify(userInput) -
CSP(内容安全策略)
HTML<!-- 只允许加载同域资源 --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';" /> -
HttpOnly Cookie
JAVASCRIPT// 防止JavaScript读取Cookie document.cookie = "session=xxx; HttpOnly; Secure" -
现代框架防护
- React/Vue自动转义JSX/模板中的变量
- 避免使用
dangerouslySetInnerHTML/v-html
20. 如何防范CSRF攻击?
难度:⭐⭐⭐(高级)
标签:#安全 #CSRF
问题描述:
详细描述CSRF攻击原理和防范措施。
参考答案要点:
CSRF原理
- 用户登录A网站,获得Cookie
- 用户未退出,访问恶意网站B
- B网站自动发起请求到A网站
- 浏览器自动携带A网站的Cookie
- A网站认为是合法请求,执行操作
防范措施
-
CSRF Token(最有效)
JAVASCRIPT// 服务端生成Token,嵌入页面 <meta name="csrf-token" content="随机Token"> // 请求时携带 fetch('/api/transfer', { method: 'POST', headers: { 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content } }); -
SameSite Cookie
JAVASCRIPT// Strict: 完全禁止第三方Cookie // Lax: 允许部分情况(GET请求) // None: 允许,但必须配合Secure document.cookie = "session=xxx; SameSite=Strict; Secure" -
验证Referer/Origin
JAVASCRIPT// 服务端验证请求来源 const referer = req.headers.referer if (!referer || !referer.startsWith("https://example.com")) { return res.status(403).send("Forbidden") } -
双重Cookie验证
JAVASCRIPT// Cookie中设置随机值,同时请求参数也携带该值 // 服务端比较两个值是否一致
六、复习建议
重点题目清单
必会题目(⭐⭐⭐ 高频)
- 从输入URL到页面加载的完整过程
- TCP三次握手和四次挥手
- 重排和重绘的区别及优化
- 浏览器渲染原理
- 前端性能优化方案
重点掌握(⭐⭐ 常考)
- HTTPS握手过程
- DNS解析过程
- script标签的defer和async
- 事件循环机制
- 虚拟列表实现
加分项(⭐ 了解)
- 前端性能监控SDK
- 购物车/登录流程设计
- XSS/CSRF防范
复习方法
- 画图理解:网络流程、渲染流程、事件循环等建议画图
- 手写代码:防抖节流、Promise、深拷贝等必须能手写
- 项目结合:结合实际项目经验回答,更有说服力
- 性能数据:记住关键性能指标的目标值(如LCP < 2.5s)
面试技巧
- 结构化回答:分点作答,逻辑清晰
- 由浅入深:先讲基础,再讲优化和原理
- 结合实际:提到自己做过的性能优化案例
- 承认不足:遇到不会的问题,诚实说明,但表达学习意愿