sqbks二面(准备)
你对这些前端面试题目的把握相当准确,很多地方都切中了要点。我来帮你逐一梳理和补充,希望能助你面试一臂之力。
面试题目 | 核心考察点 | 关键答案 |
---|---|---|
Canvas优化 | 性能优化手段 | 离屏Canvas、分层画布、减少绘制区域、避免浮点坐标、使用requestAnimationFrame |
Canvas绘制验证码 | 安全实践与前后端协作 | 前端生成显示,后端生成并存Session验证,杜绝仅前端验证 |
CKEditor原理 | 富文本编辑器架构思想 | 数据模型与视图分离、操作原子性、虚拟DOM差分更新 |
大数据量表格处理 | 性能与用户体验平衡 | 分页、虚拟滚动、懒加载 |
前端过滤大量数据 | 高效数据处理与并行计算 | 数据分片、Web Workers并行过滤、结果合并 |
🖼️ 1. Canvas优化
Canvas优化是保证复杂应用流畅性的关键。
- 离屏Canvas(预渲染):这是最常用的优化技巧。将静态或变化不频繁的图形预先绘制到一个离屏Canvas上,之后在主Canvas中只需使用
drawImage
来绘制它,避免了重复的绘制计算。非常适合重复绘制的图形,如游戏中的背景、精灵等。 - 减少重绘区域(脏矩形算法):不要每一帧都清除并重绘整个画布。只重绘内容发生变化的区域。使用
clearRect(x, y, width, height)
来局部清除,再绘制该区域。 - 分层画布:将静态背景、动态元素和UI界面分别绘制在多个叠加的Canvas元素上。这样,静态层只需绘制一次,动态层可以频繁更新,互不影响。
- 避免浮点数坐标:绘制图形时,使用
Math.floor()
或Math.round()
将坐标取整。浮点数坐标会迫使浏览器进行额外的子像素渲染,降低性能。 - 利用硬件加速:对Canvas容器使用CSS的
transform: translateZ(0)
或will-change: transform
属性,可以触发GPU加速,提升动画性能。 - 正确的动画循环:使用
requestAnimationFrame
而非setInterval
或setTimeout
来控制动画循环。它能保证回调频率与浏览器刷新率一致,并在页面不可见时暂停,节省资源。
🔐 2. Canvas绘制验证码详情
你已掌握了前端绘制的要点(随机字符、扭曲、干扰线)。这里重点补充安全与后端交互。
-
核心原则:前端仅负责展示,验证逻辑必须在后端
- 流程:
- 用户请求页面时,后端生成随机验证码文本(如
A7X9
)。 - 后端将该文本存入当前用户的Session或缓存(如Redis),并与其Session ID关联。
- 后端根据该文本生成图像数据(可能用Node.js Canvas库)或调用前端API,将图像返回前端。
- 前端Canvas仅负责展示该图片。
- 用户输入验证码并提交。
- 后端比对用户输入的验证码与Session中存储的是否一致,并返回验证结果。
- 无论成败,立即使Session中的旧验证码失效(防止重复使用或暴力破解)。
- 用户请求页面时,后端生成随机验证码文本(如
- 流程:
-
为何不能仅前端验证:前端代码和验证码答案对用户是透明的,攻击者可以轻易绕过Canvas展示环节,直接读取验证码答案或提交请求。
📝 3. CKEditor原理
CKEditor 5 采用了自定义数据模型(Custom Data Model) 的架构,这与直接操作DOM的传统编辑器有根本区别。
- 数据模型与视图分离:
- 数据模型(Model):一个结构化的、线性的数据表示,存储文档内容(如段落、文本、图片及其属性),不直接关心视觉呈现。它确保了数据的完整性和一致性。
- 视图(View):负责将数据模型渲染为用户可见的DOM内容。它使用虚拟DOM技术来进行高效的差分更新,只更新必要的DOM节点,而不是重绘整个编辑器。
- 操作原子性与撤销/重做:所有对文档的修改都通过一个
ModelWriter
接口完成,并在一个事务(Transaction) 中进行。这确保了每个操作要么完全成功,要么完全失败,使得撤销(Undo)和重做(Redo)功能可以完美实现。 - 获取光标位置:你的记忆是正确的!对于可编辑区域(如
contenteditable
的div)或文本框(<textarea>
、<input>
),可以通过selectionStart
和selectionEnd
属性来获取光标位置或选中的文本范围。
📊 4. 大数据量表格的处理方法
- 分页(Pagination):最直接、最常用的方法。每次只请求和渲染一页数据(如20条)。优点:实现简单,减轻前后端压力。缺点:无法一次性浏览所有数据。
- 虚拟滚动(Virtual Scrolling):处理超大数据量的利器。只渲染可视区域及其附近的表格行(DOM节点),随着滚动动态回收不可见的节点并填充新的数据。优点:无论数据有多少,实际DOM节点数量恒定,性能极佳。缺点:实现较为复杂,需要精确计算滚动位置和行高。
- 懒加载(Lazy Loading):可以结合虚拟滚动使用。当滚动到底部时,才去加载下一页或下一批数据。
⚙️ 5. 前端一次性拿到大量数据并过滤
一次性拿到10万条数据,前端过滤的关键是避免主线程阻塞。
- 数据分片(Data Slicing):将大数据分成多个较小的块(Chunks)。
- 使用 Web Workers:这是核心解决方案。Web Worker 允许你在后台线程中运行脚本,不会阻塞主线程的UI渲染。
- 流程:
- 主线程将数据分片,并分配给多个Web Worker并行进行过滤操作。
- 每个Worker处理自己那一份数据片,返回过滤结果。
- 主线程等待所有Worker完成后,将他们的结果合并(Merge)成一个最终数组。
- 优点:充分利用多核CPU,保持页面流畅响应。
- 流程:
- 优化过滤算法:确保你的过滤逻辑本身是高效的,避免在过滤过程中进行不必要的操作或重复计算。
这些题目确实都是前端领域的深度面试题。你的准备方向很正确,尤其是对Canvas优化的押题。
前端面试涉及的知识面广,下面我将为你梳理这些问题的参考答案。让我先用一个表格来汇总核心问题与要点,方便你快速了解:
问题序号 | 核心问题 | 关键要点 |
---|---|---|
1 | 对前端感兴趣的点 | 交互逻辑、视觉表现、性能优化、技术挑战。 |
2 | 前端安全问题 | XSS、CSRF、数据泄露、安全措施。 |
3 | 执行用户输入代码的情况 | eval() 、innerHTML 、动态脚本、反序列化。 |
4 | 点击链接获取Cookie的原理 | 反射型XSS、恶意脚本、document.cookie 、诱导点击。 |
5 | 输入URL到页面渲染的过程 | DNS解析、TCP连接、HTTP请求、响应处理、渲染。 |
6 | CDN及其作用与动态路由发生位置 | 加速内容、减轻负载、提升可用性;动态路由发生在DNS解析和请求调度阶段。 |
7 | 判断渲染页面是下载还是网页渲染 | 查看网络面板、开发者工具、响应头、文件类型。 |
8 | 页面reRender的原因 | 数据变化、响应式UI、未优化的渲染逻辑、第三方库。 |
9 | 无点击操作时触发reRender的情况 | 定时器、异步回调、CSS动画/过渡、媒体查询、资源加载。 |
10 | 新数据触发重新渲染的原因 | 状态管理、虚拟DOM Diff、数据驱动视图。 |
11 | 发送请求后是否关闭TCP连接 | 不一定,HTTP Keep-Alive可复用连接。 |
12 | 连接保持时间的配置参数 | Keep-Alive: timeout=5, max=100 (服务器配置)。 |
13 | Keep-Alive与多路复用的区别 | Keep-Alive连接复用(HTTP/1.1),多路复用请求并发(HTTP/2)。 |
接下来是每个问题的详细解答:
👨💻 1. 对前端感兴趣的点
我对前端开发的兴趣主要集中在以下几个方面:
- 直观的交互逻辑与视觉反馈:前端工作能直接看到成果,通过代码实现丰富的用户交互和流畅的视觉体验,很有成就感。
- 性能优化挑战:享受通过代码拆分、懒加载、资源压缩等手段提升应用性能,优化用户体验的过程。
- 技术迭代与框架演进:密切关注并学习Vue、React等前端框架的新特性,以及前端与后端融合(如Node.js)、智能化交互等发展趋势。
🛡️ 2. 前端安全问题
前端常见的安全问题及防范措施主要包括:
- 跨站脚本攻击(XSS):攻击者向网页中注入恶意脚本。防范措施包括对用户输入进行过滤和转义,设置安全的HTTP响应头如Content-Security-Policy (CSP),避免直接使用
innerHTML
插入用户输入内容,推荐使用textContent
或安全的模板引擎。 - 跨站请求伪造(CSRF):诱导用户在已登录的网站上执行非预期操作。防范措施包括在请求中添加CSRF Token,并使用 SameSite Cookie 属性。
- 数据泄露:敏感信息在前端意外暴露。应对敏感数据加密存储和传输,并避免在前端代码中硬编码敏感信息。
- 其他安全问题:如恶意文件上传(需严格校验文件类型和内容)、第三方库漏洞(需定期更新依赖)等。
⚠️ 3. 前端在什么情况下会去执行用户输入代码
在以下情况中,前端可能会执行用户输入的代码,这也往往是安全漏洞的来源:
- 使用
eval()
函数直接执行字符串形式的代码。 - 直接通过
innerHTML
或outerHTML
属性插入未转义的HTML字符串,其中的<script>
标签会被执行。 - 动态创建
<script>
标签并将其src
属性指向不可信的URL。 - 反序列化来自用户或第三方不可信来源的JSON数据时,如果使用
eval()
进行解析(应使用JSON.parse()
)。
🍪 4. 问什么可以让攻击者能够在点击一个链接会获取到cookie
攻击者主要通过反射型XSS攻击来实现这一点:
- 构造恶意链接:攻击者找到一个存在XSS漏洞的网站,构造一个特殊的URL,参数中包含恶意脚本,例如
http://example.com?search=<script>alert('XSS')</script>
。 - 诱导点击:通过社交工程学(如伪装成中奖信息、重要通知等)诱使用户点击此链接。
- 服务器返回恶意脚本:存在漏洞的网站未对搜索参数进行过滤,直接将恶意脚本嵌入到返回的HTML页面中。
- 浏览器执行脚本:用户的浏览器解析并执行了返回页面中的恶意脚本。
- 窃取Cookie:恶意脚本中通常包含类似
document.cookie
的代码,它能读取当前站点的Cookie信息。然后脚本可能通过向攻击者控制的服务器发送一个HTTP请求(如在Image
对象的src
属性中附加Cookie信息)将数据窃取走。
🔍 5. 假设在浏览器中输入www.taobao.com到页面渲染的过程
这个过程大致可分为以下步骤:
- DNS解析:浏览器解析域名
www.taobao.com
对应的IP地址。查询顺序为:浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS服务器 → 根域名服务器 → .com顶级域名服务器 → 淘宝权威DNS服务器。 - 建立TCP连接:浏览器获得IP后,与服务器通过三次握手建立TCP连接。由于淘宝使用HTTPS,还会进行 TLS握手 协商加密密钥。
- 发送HTTP请求:浏览器通过已建立的连接向服务器发送HTTP GET请求,请求头中包含资源路径、主机、Cookie等信息。
- 服务器处理请求并返回响应:服务器可能经过负载均衡器,将请求分发到后端应用服务器处理,生成HTML文档作为HTTP响应返回。
- 浏览器解析与渲染:
- 解析HTML:构建DOM树。
- 解析CSS:构建CSSOM树。
- 合并渲染:将DOM和CSSOM合并成渲染树(Render Tree),计算布局(Layout),然后绘制(Paint)到屏幕上。
- 加载子资源:解析过程中遇到图片、CSS、JS等外部资源,会再次发起请求获取。
- 执行JavaScript:JS可能会修改DOM或CSSOM,导致重新渲染。
🚀 6. CDN是什么,CDN解决什么问题,CDN中动态路由在哪儿发生的
- CDN是什么:CDN(内容分发网络)是分布在不同地理区域的服务器集群,用于将静态资源(如图片、CSS、JS)缓存到离用户更近的地方。
- CDN解决什么问题:
- 加速内容访问:用户可从最近的节点获取资源,减少网络延迟。
- 减轻源服务器负载:资源请求由CDN节点处理。
- 提升可用性与抗攻击能力:分布式结构避免单点故障。
- CDN动态路由的发生位置:动态路由主要发生在DNS解析阶段和用户请求到达CDN节点后。
- 在DNS解析时,CDN调度系统会根据用户IP判断其地理位置和网络状况,将其引导至最优边缘节点。
- 当用户请求到达边缘节点后,若资源未缓存或已过期,CDN节点会通过内部路由策略(如根据实时网络状况)回源 获取资源。
📊 7. 怎么判断渲染页面是下载还是网页渲染
主要通过浏览器开发者工具判断:
- 网络面板(Network Tab):
- 若触发下载,类型为
document
的请求其响应头通常包含Content-Disposition: attachment; filename="xxx"
,这会触发浏览器下载保存文件。 - 网页渲染时,类型为
document
的请求其响应头通常是Content-Type: text/html
,浏览器会开始解析渲染。
- 若触发下载,类型为
- 预览/响应体(Preview/Response Body):在开发者工具中可直接查看服务器返回的内容是HTML代码(用于渲染)还是其他文件数据(可能触发下载)。
- 文件扩展名:URL路径指向的文件扩展名(如
.html
,.pdf
,.zip
)也可作为初步判断依据。
🔄 8. 渲染页面时会不停的reRender是为什么
页面不断重新渲染的常见原因包括:
- 频繁的数据变化:例如由
setInterval
定时器持续修改状态或DOM。 - 未优化的响应式UI:在React等框架中,状态变更可能触发组件重新渲染。若渲染逻辑复杂或未使用恰当优化(如
shouldComponentUpdate
,React.memo
,useMemo
),可能导致不必要的渲染。 - CSS动画或过渡:CSS
animation
或transition
应用的元素在动画过程中会持续重绘。 - 布局抖动(Layout Thrashing):JavaScript频繁交替读写DOM样式,迫使浏览器不断重新计算布局和渲染。
- 第三方库或插件:某些库可能在内部执行循环任务或监听器,导致持续渲染。
🔔 9. DOM还没有点击操作时,哪些情况会触发这个reRender
即使没有点击操作,以下情况也可能触发重新渲染:
- 定时器:
setInterval
或setTimeout
中的代码修改了DOM或样式。 - 异步请求回调:Ajax或Fetch请求完成后,在回调函数中更新数据导致UI刷新。
- CSS动画和过渡:CSS
animation
或transition
会触发重绘。 - 媒体查询变化:视口大小改变或设备方向旋转导致CSS媒体查询结果变化,可能触发页面布局调整和重绘。
- 视频、音频播放:播放进度更新可能引发相关UI控件重绘。
- 资源加载完成:如图片加载完成后可能占用原有空间,影响布局。
📝 10. 获取到新的数据后为什么会触发重新渲染
在现代前端框架中,新数据触发重新渲染主要基于:
- 状态驱动视图:框架(如React、Vue)遵循数据变化自动更新UI的原则。当数据(如React的state、Vue的data)更新时,框架能侦测到变化。
- 虚拟DOM(Virtual DOM) Diff:许多框架使用虚拟DOM。数据变化后,会生成新的虚拟DOM树,与旧的进行差异对比(Diffing),计算出需更新的最小DOM操作集,然后提交给浏览器重新渲染。
- ** reactivity系统**:框架通过响应式系统跟踪数据依赖。当依赖数据变化,会自动通知相关组件重新渲染。
🔗 11. 发送请求后,一定会关闭TCP连接吗
不一定。TCP连接是否关闭取决于HTTP版本和 Connection
标头:
- HTTP/1.0:默认情况下,每个请求响应完成后会关闭TCP连接。如需保持连接,需显式设置
Connection: keep-alive
。 - HTTP/1.1 及以后:默认支持持久连接(Persistent Connection),即多个请求可复用同一个TCP连接。服务器或客户端可通过发送
Connection: close
标头主动关闭连接。
⏰ 12. 连接保持时间的配置参数
持久连接的保持时间通常在服务器端配置。常见的配置参数(以Apache和Nginx为例):
- Apache:通过
KeepAliveTimeout
指令设置服务器在关闭连接前等待后续请求的最长时间。 - Nginx:使用
keepalive_timeout
指令设置超时时间。
在HTTP响应头中,服务器也可告知客户端其意图,例如:Keep-Alive: timeout=5, max=100
这表示连接允许空闲时间为5秒,最多可传输100个请求。
🔀 13. keep-Alive和多路复用的区别
特性 | HTTP Keep-Alive (HTTP/1.1) | HTTP/2 多路复用 (Multiplexing) |
---|---|---|
核心目的 | 连接复用 | 请求和响应并行复用 |
解决的问题 | 减少TCP连接数,减轻开销 | 解决HTTP/1.1队头阻塞问题 |
工作方式 | 同一连接顺序处理请求响应 | 同一连接上并行交错发送处理多个请求响应 |
效率 | 仍存在队头阻塞 | 更高并发,更低延迟 |
前端面试涉及的知识面较广,从底层原理到工程实践都有所涵盖。下面我将对这些面试题进行梳理和解答,希望能帮助你巩固相关知识点。
🔍 前端核心面试题详解
1. 事件循环相关代码输出分析
JavaScript 的事件循环 (Event Loop) 是其异步编程的核心机制。由于 JS 是单线程的,它通过事件循环来处理异步操作,避免阻塞主线程。事件循环的工作机制可以概括为:执行同步代码 → 清空所有微任务 → 执行一个宏任务 → 重复循环。
- 宏任务 (MacroTask):包括 script(整体代码)、setTimeout、setInterval、setImmediate(Node.js)、I/O 操作、UI 渲染等。
- 微任务 (MicroTask):包括 Promise.then、process.nextTick(Node.js)、MutationObserver 等。
执行顺序原则:同步代码 > 微任务 > 宏任务。在同一次事件循环中,微任务总在下一个宏任务之前执行。
分析技巧:
- 先找出所有同步代码并执行。
- 识别异步代码,区分宏任务和微任务。
- 每个宏任务执行后,都会检查微任务队列并执行所有微任务。
process.nextTick
(Node.js) 的优先级高于 Promise.then。
经典示例:
console.log('1. 开始点餐'); // 同步代码,首先执行setTimeout(() => {console.log('6. 取普通套餐'); // 宏任务,最后执行
}, 0);new Promise((resolve) => {console.log('2. 正在做VIP套餐'); // Promise构造函数内的代码是同步的!resolve();
}).then(() => {console.log('4. 取VIP套餐'); // 微任务,在本轮循环结束前执行
});console.log('3. 继续点其他餐'); // 同步代码// 输出顺序:1 → 2 → 3 → 4 → 6
2. 如何判断元素是否在视口
判断元素是否在视口内是常见的需求,例如实现懒加载或追踪元素曝光。主要有两种方法:
方法一:使用 getBoundingClientRect()
(传统方式)
此方法返回元素的大小及其相对于视口的位置。
function isElementInViewport(el) {const rect = el.getBoundingClientRect();const windowHeight = window.innerHeight || document.documentElement.clientHeight;const windowWidth = window.innerWidth || document.documentElement.clientWidth;// 判断元素是否与视口有交集(部分或全部可见)return (rect.top < windowHeight &&rect.bottom > 0 &&rect.left < windowWidth &&rect.right > 0);
}// 使用示例:通常在滚动事件中监听,注意使用节流优化性能
window.addEventListener('scroll', () => {const element = document.querySelector('#myElement');if (isElementInViewport(element)) {console.log('元素在视口内!');}
});
方法二:使用 Intersection Observer API
(现代方式)
这是一个性能更优的解决方案,无需监听滚动事件,浏览器会自动回调。
// 1. 创建观察器实例
const observer = new IntersectionObserver((entries, observer) => {entries.forEach(entry => {// 如果元素进入视口(isIntersecting 为 true)if (entry.isIntersecting) {console.log('元素进入视口!', entry.target);// 可选:在触发后停止观察该元素// observer.unobserve(entry.target);} else {console.log('元素离开视口!');}});
}, {root: null, // 默认相对于视口进行观察rootMargin: '0px', // 扩大或缩小视口的边界threshold: 0.1 // 当元素10%可见时触发回调。可以是数组 [0, 0.25, 0.5, 1]
});// 2. 开始观察目标元素
const target = document.querySelector('#target');
observer.observe(target);
对比与选择:
特性 | getBoundingClientRect | Intersection Observer API |
---|---|---|
兼容性 | 非常好(包括IE) | 良好(不兼容IE11及以下,可用polyfill) |
性能 | 需主动调用,频繁操作可能导致性能问题 | 异步回调,性能更优 |
使用难度 | 简单,但需手动处理滚动事件和节流 | 稍复杂,但API设计更现代和强大 |
适用场景 | 简单需求或需要兼容老旧浏览器 | 新项目、复杂场景(如图片懒加载、无限滚动) |
3. 浏览器工具中的 ‘Performance’(性能)面板有什么用
Chrome DevTools 中的 Performance 面板是分析和诊断网页性能问题的核心工具,它提供了从页面加载到运行时交互的完整性能数据记录和可视化。
主要用途包括:
- 记录和分析运行时性能:录制页面一段时间内的活动,分析 JavaScript 执行、样式计算、布局(重排)、绘制(重绘)等细节。
- 识别卡顿和长任务 (Long Tasks):找到执行时间超过 50ms 的任务,这些任务会阻塞主线程,导致页面无响应。
- 分析帧率 (FPS):检查动画和交互是否流畅(理想帧率为 60fps)。红色长条表示帧率过低可能存在卡顿。
- 查看网络请求和内存使用情况:了解资源加载时序和 JavaScript 内存分配。
使用步骤:
- 打开 Chrome DevTools (F12) → 切换到 Performance 面板。
- 点击 Record (圆形图标) 开始录制。
- 在页面上进行你想要分析的操作(如滚动、点击)。
- 点击 Stop 停止录制。
- 在生成的报告中分析各项指标:
- Overview: 总览时间线上的 FPS、CPU 占用率、网络请求。
- Main: 主线程火焰图,清晰展示任务调用栈和耗时,是分析长任务的关键。
- Summary: 总结标签页时间消耗分布(加载、脚本计算、渲染、绘制等)。
4. 如何提升首屏加载速度
首屏加载速度(First Contentful Paint, FCP 和 Largest Contentful Paint, LCP)直接影响用户体验和 SEO。以下是一些核心优化策略:
- 优化资源加载:
- 压缩和优化图片:使用现代格式(如 WebP)、适当尺寸,并采用懒加载(
<img loading="lazy">
)。 - 代码拆分 (Code Splitting) 和 Tree Shaking:利用打包工具(如 Webpack、Vite)将代码分成多个块,按需加载,移除未使用的代码。
- 预加载关键资源:使用
<link rel="preload">
提前获取渲染首屏内容所需的关键资源(如字体、关键CSS)。
- 压缩和优化图片:使用现代格式(如 WebP)、适当尺寸,并采用懒加载(
- 减少请求数量和传输体积:
- 合并文件:将小文件合并(如雪碧图),减少 HTTP 请求次数。
- 开启 Gzip/Brotli 压缩:在服务器端压缩文本资源(JS, CSS, HTML)。
- 使用 HTTP/2:利用多路复用特性,提升资源加载效率。
- 优化渲染路径:
- 内联关键 CSS:将渲染首屏内容所需的核心样式直接内嵌在 HTML 中,减少请求。
- 延迟加载非关键 JS/CSS:使用
async
或defer
属性加载非阻塞脚本。
- 利用缓存:
- 配置浏览器缓存:通过设置
Cache-Control
、ETag
等 HTTP 头,让浏览器缓存静态资源。 - 使用 Service Worker:实现离线缓存和更精细的缓存策略。
- 配置浏览器缓存:通过设置
5. Vue中的 nextTick 有什么用
Vue.nextTick()
或实例方法 this.$nextTick()
用于在 下次 DOM 更新循环结束之后 执行延迟回调。
核心作用:当你修改了 Vue 组件的数据后,DOM 并不会立即更新。Vue 会开启一个异步队列来缓冲同一事件循环中的所有数据变更,然后批量更新视图以提高性能。使用 nextTick
可以确保你的回调函数在 DOM 更新完成后执行。
主要使用场景:
- 获取更新后的 DOM:当你改变数据后,需要立即操作依赖于新数据的 DOM。
this.message = 'Hello!'; // 修改数据 // DOM 还未更新 this.$nextTick(() => {// 这里可以获取到更新后的 DOMconst element = this.$el.textContent; // 现在 element 的值是 'Hello!' });
- 在
created
生命周期钩子中操作 DOM:created
时 DOM 还未渲染,任何 DOM 操作都应放在nextTick
中。 - 与第三方插件集成:在 Vue 更新 DOM 后,调用需要操作更新后 DOM 的第三方库。
原理简述:Vue 内部会尝试使用原生的 Promise.then
(微任务)、MutationObserver
,降级到 setImmediate
或 setTimeout
(宏任务)来异步执行 flushCallbacks
函数,该函数会清空并执行所有通过 nextTick
注册的回调函数。
6. 如何实现缓存
在前端开发中,"缓存"通常指浏览器缓存和前端状态缓存。
浏览器缓存 (HTTP缓存)
通过设置 HTTP 响应头来控制:
- 强缓存:浏览器在缓存过期前直接使用本地副本,不请求服务器。
Cache-Control: max-age=3600
(相对时间,单位秒,优先级高)Expires: Wed, 21 Oct 2025 07:28:00 GMT
(绝对时间)
- 协商缓存:浏览器询问服务器资源是否过期,若未过期则返回 304 状态码,使用缓存。
Last-Modified
(最后修改时间) 和If-Modified-Since
ETag
(资源标识符,更精确) 和If-None-Match
(优先级高)
前端状态缓存
- Service Worker:可以拦截网络请求,实现复杂的离线缓存策略。
- 本地存储 (LocalStorage, SessionStorage):用于存储不常变的静态数据或用户偏好设置。注意它们只存储字符串,存储对象需用
JSON.stringify()
。 - Vuex / Pinia (状态管理库):配合持久化插件(如
vuex-persistedstate
)可以将状态保存到本地存储,实现页面刷新后数据不丢失。 - 内存缓存:在组件或全局变量中缓存数据,适用于单页面应用内临时存储,页面刷新后失效。
7. 项目相关
这是一个开放性问题,旨在了解你的项目经验和技术决策能力。虽然没有搜索结果可以直接参考,但你可以准备一个结构化的回答:
- 项目背景:简要介绍项目是做什么的,目标用户是谁,你在其中的角色。
- 技术选型:为什么选择当前的技术栈(如 Vue React、Vite/Webpack)?遇到了哪些挑战?
- 负责的核心模块/功能:详细说明你负责的 1-2 个复杂功能或技术难点,以及你是如何分析和解决的(可结合事件循环、性能优化、组件设计等前述知识点)。
- 性能优化实践:是否对项目进行过性能优化?例如首屏加载、运行时性能等(可结合第4点)。
- 总结与收获:项目成果,以及你从中学到的技术或非技术经验。
8. 从零设计一个组件,比如说弹窗组件,会考虑什么?如果需要自定义样式,可以怎么做?
设计考虑因素
- 功能核心 (Core Functionality):确保基本功能,如打开/关闭、显示内容、遮罩层。
- 用户体验 (UX & Accessibility):
- 可访问性 (A11y):支持键盘事件(ESC 关闭、Tab 键聚焦在弹窗内)、适当的 ARIA 属性(
role="dialog"
,aria-modal="true"
)。 - 交互:点击遮罩层是否关闭、是否支持拖动。
- 可访问性 (A11y):支持键盘事件(ESC 关闭、Tab 键聚焦在弹窗内)、适当的 ARIA 属性(
- 灵活性 (Flexibility & Configurability):
- 内容:支持传入字符串、HTML 片段、甚至 Vue/React 组件。
- 配置化:通过 Props 控制是否显示、标题、按钮文字、自定义回调函数等。
- 样式与动画 (Style & Animation):
- 结构稳定,不会因内容多少而破裂。
- 提供打开/关闭的动画效果。
- 技术实现 (Technical Implementation):
- 挂载方式:是否脱离当前组件层级,通常使用
appendChild
或 Vue/React 的 Portal 功能挂载到body
下,避免父组件样式影响。 - 控制状态:使用
v-model
/value
和@input
/onChange
实现双向绑定。 - 阻止滚动穿透:弹窗打开时,锁定背景页面滚动。
- 挂载方式:是否脱离当前组件层级,通常使用
- 兼容性与稳定性:兼容不同浏览器,做好边界情况处理。
自定义样式方案
- Props 参数化:通过 Props 传入自定义类名 (
customClass
)、内联样式 (customStyle
) 或控制特定样式(如titleColor
)。<template><div class="my-modal" :class="customClass" :style="customStyle"><!-- ... --></div> </template> <script> export default {props: {customClass: String,customStyle: Object} } </script>
- 插槽 (Slots):提供具名插槽(如
header
,body
,footer
),让用户完全自定义内部结构和样式。<template><div class="modal"><div class="modal-header"><slot name="header"><!-- 默认头部内容 --></slot></div><div class="modal-body"><slot name="body"><!-- 默认主体内容 --></slot></div></div> </template>
- CSS 变量 (CSS Custom Properties):暴露一系列 CSS 变量,用户可以通过在外部修改这些变量来定制主题。
/* 组件内部 */ .my-modal {background-color: var(--modal-bg-color, #fff); /* 第二个值为默认值 */border-radius: var(--modal-border-radius, 8px); }
/* 用户使用 */ .my-wrapper {--modal-bg-color: #f0f0f0;--modal-border-radius: 4px; }
- 样式覆盖:提供结构良好、语义清晰的类名,并保持较低的 CSS 特异性,方便用户通过 CSS 选择器进行覆盖。但这种方式耦合度较高,需谨慎使用。
9. 项目中为什么要同时用Vite + Webpack
这是一个关于构建工具的问题。虽然搜索结果未直接提供答案,但基于它们的特性,可以理解这种搭配的常见原因:
- Vite:基于原生 ES 模块,开发服务器启动速度极快,HMR(热更新)响应迅速,提供优秀的开发体验。它更现代、更轻量。
- Webpack:非常成熟、稳定,拥有极其丰富的插件和 loader 生态,能处理各种复杂的构建需求,打包优化策略(如代码分割、摇树)经过多年考验。
共存的可能场景:
- 渐进式迁移:旧项目使用 Webpack,部分新模块或开发流程逐步迁移到 Vite 以提升开发效率,构建生产包时仍使用经过稳定测试的 Webpack。
- 微前端架构:主应用和子应用可能分别采用不同的构建工具。一个应用使用 Vite,另一个老应用使用 Webpack,它们可以共存。
- 差异化利用:开发阶段使用 Vite 获得飞速体验,生产构建时可能使用 Webpack 以利用其更成熟的打包优化和插件生态来处理复杂项目。
- 特定需求:项目中某个环节可能需要依赖一个只有 Webpack 才有的特定插件。
10. 函数式编程,面向对象编程和过程式编程之间的区别是什么
这是一个编程范式的问题。搜索结果中未提供直接答案,以下是基于常见知识的解释:
编程范式 | 核心思想 | 主要特点 | 典型语言 |
---|---|---|---|
面向对象编程 (OOP) | 将程序视为一系列对象的交互,对象是数据和操作数据的方法的集合。 | 封装、继承、多态。强调状态和行为在对象内的绑定。 | Java, C++, Python, JavaScript |
函数式编程 (FP) | 将计算视为数学函数的求值,避免状态改变和可变数据。 | 纯函数(相同输入永远得到相同输出,无副作用)、不可变性、高阶函数、函数是一等公民。 | Haskell, Lisp, Scala, JavaScript |
过程式编程 (Procedural) | 以步骤和过程(函数)为中心,从上到下线性执行指令。 | 程序由一系列可重用的子程序(函数或过程)调用组成。关注“如何做”的步骤。 | C, Pascal, BASIC |
JavaScript 的特殊性:JS 是一种多范式语言,它支持原型继承(OOP)、函数是一等公民(FP),也可以写出过程式的代码。现代 JavaScript 开发(特别是 React 生态)越来越多地融入函数式思想,如不可变数据、纯组件。
11. 手撕代码
拍平数组并且去重和排序
function flattenAndSort(arr) {// 1. 拍平数组:使用 Array.prototype.flat(Infinity) 可扁平化任意嵌套深度的数组const flatArray = arr.flat(Infinity);// 2. 去重:使用 Set 数据结构,它只允许存储唯一值const uniqueArray = [...new Set(flatArray)];// 3. 排序:默认的 sort() 方法将元素转换为字符串后排序,对于数字数组需提供比较函数const sortedArray = uniqueArray.sort((a, b) => a - b);return sortedArray;
}// 示例测试
const input = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [13, 14]]], 10];
console.log(flattenAndSort(input)); // 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
括号匹配
function isValidBrackets(str) {const stack = []; // 使用栈来存储遇到的左括号const bracketMap = { // 建立一个括号映射表,键为右括号,值为对应的左括号')': '(',']': '[','}': '{'};for (let char of str) {if (['(', '[', '{'].includes(char)) {// 如果是左括号,压入栈中stack.push(char);} else if ([')', ']', '}'].includes(char)) {// 如果是右括号,检查栈顶元素是否与之匹配if (stack.length === 0 || stack.pop() !== bracketMap[char]) {return false; // 栈为空或弹出不匹配,则字符串无效}}}// 遍历结束后,如果栈为空,说明所有括号都正确匹配return stack.length === 0;
}// 示例测试
console.log(isValidBrackets("()[]{}")); // true
console.log(isValidBrackets("([{}])")); // true
console.log(isValidBrackets("(]")); // false
console.log(isValidBrackets("([)]")); // false
console.log(isValidBrackets("{")); // false
前端开发涉及的知识面广且深。下面我将梳理你的问题,并提供清晰、详细的解答,希望能帮你巩固这些核心知识点。
⚙️ 前端与网络技术核心面试题详解
1. 实现 Array.prototype.getLevel() 方法
该方法用于计算数组的最大嵌套深度(即最深层数)。
实现思路:采用递归或迭代的方式遍历数组。对于每个元素,如果是数组,则递归计算其深度;否则,该元素的深度为 0。整个数组的深度是其所有元素深度的最大值加 1。
代码实现:
Array.prototype.getLevel = function() {let maxDepth = 0; // 初始化最大深度// 遍历数组的每一个元素for (const item of this) {let currentDepth = 0; // 当前元素的深度if (Array.isArray(item)) { // 如果当前元素是数组currentDepth = item.getLevel(); // 递归计算其深度}// 更新整个数组的最大嵌套深度if (currentDepth > maxDepth) {maxDepth = currentDepth;}}return maxDepth + 1; // 返回最大深度(需要加上当前层)
};// 测试示例
console.log([1, [2], 3].getLevel()); // 输出: 2
console.log([1, [2, [3]], 4].getLevel()); // 输出: 3
替代迭代方案:除了递归,你也可以使用队列进行广度优先搜索(BFS)或利用JSON.stringify方法通过计算最大方括号嵌套来估算深度,但这些方法可能没有递归直观或准确。
2. 判断数组的方法
JavaScript 中判断一个变量是否为数组有多种方法,各有其适用场景和注意事项:
方法 | 示例 | 优点 | 缺点 |
---|---|---|---|
Array.isArray() | Array.isArray([]) // true | ES5 引入,最可靠、推荐使用的方法 | 在极老的浏览器(如 IE8-)中不支持,但现代开发无需担心 |
Object.prototype.toString.call() | Object.prototype.toString.call([]) // '[object Array]' | 非常可靠,可用于判断所有内置类型 | 语法稍显繁琐 |
instanceof | [] instanceof Array // true | 语法简洁 | 在多个 frame 或 window 之间传递数组时可能失效(因为不同全局环境的 Array 构造函数不同) |
constructor | [].constructor === Array // true | 直接访问构造函数 | 容易被修改,obj.constructor = Array 会导致误判 |
最佳实践:在现代项目中,优先使用 Array.isArray()
。
3. Promise.all, allSettled, race 的区别
这三个方法都用于处理多个 Promise,但行为各异:
方法 | 描述 | 成功条件 | 失败条件 | 结果 |
---|---|---|---|---|
Promise.all(iterable) | 等待所有 Promise 成功(resolve) | 所有输入 Promise 都成功 | 任何一个输入 Promise 失败则立即失败,并返回第一个失败的原因 | 一个包含所有成功结果的数组(顺序与输入一致) |
Promise.allSettled(iterable) | 等待所有 Promise 完成(无论成功或失败) | 总是成功(不会进入 catch) | 不适用(总是成功) | 一个对象数组,描述每个 Promise 的最终状态({status: "fulfilled", value: v} 或 {status: "rejected", reason: r} ) |
Promise.race(iterable) | 等待第一个完成的 Promise(无论成功或失败) | 第一个完成的 Promise 成功 | 第一个完成的 Promise 失败 | 第一个完成的 Promise 的结果(值或原因) |
简单比喻:
Promise.all
: “全部成功才算成功,一个失败全军覆没”。适用于多个异步操作缺一不可的场景,如并行加载多个关键资源。Promise.allSettled
: “不论成败,收集所有结果”。适用于需要知道每个异步操作最终结果的场景,如批量提交表单,需要知道每个请求的成功与否。Promise.race
: “谁第一个完成就听谁的”。适用于竞速或超时控制场景,如为网络请求设置超时时间。
超时控制示例:
// 利用 Promise.race 实现请求超时控制
const fetchData = fetch('/api/data'); // 网络请求
const timeoutPromise = new Promise((_, reject) => {setTimeout(() => reject(new Error('Request timeout')), 5000);
});Promise.race([fetchData, timeoutPromise]).then(data => console.log('数据获取成功:', data)).catch(err => console.error('错误:', err)); // 5秒内未完成则触发超时错误
4. 浏览器输入 URL 到渲染页面的过程(重点 DNS 缓存)
这是一个经典的前端面试题,过程可分为以下几个阶段:
- URL 解析:浏览器解析 URL,提取出协议、主机名、端口、路径等信息。
- DNS 解析(域名解析):这是关键步骤。浏览器需要将主机名(如
www.example.com
)转换为服务器的 IP 地址。DNS 缓存就发生在这个阶段,其查找顺序为:- ① 浏览器缓存:浏览器会缓存之前解析过的域名。
- ② 操作系统缓存(本机缓存):检查本地的 Hosts 文件 (
C:\Windows\System32\drivers\etc\hosts
或/etc/hosts
) 和系统的 DNS 缓存。 - ③ 路由器缓存:查询本地路由器的缓存。
- ④ ISP DNS 缓存(递归查询):向互联网服务提供商(ISP)的 DNS 服务器发起查询。这台服务器会代表你进行递归查询,从根域名服务器(
.
) -> 顶级域名服务器(如.com
) -> 权威域名服务器(如example.com
)一步步查找,并缓存结果。
- 建立 TCP 连接:获取到 IP 后,通过“三次握手”与服务器建立 TCP 连接。
- 发送 HTTP 请求:浏览器构建 HTTP 请求报文并通过 TCP 连接发送给服务器。
- 服务器处理请求并返回响应:服务器处理请求,生成 HTTP 响应报文发回浏览器。
- 浏览器渲染页面:浏览器解析 HTML、CSS,构建 DOM 树和 CSSOM 树,合并成渲染树,然后布局(Layout)和绘制(Paint),最终将页面呈现给用户。
- 关闭 TCP 连接:页面渲染完成后,通过“四次挥手”断开 TCP 连接。
5. 协商缓存
协商缓存是浏览器缓存策略的一种。当强缓存(通过 Cache-Control
和 Expires
控制)失效后,浏览器会向服务器验证本地缓存是否依然有效。这个过程就是协商缓存,旨在减少不必要的数据传输。
-
工作原理:
- 浏览器请求资源时,如果发现本地有该资源的缓存且强缓存已过期,它会携带缓存资源的“标识符”向服务器发起请求。
- 服务器根据这个标识符判断资源是否发生变化。
- 如果未变化,服务器返回
304 Not Modified
状态码,响应体为空。浏览器收到 304 后,会直接使用本地缓存。 - 如果已变化,服务器返回
200 OK
和完整的资源数据。
-
常用标识符:
Last-Modified
(响应头) /If-Modified-Since
(请求头):- 服务器返回资源时带上
Last-Modified
,表示资源的最后修改时间。 - 浏览器下次验证时,会通过
If-Modified-Since
头将这个时间发送给服务器。 - 缺点:精度通常只到秒级;如果文件被重复生成但内容不变,时间戳也会改变,导致缓存失效。
- 服务器返回资源时带上
ETag
(响应头) /If-None-Match
(请求头):ETag
是服务器为资源生成的唯一标识符(通常是哈希值)。只要资源内容不变,ETag
就不会变。- 浏览器下次验证时,会通过
If-None-Match
头将ETag
值发送给服务器。 - 优点:比
Last-Modified
更精确,能准确感知内容变化。 - 缺点:计算
ETag
会有少量服务器性能开销。
最佳实践:通常优先使用 ETag
,因为它能更准确地判断内容是否改变。
6. 渲染环节的 script 标签
浏览器解析 HTML 构建 DOM 时,遇到 <script>
标签会暂停 DOM 构建,先执行 JavaScript 代码。这是因为 JavaScript 可能会修改 DOM 结构。不同的加载方式对页面渲染的影响很大:
加载方式 | 行为 | 对渲染的影响 | 使用建议 |
---|---|---|---|
无属性 (<script> ) | 立即停止 HTML 解析,下载(如果是外部脚本)并同步执行脚本,执行完成后才继续解析 HTML。 | 阻塞解析和渲染,会明显增加页面白屏时间。 | 除非脚本必须立即执行(如用于测量或必须最先运行的代码),否则应避免在 <head> 中或页面顶部使用无属性脚本。 |
async (<script async> ) | 异步下载脚本,下载过程中不阻塞 HTML 解析。下载完成后立即执行,此时会阻塞解析。 | 执行顺序不确定(谁先下载完谁先执行)。适用于完全独立的脚本,如广告、 analytics。 | 用于无需等待 DOM 或其他脚本,且执行顺序无关紧要的第三方脚本。 |
defer (<script defer> ) | 异步下载脚本,但会延迟执行,直到整个 HTML 文档解析完成(DOMContentLoaded 事件之前)才按顺序执行。 | 完全不阻塞 HTML 解析。保证执行顺序。 | 最佳实践 for 大部分需要操作 DOM 或依赖其他脚本的内部脚本。应将脚本放在 <head> 中并添加 defer ,以便浏览器尽早开始下载。 |
现代开发推荐:使用 defer
或将脚本放在 <body>
底部,以最大化减少对渲染的阻塞。
7. Webpack 与 Vite 的区别
特性 | Webpack | Vite |
---|---|---|
构建理念 | 打包器(Bundler)。无论开发还是生产,都需先打包所有模块(合并成少数几个文件)再提供服务。 | 基于原生 ES 模块的开发服务器 + 生产构建打包器。 |
开发服务器启动速度 | 慢。需要先完整打包所有模块,项目越大,启动越慢。 | 极快。无需打包,直接按需提供源文件。启动速度与项目大小无关。 |
热更新(HMR) | 速度随项目增大而变慢。因为需要重新打包变动的模块及其受影响的部分。 | 非常快。利用浏览器原生 ESM 和缓存,通常只更新单个模块,边界更小。 |
工作原理 | 通过各种 loader 和 plugin 处理、打包所有模块。 | 开发环境:浏览器直接通过 ES import 请求模块,Vite 服务器按需编译并返回依赖(如 node_modules 用 esbuild 预构建,源码按需转换)。生产环境:使用 Rollup(高度优化)进行打包。 |
优势 | 生态极其丰富,插件系统强大,能处理各种复杂场景和资源。 | 开发体验极致流畅,开箱即用,对现代 web 项目(如 Vue, React, TS)支持非常好。 |
Vite 开发环境快的原因:其核心在于利用了浏览器对 ES 模块的原生支持。它不需要在开发时打包整个应用,而是启动一个服务器,当浏览器请求模块时,Vite 再按需编译并提供该模块。对于依赖(如 node_modules
),Vite 使用速度极快的 esbuild 进行预构建(将 CommonJS 模块转换为 ESM),从而优化了大量模块的请求性能。
8. 打包工具相关的优化(Webpack 为例)
打包优化主要围绕减小打包体积和提升构建速度两大目标。
-
优化构建速度:
- 缓存:
- 使用
cache-loader
或 Webpack 5 自带的持久化缓存 (cache: { type: "filesystem" }
),避免重复构建未变化的模块。
- 使用
- 减少处理范围:
- 在
loader
规则中,使用exclude
和include
字段,明确指定处理目录,避免对node_modules
等不必要的文件进行处理。
- 在
- 使用更快的工具:
- 用 esbuild 或 swc 替代 Babel 进行代码转换,它们通常比 Babel 快一个数量级。Vite 在开发环境就利用了这一点。
- 缓存:
-
优化输出体积(减小 Bundle Size):
- 代码分割 (Code Splitting):
- 使用
SplitChunksPlugin
提取公共代码和第三方库(如react
,lodash
)到单独文件,避免重复打包。 - 利用动态导入 (
import()
) 实现路由懒加载或组件懒加载,将代码拆分成多个 chunk,按需加载。
- 使用
- Tree Shaking:
- 移除未被使用的代码(Dead Code)。这需要采用 ES2015 模块语法(
import
/export
),因为它的依赖关系是静态的。在package.json
中设置"sideEffects": false
来帮助 Webpack 安全地摇树。
- 移除未被使用的代码(Dead Code)。这需要采用 ES2015 模块语法(
- 压缩:
- 使用
TerserWebpackPlugin
压缩和混淆 JavaScript 代码。 - 使用
CssMinimizerWebpackPlugin
压缩 CSS。
- 使用
- 分析工具:
- 使用
webpack-bundle-analyzer
可视化分析打包结果,找出体积过大的模块,针对性优化。
- 使用
- 代码分割 (Code Splitting):
9. Node.js 与 Golang 的应用
虽然搜索结果未提供直接信息,但根据常见用法,可以这样介绍:
-
Node.js: 它是一个基于 Chrome V8 引擎的 JavaScript 运行时,非常适合 I/O 密集型任务(如网络操作、API 服务)。你可能会用它来:
- 搭建轻量级、高性能的 RESTful API 服务器(使用 Express.js, Koa, NestJS 等框架)。
- 构建全栈应用,前后端都使用 JavaScript,统一技术栈,降低上下文切换成本。
- 编写中间层(BFF - Backend for Frontend),为前端应用聚合不同后端服务的数据。
- 开发工具链,如各种 CLI 工具、构建脚本等,得益于 npm 庞大的生态。
-
Golang (Go): 它是一种编译型静态语言,以简洁的语法、高效的并发模型(goroutine)和出色的性能著称。你可能会用它来:
- 构建高性能、高并发的后端服务,特别适合处理大量并发连接的场景(如微服务、消息推送、实时通信)。
- 编写系统工具或命令行应用,生成的是单一可执行文件,部署简单。
- 处理计算密集型任务,其原生性能通常优于 Node.js。
结合使用:在实际项目中,可以优势互补。例如,用 Go 开发对性能要求极高的核心微服务(如图像处理、复杂计算),同时用 Node.js 开发聚合层(BFF)或面向用户的应用服务器,快速迭代并利用丰富的 JavaScript 生态。
📦 前端核心面试题深度解析
1. Webpack打包
Webpack的核心概念
Webpack是一个静态模块打包器,它通过分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(如Scss、TypeScript等),并将其转换和打包为合适的格式供浏览器使用。其核心概念包括:
- 入口(Entry):指示Webpack应该使用哪个模块来作为构建其内部依赖图的开始。
- 输出(Output):告诉Webpack在哪里输出它所创建的bundles,以及如何命名这些文件。
- Loader:让Webpack能够去处理那些非JavaScript文件(Webpack本身只理解JavaScript)。
- 插件(Plugin):用于执行范围更广的任务,从打包优化和压缩,一直到重新定义环境中的变量。
Webpack打包优化
Webpack打包优化主要从构建速度和打包体积两个方向入手。
优化构建速度:
- 缩小构建范围:在
rules
中使用include
和exclude
来精确指定loader的处理范围,避免对node_modules
等不必要的文件进行处理。 - 缓存:利用Webpack 5内置的持久化缓存 (
cache: { type: 'filesystem' }
),或使用babel-loader
的cacheDirectory
选项,避免重复编译。 - 多进程/并行处理:使用
thread-loader
将耗时的loader(如babel-loader)交给worker线程池处理,提升打包效率。
优化打包体积:
- Tree Shaking:利用ES6模块的静态结构,移除JavaScript上下文中未引用的代码(dead-code)。需要设置
mode: 'production'
,并在package.json
中配置"sideEffects": false
或指定有副作用的文件。 - 代码分割(Code Splitting):使用
SplitChunksPlugin
(Webpack 4及以上)来分离代码,提取公共依赖到单独的chunk,避免重复打包。对于动态导入(如路由组件),使用import()
语法实现懒加载,按需加载代码。 - 压缩:使用
TerserWebpackPlugin
压缩JavaScript,使用CssMinimizerWebpackPlugin
压缩CSS。使用image-webpack-loader
等工具优化和压缩图片。 - 外部扩展(Externals):通过配置
externals
,将一些大型库(如React、Vue、Lodash)排除在打包产物之外,转而通过CDN引入,有效减小bundle体积。
2. React Hooks
常用的Hooks
useState
: 用于在函数组件中添加状态。const [count, setCount] = useState(0);
useEffect
: 用于处理副作用操作(如数据获取、订阅、手动修改DOM等),可以模拟componentDidMount
,componentDidUpdate
,componentWillUnmount
等生命周期。useEffect(() => {// 副作用逻辑const subscription = source.subscribe();// 清理函数(可选)return () => subscription.unsubscribe(); }, [dependency]); // 依赖数组
useRef
: 返回一个可变的ref对象,其.current
属性被初始化为传入的参数。常用于访问DOM节点或存储任何可变值,且更改它不会引发组件重新渲染。const inputEl = useRef(null); useEffect(() => { inputEl.current.focus(); }, []);
useMemo
: 用于缓存昂贵的计算结果,只有在依赖项发生变化时才会重新计算。const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useContext
: 接收一个context对象(React.createContext
的返回值)并返回该context的当前值。const theme = useContext(ThemeContext);
Hooks为什么不能打乱顺序使用
React Hooks必须总是在React函数组件或自定义Hook的顶层调用,不能在循环、条件判断或嵌套函数中调用。这是因为:
- React 依赖于Hooks的调用顺序来正确管理多个Hook的状态。在内部,React为每个组件维护了一个“Hook链表”,每次调用Hook时,它都会移动指针指向下一个Hook。
- 如果Hook的调用顺序在多次渲染之间发生变化(例如,某个Hook在条件语句中有时调用有时不调用),React就无法确定每次渲染时Hook状态的对应关系,从而导致状态混乱和错误。
- 这个规则保证了React能在多次
useState
和useEffect
调用之间保持Hook状态的正确性。
3. Typescript的范型interface
TypeScript中的泛型(Generics)允许我们创建可重用的组件,一个组件可以支持多种类型的数据。
泛型接口(Generic Interface)是应用了泛型的接口,它增加了接口的灵活性。
// 定义一个泛型接口
interface ApiResponse<T = any> {code: number;message: string;data: T; // data的类型由使用接口时指定的类型参数决定
}// 使用泛型接口,指定T为User类型
interface User {id: number;name: string;
}const userResponse: ApiResponse<User> = {code: 200,message: 'success',data: { id: 1, name: 'Alice' }
};// 使用泛型接口,指定T为产品数组类型
interface Product {price: number;title: string;
}const productResponse: ApiResponse<Product[]> = {code: 200,message: 'success',data: [{ price: 100, title: 'Product A' },{ price: 200, title: 'Product B' }]
};// 泛型接口也可以约束类型参数
interface Lengthwise {length: number;
}// 使用 extends 约束 T 必须包含 Lengthwise 的形状
function logLength<T extends Lengthwise>(arg: T): T {console.log(arg.length);return arg;
}logLength({ length: 10, value: 3 }); // OK
// logLength(3); // Error: number doesn't have a .length property
4. Flex原理题
问题:给定父元素大小为600px,子元素A大小500px,flex-shrink: 1
,子元素B大小400px,flex-shrink: 2
。问最后的空间分配如何?
解答:
-
计算总溢出空间:
子元素总基准大小 = 500px + 400px = 900px
父容器可用空间 = 600px
溢出空间 = 900px - 600px = 300px -
计算收缩权重:
收缩权重 = 子元素基准大小 × flex-shrink值
元素A收缩权重 = 500 × 1 = 500
元素B收缩权重 = 400 × 2 = 800
总收缩权重 = 500 + 800 = 1300 -
计算各元素需收缩的空间:
元素A需收缩空间 = (500 / 1300) × 300px ≈ 115.38px
元素B需收缩空间 = (800 / 1300) × 300px ≈ 184.62px -
计算各元素最终大小:
元素A最终大小 = 500px - 115.38px ≈ 384.62px
元素B最终大小 = 400px - 184.62px ≈ 215.38px
延伸:假如元素分配到的收缩空间超过了元素本身大小怎么办?
- 在CSS Flexbox规范中,元素不会收缩到小于其最小内容尺寸(即
min-content
的大小,大致是元素中单词或不可断行内容的最长长度)。 - 如果计算出的收缩尺寸小于元素的
min-content
大小,浏览器会以min-content
作为该元素的最终尺寸。 - 然后,剩余的溢出空间会重新分配给其他具有
flex-shrink
属性且未达到最小内容限制的元素。 - 在实际开发中,可以使用
min-width
或min-height
属性来显式设置Flex项目的最小尺寸,从而更好地控制收缩行为。
5. ES6模板字符串
ES6模板字符串(Template Strings/Literals)是用反引号(`
)标识的字符串,允许嵌入表达式、多行字符串和标签模板。
基本用法:
const name = "Alice";
const age = 25;// 字符串插值:使用 ${} 嵌入变量或表达式
const greeting = `Hello, my name is ${name} and I am ${age} years old.`;
console.log(greeting); // 输出: Hello, my name is Alice and I am 25 years old.// 多行字符串:直接换行即可
const multiLineString = `This is line 1.
This is line 2.This line is indented.`;
console.log(multiLineString);
标签模板(Tagged Templates):
模板字符串可以紧跟在一个函数名后面,该函数(标签函数)会被调用来处理这个模板字符串。
function highlight(strings, ...values) { // ...values 是表达式的值let result = '';strings.forEach((string, i) => {result += string;if (i < values.length) {result += `<span class="highlight">${values[i]}</span>`;}});return result;
}const name = "Alice";
const age = 25;
const highlightedText = highlight`Name: ${name}, Age: ${age}.`;
console.log(highlightedText);
// 输出: Name: <span class="highlight">Alice</span>, Age: <span class="highlight">25</span>.
原始字符串(Raw Strings):
通过String.raw
标签可以获取字符串的原始版本,忽略转义字符。
const path = String.raw`C:\Users\Document\file.txt`; // 不会将 \U, \D, \f 解释为转义字符
console.log(path); // 输出: C:\Users\Document\file.txt
6. 算法题:查找二叉树根节点到叶子结点和为target的路径
问题:给定一个二叉树和一个目标值target,找出所有从根节点到叶子节点的路径,使得路径上所有节点的值之和等于target。
解法(使用深度优先搜索DFS和回溯):
function findPath(root, target) {const result = []; // 存储所有符合条件的路径const currentPath = []; // 记录当前路径function dfs(node, currentSum) {if (!node) return;currentPath.push(node.val); // 将当前节点加入路径currentSum += node.val; // 更新当前和// 如果是叶子节点且当前和等于目标值,将当前路径加入结果if (!node.left && !node.right && currentSum === target) {result.push([...currentPath]); // 注意需要拷贝当前路径}// 递归遍历左子树和右子树dfs(node.left, currentSum);dfs(node.right, currentSum);currentPath.pop(); // 回溯,弹出当前节点,尝试其他分支}dfs(root, 0);return result;
}// 二叉树节点定义
class TreeNode {constructor(val, left = null, right = null) {this.val = val;this.left = left;this.right = right;}
}// 示例用法:
// 构造二叉树:
// 5
// / \
// 4 8
// / / \
// 11 13 4
// / \ / \
// 7 2 5 1
const root = new TreeNode(5);
root.left = new TreeNode(4);
root.right = new TreeNode(8);
root.left.left = new TreeNode(11);
root.left.left.left = new TreeNode(7);
root.left.left.right = new TreeNode(2);
root.right.left = new TreeNode(13);
root.right.right = new TreeNode(4);
root.right.right.left = new TreeNode(5);
root.right.right.right = new TreeNode(1);const target = 22;
console.log(findPath(root, target));
// 输出: [[5, 4, 11, 2], [5, 8, 4, 5]]
你提的这些都是前端面试中常见的重要知识点。我会逐一为你梳理和解答,并提供清晰的解释和代码示例。
📝 前端核心面试题解答
1. React 手写购物车页面
实现一个基本的购物车页面通常涉及展示商品列表、添加商品、移除商品、计算总价等功能。使用 React 的状态管理(如 useState
)是关键。
import React, { useState } from 'react';function ShoppingCart() {const [cartItems, setCartItems] = useState([{ id: 1, name: '商品A', price: 500, quantity: 2 },{ id: 2, name: '商品B', price: 300, quantity: 1 },// ... 其他商品]);// 计算总价const calculateTotal = () => {return cartItems.reduce((total, item) => total + item.price * item.quantity, 0);};// 增加商品数量const increaseQuantity = (id) => {setCartItems(prevItems => prevItems.map(item => item.id === id ? { ...item, quantity: item.quantity + 1 } : item));};// 减少商品数量const decreaseQuantity = (id) => {setCartItems(prevItems => prevItems.map(item => item.id === id && item.quantity > 1 ? { ...item, quantity: item.quantity - 1 } : item));};// 移除商品const removeItem = (id) => {setCartItems(prevItems => prevItems.filter(item => item.id !== id));};return (<div className="shopping-cart"><h2>购物车</h2>{cartItems.length === 0 ? (<p>购物车为空</p>) : (<>{cartItems.map(item => (<div key={item.id} className="cart-item"><span>{item.name}</span><span>单价: {item.price}元</span><div className="quantity-control"><button onClick={() => decreaseQuantity(item.id)}>-</button><span>{item.quantity}</span><button onClick={() => increaseQuantity(item.id)}>+</button></div><span>小计: {item.price * item.quantity}元</span><button onClick={() => removeItem(item.id)}>移除</button></div>))}<div className="cart-total"><h3>总计: {calculateTotal()}元</h3></div></>)}</div>);
}export default ShoppingCart;
关键点说明:
- 状态管理: 使用
useState
Hook 来管理购物车商品的状态。 - 商品操作: 提供了增加数量、减少数量和移除商品的方法。注意在处理状态时不要直接修改原状态,而是创建新的数组或对象。
- 计算属性: 使用
reduce
方法计算总价。 - 条件渲染: 根据购物车是否为空渲染不同的内容。
- 列表渲染: 使用
map
方法渲染商品列表,并为每个元素添加唯一的key
属性。
在实际项目中,你很可能还需要结合 Context API 或 Redux 等状态管理库来跨组件共享购物车状态,以及使用 useReducer
来管理更复杂的状态逻辑。
2. RGBA 颜色转换
RGBA 颜色转换为其他格式(如 HEX)在前端开发中很常见,主要用于颜色处理和兼容性。
RGBA 转 HEX:
RGBA 颜色值包含红®、绿(G)、蓝(B)和透明度(A)四个通道。转换 HEX 时,需要将 R、G、B 的十进制值转换为两位十六进制数,透明度(A)也需要从 0~1 的小数转换为 0~255 的整数再转为两位十六进制。
function rgbaToHex(rgbaStr) {// 从字符串 "rgba(255, 100, 50, 0.5)" 中提取 R, G, B, A 的值const [r, g, b, a] = rgbaStr.match(/\d+(\.\d+)?/g).map(Number);// 将 R, G, B 转换为两位十六进制,不足两位用0补齐const toHex = (value) => {const hex = Math.max(0, Math.min(255, value)).toString(16); // 确保值在0-255之间return hex.length === 1 ? '0' + hex : hex;};// 将透明度 (0~1) 转换为 0~255 的整数,再转为十六进制const alphaHex = toHex(Math.round(a * 255));// 组合成 #RRGGBBAA 格式的字符串并返回return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaHex}`.toUpperCase();
}// 示例
console.log(rgbaToHex('rgba(255, 100, 50, 0.5)')); // 输出: #FF643280
console.log(rgbaToHex('rgba(51, 51, 51, 1)')); // 输出: #333333FF
注意事项:
- 参数验证: 在实际应用中,应添加对输入字符串格式的验证。
- 透明度处理: 透明度 (A) 的取值范围是 0~1,需要乘以 255 并四舍五入转换为整数后再转十六进制。
- 边界处理: 确保 R、G、B 的值在 0~255 之间,A 的值在 0~1 之间。
3. 实现一个三角形(CSS)
纯 CSS 绘制三角形是利用边框(border)特性的一种技巧。
方法:通过边框(Border)
将一个元素的宽度和高度设置为0,然后给边框设置不同的颜色和宽度,其中三条边的颜色设置为透明(transparent)。
<!DOCTYPE html>
<html>
<head>
<style>
.triangle {width: 0;height: 0;border-left: 50px solid transparent;border-right: 50px solid transparent;border-bottom: 100px solid #f00; /* 三角形的颜色 */
}
/* 指向不同方向的三角形 */
.triangle-up {border-left: 50px solid transparent;border-right: 50px solid transparent;border-bottom: 100px solid #f00;
}
.triangle-down {border-left: 50px solid transparent;border-right: 50px solid transparent;border-top: 100px solid #f00;
}
.triangle-left {border-top: 50px solid transparent;border-bottom: 50px solid transparent;border-right: 100px solid #f00;
}
.triangle-right {border-top: 50px solid transparent;border-bottom: 50px solid transparent;border-left: 100px solid #f00;
}
</style>
</head>
<body><div class="triangle"></div><div class="triangle-up"></div><div class="triangle-down"></div><div class="triangle-left"></div><div class="triangle-right"></div>
</body>
</html>
原理: 元素的边框在交界处是斜切的。当内容区域为0时,边框实际上是由三角形组成的。
4. Axios、Fetch 和 Ajax 的区别与请求方式
这些都是在 JavaScript 中进行 HTTP 请求的工具或 API。
特性 | Ajax (XMLHttpRequest) | Fetch API | Axios |
---|---|---|---|
本质 | 使用 XMLHttpRequest 对象的传统技术 | 浏览器原生提供的现代 API | 基于 Promise 的 第三方 HTTP 客户端库 |
语法/易用性 | 回调函数方式,代码相对冗长繁琐 | 基于 Promise,语法简洁 | 基于 Promise,API 设计非常简洁直观 |
默认数据处理 | 需要手动解析 JSON 响应 | 需要调用 .json() 等方法解析响应体 | 自动 转换 JSON 数据 |
请求/响应拦截器 | 不支持 | 不支持 | 支持,便于全局处理请求和响应 |
取消请求 | 支持(abort() ) | 支持(AbortController ) | 支持,且 API 更友好 |
浏览器兼容性 | 非常好(包括 IE) | 现代浏览器(IE 基本不支持) | 现代浏览器(通过 polyfill 可增强兼容性) |
进度事件 | 支持(onprogress ) | 不支持 | 支持(onUploadProgress , onDownloadProgress ) |
CSRF 防护 | 需手动实现 | 需手动实现 | 内置 XSRF token 支持 |
请求方式示例:
Ajax (XMLHttpRequest):
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.onreadystatechange = function() {if (xhr.readyState === 4 && xhr.status === 200) {console.log(JSON.parse(xhr.responseText));}
};
xhr.send();
Fetch API:
// GET 请求
fetch('https://api.example.com/data').then(response => {if (!response.ok) { // Fetch 需要手动检查响应是否成功throw new Error('Network response was not ok');}return response.json(); // 需要调用 .json() 解析}).then(data => console.log(data)).catch(error => console.error('Error:', error));// POST 请求
fetch('https://api.example.com/data', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ name: 'John', age: 30 })
})
.then(response => response.json())
.then(data => console.log(data));
Axios:
// GET 请求
axios.get('https://api.example.com/data').then(response => console.log(response.data)) // 数据在 response.data.catch(error => console.error('Error:', error));// POST 请求
axios.post('https://api.example.com/data', { name: 'John', age: 30 }).then(response => console.log(response.data)).catch(error => console.error('Error:', error));
总结:
- Ajax 是一个广义概念,通常特指使用
XMLHttpRequest
对象,它是基础但写法繁琐。 - Fetch 是更现代、更强大的原生 API,基于 Promise,但需要一些额外处理(如错误检查、JSON解析)。
- Axios 是一个功能丰富的第三方库,提供了很多便利的特性(自动JSON转换、拦截器、取消请求等),简化了开发。
5. window.onload 和 document.ready(DOMContentLoaded)的区别
这两个事件都用于在页面加载过程中执行代码,但触发的时机不同。
特性 | window.onload / load 事件 | DOMContentLoaded 事件 |
---|---|---|
触发时机 | 整个页面及其所有依赖资源(如图片、样式表、脚本、iframe)完全加载完成后 | HTML 文档被完全加载和解析完成(DOM树构建完成)后,无需等待样式表、图片、子框架等外部资源完全加载 |
等待资源 | 等待所有资源 | 不等待样式表、图片等 |
执行速度 | 较慢 | 较快 |
使用方式 | window.onload = function() { ... }; 或 window.addEventListener('load', function() { ... }); | document.addEventListener('DOMContentLoaded', function() { ... }); |
适用场景 | 需要操作依赖于外部资源(如图像大小)的元素 | DOM 操作、绑定事件监听器、初始化界面等,希望尽早交互 |
简单来说: DOMContentLoaded
让你可以更早地操作 DOM,而 window.onload
则确保所有资源都已就绪。
6. 事件捕获、冒泡、事件对象与事件代理
这是 DOM 事件机制的核心概念。
-
事件捕获(Capturing)和事件冒泡(Bubbling):
- 当一个事件发生在某个元素上时,它会经历三个阶段:
- 捕获阶段 (Capture Phase): 事件从
window
向下传播到目标元素的路径。 - 目标阶段 (Target Phase): 事件到达目标元素。
- 冒泡阶段 (Bubble Phase): 事件从目标元素向上冒泡回
window
。
- 捕获阶段 (Capture Phase): 事件从
- 你可以通过
addEventListener
的第三个参数(useCapture
)来选择在哪个阶段监听事件:true
(捕获阶段)或false
(默认,冒泡阶段)。
- 当一个事件发生在某个元素上时,它会经历三个阶段:
-
事件对象 (Event Object):
- 当事件发生时,浏览器会创建一个事件对象,它包含了事件的相关信息(如类型、目标、坐标、按键等)。这个对象会作为参数传递给事件处理函数。
-
element.addEventListener('click', function(event) { // `event` 就是事件对象console.log(event.type); // 'click'console.log(event.target); // 实际触发事件的元素 });
-
事件代理(委托)(Event Delegation):
- 原理:利用事件冒泡机制,将事件监听器绑定在父元素上,通过事件对象的
event.target
属性来识别真正触发事件的子元素,然后执行相应的操作。 - 优点:
- 减少内存消耗:只需要一个事件监听器处理多个子元素的事件。
- 动态添加的子元素无需再单独绑定事件监听器。
- 示例:
document.getElementById('parent-list').addEventListener('click', function(event) {if (event.target && event.target.matches('li.item')) { // 检查点击的目标元素是否是我们关心的元素console.log('List item clicked: ', event.target.textContent);// 执行针对该子元素的处理逻辑} });
- 原理:利用事件冒泡机制,将事件监听器绑定在父元素上,通过事件对象的
7. event.target 和 event.currentTarget 的区别
这两个属性都存在于事件对象中,但在事件流中代表不同的元素。
属性 | event.target | event.currentTarget |
---|---|---|
含义 | 始终指向最初触发事件的( deepest )那个元素(事件发生的源头)。 | 指向当前正在处理该事件的那个元素(即 addEventListener 所绑定的元素)。在事件处理函数中,this 通常等于 event.currentTarget 。 |
是否变化 | 在事件流的整个过程中永不改变。 | 在事件捕获和冒泡阶段,随着事件传播到不同的节点,其值会改变。 |
示例说明 | 如果点击了一个 <button> ,即使事件处理函数绑定在其父元素 <div> 上,event.target 也永远是那个 <button> 。 | 如果事件处理函数绑定在 <div> 上,那么当事件冒泡到 <div> 时,event.currentTarget 就是这个 <div> 。 |
简单比喻:
event.target
:是被点击的按钮(事件真正的源头)。event.currentTarget
:是正在处理这个点击事件的负责人(你绑定事件的元素)。
前端面试确实涉及面广,从基础到原理再到实战都会考察。我来帮你梳理这些问题,并提供清晰、详细的解答。
⚙️ 前端面试核心问题详解
1. 输入URL到页面显示发生了什么
这个过程是前端性能优化和排查问题的基础,可分为以下几个阶段:
- URL解析:浏览器解析URL,提取出协议、主机名、端口、路径和查询参数等信息。
- DNS解析:浏览器将主机名转换为IP地址。查找顺序为:
- 浏览器缓存 → 操作系统缓存(如本地hosts文件) → 路由器缓存 → ISP的DNS服务器 → 若未找到,则进行递归查询(根域名服务器→顶级域名服务器→权威域名服务器)。
- 建立TCP连接:浏览器通过三次握手(SYN, SYN-ACK, ACK)与服务器建立TCP连接,确保可靠的数据传输。若是HTTPS,还会进行TLS握手,交换密钥,建立安全加密通道。
- 发送HTTP请求:浏览器构建HTTP请求报文(包含请求行、请求头、请求体),并通过TCP连接发送给服务器。
- 服务器处理请求并返回响应:服务器处理请求(可能涉及应用逻辑、数据库查询),然后返回HTTP响应(状态码、响应头、响应体)。
- 浏览器解析和渲染
- 解析HTML:构建DOM树(文档对象模型)。
- 解析CSS:构建CSSOM树(CSS对象模型)。
- 合并成渲染树:将DOM和CSSOM合并为渲染树(Render Tree),排除不可见元素。
- 布局(Layout):计算渲染树中每个元素在视口内的确切位置和大小(重排)。
- 绘制(Paint):将布局后的节点转换为屏幕上的实际像素(重绘)。
- 合成(Composite):将各层绘制结果合成为最终页面(如有分层)。
- 加载和执行JS:HTML解析过程中遇到JS会暂停DOM构建(除非脚本是
async
或defer
),执行完毕后继续。 - 连接结束:页面渲染完成后,可能保持TCP连接以供复用,或通过四次挥手关闭连接。
2. 缓存机制(强制、协商)
浏览器缓存可显著提升加载速度,减轻服务器压力,主要分为强缓存和协商缓存。
特性 | 强缓存 (Strong Caching) | 协商缓存 (Negotiated Caching) |
---|---|---|
核心思想 | 直接从本地读取资源,不向服务器发送请求 | 向服务器询问资源是否过期,由服务器决定是否使用缓存 |
触发时机 | 缓存未过期时 | 强缓存失效后 |
HTTP状态码 | 200 (from disk cache) 或 200 (from memory cache) | 304 (Not Modified) |
是否与服务器交互 | 否 | 是 |
相关HTTP头 | Cache-Control 、Expires | Last-Modified / If-Modified-Since 、ETag / If-None-Match |
工作流程:
- 浏览器请求资源时,先检查强缓存。若
Cache-Control
的max-age
或Expires
设置的过期时间未到,则直接使用缓存。 - 若强缓存失效,浏览器会携带缓存标识(如之前的
Last-Modified
或ETag
值)向服务器发起请求,进行协商缓存。 - 服务器验证缓存标识(对比资源最后的修改时间或唯一标识)。若资源未改变,返回
304
,浏览器使用缓存;若已改变,返回200
和最新资源,并更新缓存标识。
常用字段:
- 强缓存:
Cache-Control: max-age=3600
(相对时间,单位秒,优先级高于Expires
)Expires: Wed, 21 Oct 2025 07:28:00 GMT
(绝对时间,HTTP/1.0)
- 协商缓存:
Last-Modified
(响应头) /If-Modified-Since
(请求头):基于时间,精度为秒,可能因文件周期性更改但内容不变而失效。ETag
(响应头) /If-None-Match
(请求头):基于资源内容生成的唯一标识(如哈希值),更精确可靠。服务器会优先验证ETag
。
3. 跨域怎么解决?
跨域是由浏览器的同源策略(协议、域名、端口任一不同)引起的。常见解决方案:
方案 | 原理 | 特点 | 适用场景 |
---|---|---|---|
CORS | 服务器设置Access-Control-Allow-Origin 等响应头,告知浏览器允许哪些源跨域访问资源 | W3C标准,需服务器配合,支持所有HTTP方法,安全性高 | 主流方案,前后端分离项目 |
代理服务器 | 利用同源策略对服务器无效的特点。前端请求同源代理,代理转发请求至目标服务器 | 前端无需修改,开发环境常用(如webpack-dev-server的proxy) | 开发环境,无法修改服务端响应头时 |
JSONP | 利用<script> 标签天然可跨域的特性,通过回调函数接收数据 | 仅支持GET,需服务器配合返回特定JS代码,安全性较低 | 兼容老旧浏览器,简单GET请求 |
Nginx反向代理 | 通过Nginx配置将特定路径的请求代理到目标服务器,统一解决跨域 | 生产环境常用,性能好,配置灵活 | 生产环境部署 |
WebSocket | WebSocket协议本身不受同源策略限制 | 全双工通信,适合实时应用 | 实时通信(如聊天室、股票行情) |
postMessage | HTML5的API,允许跨域的窗口/iframe间安全通信 | 点对点通信,需控制好origin验证 | 跨域iframe间通信 |
CORS详解:
- 简单请求:直接发出,服务器需设置
Access-Control-Allow-Origin: *
(允许所有)或Access-Control-Allow-Origin: https://example.com
(允许特定域)。 - 非简单请求(如PUT、DELETE或带自定义头):浏览器会先发OPTIONS预检请求,询问服务器是否允许实际请求。服务器需响应
Access-Control-Allow-Methods
(允许的方法)、Access-Control-Allow-Headers
(允许的头)等。
4. 闭包怎么理解?
闭包是指一个函数能够记住并访问其所在的词法作用域,即使该函数是在当前词法作用域之外执行。
形成条件:
- 函数嵌套。
- 内部函数引用了外部函数的变量或参数。
- 内部函数被外部函数返回或在外部作用域被持有引用。
简单示例:
function outer() {let count = 0; // outer 函数的局部变量function inner() { // inner 函数,一个闭包count++; // 引用了外部函数的变量 countconsole.log(count);}return inner; // 返回 inner 函数
}const closureFn = outer(); // 执行 outer,返回 inner,赋值给 closureFn
closureFn(); // 1 → 仍能访问 outer 函数作用域内的 count
closureFn(); // 2
closureFn(); // 3
此例中,inner
函数就是一个闭包。outer()
执行后,其作用域本应销毁,但因返回的 inner
函数引用了 outer
的变量 count
,导致 outer
的作用域无法被释放。closureFn
(即 inner
)在任何地方执行时,都能访问到这个 count
。
追问:闭包有啥用?
- 创建私有变量:如上面的
count
,只能通过inner
函数操作,无法直接从外部访问,实现了数据的封装和私有化。 - 实现函数柯里化:预先设置一些参数,返回一个新函数接收剩余参数。
- 模块化开发:在模块模式中,用闭包隐藏实现细节,只暴露公开API。
- 保存状态:如事件处理回调、异步回调(setTimeout、Ajax),需要记住之前的状态时。
追问:外部引用不销毁造成的问题
闭包会导致外部函数的词法作用域(活动对象)无法在执行完毕后被垃圾回收(GC),如果滥用或使用不当,可能导致:
- 内存泄漏:大量变量被闭包引用,无法释放,内存占用不断升高,可能导致页面卡顿甚至崩溃。
- 解决方法:在不需要闭包时,及时将引用它的变量置为
null
(closureFn = null
),打破引用,以便GC回收。
- 解决方法:在不需要闭包时,及时将引用它的变量置为
5. 手撕:防抖
防抖的核心是:事件触发后,等待一段时间再执行函数。若在等待期内事件再次被触发,则重新计时,直到等待期内无新触发,才执行函数。常用于搜索框输入、窗口resize等。
基础实现:
function debounce(func, wait) {let timeoutId; // 使用闭包保存定时器IDreturn function (...args) { // 返回防抖处理后的函数clearTimeout(timeoutId); // 清除之前的定时器,重新计时timeoutId = setTimeout(() => {func.apply(this, args); // 使用 apply 确保正确上下文和参数}, wait);};
}// 使用示例
const input = document.getElementById('search');
const debouncedInputHandler = debounce(function(event) {console.log('Search for:', event.target.value);
}, 500);
input.addEventListener('input', debouncedInputHandler);
追问:防抖里为啥要用func.apply(this, args)
?(this隐式丢失+柯里化)
- 保持
this
上下文:返回的匿名函数可能被当作方法调用(如obj.handler()
),其this
应指向obj
。若不使用apply
,func
中的this
会指向全局对象(非严格模式)或undefined
(严格模式),而非事件触发者。 - 传递正确参数:事件处理函数(如
event
)或其他参数需要通过args
传递给原函数。 apply
方法能显式设置函数执行时的this
值并以数组形式传递参数。
带立即执行选项的防抖:
function debounce(func, wait, immediate = false) {let timeoutId;return function (...args) {const context = this;const callNow = immediate && !timeoutId; // 如果立即执行且当前没有计时clearTimeout(timeoutId);timeoutId = setTimeout(() => {timeoutId = null;if (!immediate) {func.apply(context, args);}}, wait);if (callNow) {func.apply(context, args);}};
}
6. vue-router有几种模式?介绍下。让你自己实现,你打算怎么做?
Vue Router的三种模式:
- Hash模式:使用URL的hash(
#
后面的部分)。变化不会导致浏览器向服务器发送请求,通过监听hashchange
事件响应变化。兼容性最好。 - History模式:利用HTML5 History API(
pushState
,replaceState
)操作浏览器会话历史记录,生成美观的无#
URL。但需服务器配置支持,避免直接访问子路由返回404。 - Abstract模式:主要用于非浏览器环境(如Node.js、SSR、移动端Native),其路由历史记录在内存中维护。
自己实现一个简单的Router(Hash模式)思路:
- 定义一个
Router
类:class MyRouter {constructor(options) {this.routes = {}; // 存储路由路径和对应的组件/回调this.currentUrl = ''; // 当前URLoptions.routes.forEach(route => {this.routes[route.path] = route.component;});this.init(); // 初始化} }
- 初始化及监听hash变化:
init() {window.addEventListener('load', () => this.updateView(), false);window.addEventListener('hashchange', () => this.updateView(), false); }
- 更新视图:
updateView() {this.currentUrl = window.location.hash.slice(1) || '/'; // 获取#后的路径const component = this.routes[this.currentUrl];if (component) {// 简单起见,假设component是渲染函数或组件模板document.getElementById('app').innerHTML = component;} }
- 编程式导航:
push(path) {window.location.hash = path; }
7. VUE源码,模板机制啥的没记清。(不会)
Vue的模板机制涉及编译和渲染过程:
- 编译:Vue会将模板(template)编译成渲染函数(render function)。这个过程包括:
- 解析:将模板字符串解析成抽象语法树。
- 优化:遍历AST,标记静态节点和静态根节点,在后续更新中跳过它们,优化性能。
- 生成代码:将AST生成可执行的渲染函数代码字符串。
- 渲染:执行渲染函数,会递归地创建虚拟DOM树。后续数据发生变化时,会生成新的VNode树,通过与旧VNode树进行Diff算法对比,计算出最小更新量,然后patch到真实DOM上。
8. 说一下你了解到的VUE2、3区别(双向绑定区别、diff)
方面 | Vue 2 | Vue 3 |
---|---|---|
响应式原理 | Object.defineProperty | Proxy |
性能 | 需要递归遍历数据对象、初始化时性能稍差 | 惰性代理、性能更好,尤其对于大型对象/数组 |
新增/删除属性 | 无法直接检测,需Vue.set /Vue.delete | 直接检测 |
数组变化 | 需重写数组方法(push, pop等)或Vue.set | 直接检测 |
Diff算法 | 双端比较 | 优化:静态标记(PatchFlag)、静态提升、树结构优化,更新效率更高 |
API风格 | 主要Options API | 兼容Options API,主打Composition API(setup ) |
生命周期 | beforeCreate , created 等 | 选项式API名称不变,Composition API有onMounted 等钩子 |
Fragment/Teleport/Suspense | 不支持 | 支持 |
TypeScript支持 | 需要装饰器等,支持度一般 | 原生支持更好 |
双向绑定原理区别:
- Vue 2:基于
Object.defineProperty
劫持数据属性的getter
和setter
。需要递归遍历数据对象,且对数组方法需特殊处理。无法检测属性的添加和删除。 - Vue 3:基于
Proxy
代理整个对象。功能更强大,能直接监听对象和数组的各种变化(增、删、改),无需特殊API。性能也更优。
Diff算法优化:
Vue 3的Diff算法在Vue 2双端比较的基础上,利用编译时的优化信息(如PatchFlag):
- 静态提升:将静态节点提升到渲染函数外,避免重复创建。
- PatchFlag:在编译时给VNode打上标记(如
TEXT
,CLASS
),Diff时只需对比带有动态内容的节点,跳过大量静态内容对比。 - 树结构优化:编译时检测静态根节点,减少比对粒度。
9. 平时学习新知识吗?
(此题无标准答案,考察学习习惯和主动性)
可以坦诚回答“是”,并举例说明你如何学习:
- 关注社区:阅读掘金、博客园、GitHub Trending、技术新闻(如InfoQ)。
- 系统学习:看官方文档、在线课程(慕课网、极客时间)、阅读经典书籍。
- 实践驱动:在个人项目或工作中尝试新技术,阅读优秀开源项目源码。
- 输出总结:写博客、做笔记,加深理解。
10. 0.1+0.2 有什么问题
问题:0.1 + 0.2 !== 0.3
,结果可能是 0.30000000000000004
。
原因:这是由浮点数精度丢失引起的。JavaScript(遵循IEEE 754标准)使用双精度浮点数(64位)存储数字。0.1
和0.2
这样的十进制小数在转换为二进制浮点数时是无限循环的,就像1/3
在十进制中是0.333...
一样。由于存储空间有限,计算时就会发生精度丢失。
解决方法:
- 显示时格式化:
(0.1 + 0.2).toFixed(1) // "0.3"
(注意结果是字符串)。 - 转换为整数计算后再转回:
(0.1 * 10 + 0.2 * 10) / 10 === 0.3
。 - 使用第三方数学库(如
decimal.js
)处理精确计算。
11. 如何跟多个后端对接?如果后端接口格式不一致(后端有问题)你要怎么办?
协作流程:
- 前期沟通:参与接口评审,明确数据格式、错误码规范、联调时间等。
- 文档管理:使用Swagger/OpenAPI等工具维护接口文档,确保双方理解一致。
- Mock数据:前期根据文档Mock数据,并行开发,不阻塞进度。
- 统一请求层封装:在项目中统一封装HTTP请求工具(如基于axios),处理公共逻辑(如基地址、超时、认证、错误处理)。
接口格式不一致问题:
- 主动沟通:首先与相关后端开发者沟通,说明不一致性带来的问题(如前端难以统一处理),推动对方遵循约定规范。
- 适配器模式:如果短期内无法统一,在前端设计适配层(Adapter)。为每个不一致的接口编写特定的数据转换函数,将不同格式转换为前端统一的格式。
// 适配器示例 const adapterForBackendA = (data) => {return {id: data.user_id,name: data.user_name// ... 将后端A的数据结构转换为前端通用结构}; }; const adapterForBackendB = (data) => {return {id: data.id,name: data.name// ... 后端B的数据结构可能更接近通用结构,转换较少}; };
- 向上反馈:若问题普遍且沟通无效,及时向项目经理或技术负责人反馈,从更高层面推动规范制定和执行。
12. 用过Node嘛?用过他的中间件嘛?(用的少,没追问了)
(根据实际情况回答)可以回答“了解/用过一些”,并简要说明:
- Node.js:是一个基于Chrome V8引擎的JavaScript运行时,允许在服务器端运行JS。常用于构建Web服务器、API网关、CLI工具等。
- 中间件:是指在请求-响应周期中,对请求和响应对象执行特定任务的函数。在Express/Koa等框架中,中间件函数可以:
- 执行任何代码。
- 修改请求和响应对象。
- 结束请求-响应周期。
- 调用堆栈中的下一个中间件。
- 常见中间件:日志记录(morgan)、解析请求体(body-parser)、会话管理(express-session)、错误处理等。
13. 工作中遇到比较困难的东西(说了个遇到的diff算法bug)
(此题无标准答案,考察问题解决能力和经验)
示例回答:曾遇到一个Vue列表渲染性能问题。一个大型列表项在频繁更新时非常卡顿。排查发现是v-for
中使用了index
作为key。当列表顺序变化时,基于index的key会导致Vue的Diff算法无法高效复用已有节点,需要进行大量不必要的DOM操作和状态更新。解决方案是改为使用数据项中唯一且稳定的id
字段作为key。这使得Diff算法能准确跟踪每个节点的身份,最大程度复用DOM,性能得到显著提升。
14. 手撕:包装一个ajax方法,在不修改外部使用方法的情况下(.then、传参不变),内部并发最多3个ajax,任意一个完成就立即执行另外一个(队列不靠谱。这玩意根本不是从题库里挑的题目,不会,所以换下一题写了)
这题考察的是控制并发数的Promise调度器。
实现思路:
- 维护一个等待队列(存储待执行的异步任务函数)。
- 维护一个当前并发数计数器。
- 提供一个方法,用于添加任务。添加时,若当前并发数未达上限(3),则立即执行;否则放入队列等待。
- 任务执行完毕后,并发数减1,并检查队列中是否有等待任务,有则取出执行。
代码实现:
class ConcurrentAjax {constructor(maxConcurrent = 3) {this.maxConcurrent = maxConcurrent; // 最大并发数this.currentCount = 0; // 当前并发数this.queue = []; // 任务队列}// 添加任务(外部调用方法,返回Promise)add(task) { // task是一个返回Promise的函数,如 () => axios.get(url)return new Promise((resolve, reject) => {const runTask = () => {this.currentCount++;task().then(resolve).catch(reject).finally(() => {this.currentCount--;this._next(); // 一个任务完成,尝试执行下一个});};if (this.currentCount < this.maxConcurrent) {runTask(); // 当前并发数未满,立即执行} else {this.queue.push(runTask); // 否则加入队列等待}});}// 执行队列中的下一个任务(私有方法)_next() {if (this.queue.length > 0 && this.currentCount < this.maxConcurrent) {const nextTask = this.queue.shift();nextTask();}}
}// 使用示例
const scheduler = new ConcurrentAjax(3);// 外部使用方式不变,仍然是 .then 和 .catch
scheduler.add(() => axios.get('/api/data1')).then(console.log);
scheduler.add(() => axios.get('/api/data2')).then(console.log);
scheduler.add(() => axios.get('/api/data3')).then(console.log);
scheduler.add(() => axios.get('/api/data4')).then(console.log); // 这个会等待前3个中的一个完成后再执行
15. 手撕:最大连续递增子字符串
问题:给定一个字符串,找出其中最长的连续递增子字符串。
例如:"abcdeabcdefghij"
的最长连续递增是 "abcdefghij"
(长度10),"abcfbcdef"
的是 "bcdef"
(长度5)。
思路:使用滑动窗口(双指针)。
- 初始化两个指针
start
(当前递增序列的开始)和end
(当前遍历位置),以及变量maxStart
和maxLength
记录最长序列的起始索引和长度。 - 遍历字符串(从第2个字符开始):
- 如果当前字符比前一个字符的ASCII码大(即递增),则继续扩展窗口(
end++
)。 - 否则,说明递增序列中断。计算当前序列长度
end - start
,若大于maxLength
则更新maxStart
和maxLength
。然后将start
移动到end
处,开始新的序列。
- 如果当前字符比前一个字符的ASCII码大(即递增),则继续扩展窗口(
- 遍历结束后,再最后计算一次当前序列长度并更新结果。
- 根据
maxStart
和maxLength
返回子串。
代码实现:
function findLongestIncreasingSubstring(str) {if (str.length <= 1) return str;let start = 0;let maxStart = 0;let maxLength = 1;for (let end = 1; end < str.length; end++) {// 如果当前字符不大于前一个字符,递增序列中断if (str.charCodeAt(end) <= str.charCodeAt(end - 1)) {const currentLength = end - start;if (currentLength > maxLength) {maxLength = currentLength;maxStart = start;}start = end; // 重置start到当前位置,开始寻找新的序列}// 如果是最后一个字符,需要检查一次if (end === str.length - 1) {const currentLength = end - start + 1;if (currentLength > maxLength) {maxLength = currentLength;maxStart = start;}}}return str.substring(maxStart, maxStart + maxLength);
}// 测试
console.log(findLongestIncreasingSubstring("abcdeabcdefghij")); // "abcdefghij"
console.log(findLongestIncreasingSubstring("abcfbcdef")); // "bcdef"
前端性能优化是个系统工程,涉及网络、资源、代码、渲染等多个层面。下面我将结合你的问题,梳理性能优化的关键点,并对比 React 和 Vue 的不同,最后介绍微前端、ahooks 和 React 18 的新特性。
⚙️ 前端性能优化与框架对比
1. 深度性能优化:火焰图、Performance API 与全方位策略
前端性能优化是一个涵盖网络加载、资源处理、代码执行、渲染性能等多个方面的系统工程。其核心目标是提升用户体验,减少加载时间,增强交互流畅度。
性能优化核心策略
以下表格概括了性能优化的主要方向和具体措施:
优化方向 | 具体策略与示例 | 关键工具/API |
---|---|---|
网络加载优化 | - 减少HTTP请求:合并文件、使用雪碧图 - 启用压缩:Gzip/Brotli压缩文本资源 - 利用缓存:强缓存 ( Cache-Control )、协商缓存 (ETag )- 使用CDN加速静态资源分发 | Chrome DevTools Network面板 |
资源优化 | - 代码压缩与摇树:使用 Terser、UglifyJS 压缩 JS,CSSNano 压缩 CSS,移除未使用代码 - 图片优化:使用 WebP/AVIF 格式,响应式图片 ( srcset ),懒加载 (loading="lazy" )- 字体优化:使用 font-display: swap 避免阻塞渲染 | Webpack Bundle Analyzer, ImageOptim |
渲染性能优化 | - 减少重排重绘:批量DOM操作,使用 transform /opacity - 优化CSS:避免深层嵌套选择器,使用 Flex/Grid 布局 - GPU加速:使用 will-change 或 transform: translateZ(0) | Chrome DevTools Performance面板 |
JavaScript优化 | - 代码拆分与懒加载:动态导入 (import() ), React.lazy, Vue 异步组件- 避免阻塞主线程:使用 Web Workers 处理密集型任务 - 事件优化:防抖(Debounce)与节流(Throttle) | Webpack, Vite |
缓存策略 | - 强缓存:Cache-Control: max-age=31536000 - 协商缓存: ETag / Last-Modified - Service Worker:实现离线缓存和动态缓存策略 | Workbox |
预加载与预渲染 | - 资源预加载:<link rel="preload"> (关键资源), prefetch (未来页面资源)- 预渲染: prerender 提前渲染下一页 | |
性能监测与分析 | - Core Web Vitals:LCP (最大内容绘制), FID (首次输入延迟), CLS (累积布局偏移) - 性能分析工具:Lighthouse, WebPageTest, Chrome DevTools Performance面板 - 真实用户监控 (RUM):Sentry, New Relic | Lighthouse, WebPageTest |
🔥 火焰图(Flame Graph)实战
火焰图是由 Brendan Gregg 发明的一种可视化性能分析工具,它将采样数据转换为层次化的图表,直观展示函数调用栈和 CPU 时间消耗分布。其核心特征如下:
特征 | 说明 | 分析价值 |
---|---|---|
宽度 | 表示函数执行时间占比 | 识别性能瓶颈 |
高度 | 表示调用栈深度 | 理解调用关系 |
颜色 | 通常表示不同模块或库 | 区分系统/用户空间 |
排序 | 按字母顺序或采样计数 | 便于查找特定函数 |
生成火焰图的基本步骤:
- 数据采集:使用
perf record
等工具收集 CPU 调用栈信息。perf record -F 99 -g -p <pid> -- sleep 30
- 处理数据:将采集的数据转换为适合生成的格式。
perf script > out.perf
- 生成SVG:使用 FlameGraph 工具链生成交互式火焰图。
./stackcollapse-perf.pl out.perf > out.folded ./flamegraph.pl out.folded > flamegraph.svg
如何分析火焰图:
- 查找最宽的平顶山:这些是消耗 CPU 时间最多的函数,是优化的首要目标。
- 关注调用栈深度:过深的调用链可能意味着存在过度封装或复杂的逻辑。
- 识别常见模式:
- 宽平顶:高耗时函数,计算密集型热点。
- 长调用链:过度封装。
- 重复锯齿:低效循环。
📊 Performance API 详解
Performance API 是浏览器提供的一组用于测量和监控网页性能的接口。它提供了丰富的性能数据,如页面加载时间、资源加载性能、用户交互延迟等。
常见使用场景:
- 测量页面加载时间:
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart; console.log(`页面加载时间: ${loadTime}ms`);
- 获取资源加载性能:
performance.getEntriesByType('resource').forEach(resource => {console.log(`${resource.name} 的加载时间: ${resource.duration}ms`); });
- 测量代码执行时间:
const start = performance.now(); // 执行某些操作 const end = performance.now(); console.log(`耗时: ${end - start}ms`);
- 监控用户交互延迟(如点击延迟):
document.addEventListener('click', () => {const clickDelay = performance.now() - event.timeStamp;console.log(`用户点击延迟: ${clickDelay}ms`); });
2. React 与 Vue 的上手区别
React 和 Vue 都是优秀的前端框架,但在上手体验和设计哲学上有所不同。
方面 | React | Vue |
---|---|---|
设计理念 | 声明式和函数式编程,推崇“单向数据流”和“不可变性” | 渐进式框架,核心库专注于视图层,易于上手,并支持逐步采用更复杂的功能。提供更模板化和响应式的体验。 |
组件编写方式 | 主要使用 JSX(JavaScript XML),允许在 JavaScript 中编写类似 HTML 的语法,JavaScript 能力更强。 | 主要使用单文件组件 (SFC),将模板、逻辑和样式封装在单个 .vue 文件中,结构更清晰。 |
状态管理 | 使用 useState Hook (函数组件) 或 setState (类组件),状态更新需要手动处理(不可变更新)。 | 使用 data 返回响应式对象,Vue 自动处理响应式变化,直接修改属性即可更新视图。 |
样式处理 | 样式方案多样,如 CSS Modules、Styled-components、CSS-in-JS,无官方指定方式。 | 在 SFC 中支持 <style> 块,可搭配 scoped 属性实现组件作用域 CSS,提供更集成的样式解决方案。 |
学习曲线 | 对 JavaScript 基础要求较高,概念相对更抽象(如 Hook 规则、不可变性)。 | 对于有 HTML/CSS 背景的开发者更友好,模板和选项式的 API 更直观,易于入门。 |
生态系统 | 生态系统庞大且灵活,但路由、状态管理等需选择第三方库(如 React Router, Redux, Zustand)。 | 官方提供了全家桶(Vue Router, Pinia, VueUse),集成度更高,减少了选择成本。 |
构建工具 | 常用 Create React App (CRA),但也可选择 Vite、Next.js 等。 | 常用 Vue CLI 或 Vite(尤雨溪同时是 Vite 的作者),Vite 提供极快的开发服务器启动和热更新。 |
选择建议:
- 如果你喜欢灵活性和强大的 JavaScript 表达力,享受自主选择技术栈,React 及其丰富的生态系统可能更适合你。
- 如果你希望更平缓的学习曲线、更集成的官方解决方案以及模板化的开发体验,Vue 可能是更好的起点。
3. 微前端(Micro Frontends)概念与解决的问题
微前端是一种将前端应用程序分解为更小、更简单、可以独立开发、测试、部署和运行的模块的架构风格。它借鉴了微服务的概念,将后端微服务的理念应用于前端。
微前端主要解决以下问题:
- 单体前端应用膨胀:随着项目迭代,单体应用变得臃肿,构建、测试、部署速度变慢,代码维护成本高。
- 技术栈迭代与多样化:允许不同团队根据需求或偏好,在不同子项目或模块中采用不同的技术栈(如 React, Vue, Angular)。
- 团队协作与自治:多个团队可以独立开发、测试和部署其负责的前端部分,提升并行效率和团队自主权。
- 增量升级与迁移:允许逐步重构或替换老系统,降低大规模重写的风险。
常见的微前端实现方案:
- 基座模式:一个主应用(基座)负责注册、集成、路由转发和调度各子应用。
- Web Components:使用浏览器原生技术实现组件级别的隔离和集成。
- 模块 Federation:通过 Webpack 5 的 Module Federation 功能,实现应用间的模块共享和远程加载。
4. ahooks 中印象深刻的 Hooks
ahooks 是一个高质量且强大的 React Hooks 库。其中一些非常实用的 Hooks 包括:
- useRequest:一个强大的异步数据管理 Hook。它自动处理请求的
loading
,data
,error
状态,并提供了缓存、轮询、防抖、节流、依赖刷新、屏幕聚焦重新请求等强大功能,极大简化了数据请求的逻辑。const { data, error, loading } = useRequest(getUserInfo, {onSuccess: (data) => { console.log(data); },refreshOnWindowFocus: true, // 屏幕聚焦重新请求 });
- useAntdTable:与 Ant Design 表格组件深度结合,专为处理表格数据请求和分页而设计,自动管理分页参数、筛选条件,并与
useRequest
无缝集成。 - useSize:用于监听 DOM 节点的尺寸变化,获取其宽高信息。基于
ResizeObserver
API 实现。const size = useSize(document.getElementById('container')); console.log(size); // { width: 100, height: 50 }
- useDebounce / useThrottle:对值或函数进行防抖或节流处理,常用于处理频繁触发的事件,如搜索输入、窗口滚动等。
- useLocalStorageState / useSessionStorageState:方便地在组件状态和
localStorage
或sessionStorage
之间同步数据。
5. React 18 新特性
React 18 是一个主要版本,引入了许多重要特性,专注于并发特性和性能提升:
- 并发特性 (Concurrent Features):React 18 的核心是引入了并发渲染器,它允许 React 在渲染过程中进行中断、暂停和恢复,以便浏览器能够优先处理用户交互等高优先级任务,从而提升应用的响应速度和用户体验。
- 自动批处理 (Automatic Batching):React 18 通过在更多场景(如
setTimeout
、Promise
等)中自动将多个状态更新合并为一次重新渲染,减少了不必要的渲染次数,提升了性能。 - Transitions:提供了
useTransition
和startTransition
API,用于区分紧急更新(如用户输入)和非紧急更新(如搜索结果渲染)。非紧急更新可以被中断,从而不会阻塞紧急更新和用户交互。 - 新的 Root API:引入了新的
ReactDOM.createRoot()
API 来替代旧的ReactDOM.render()
,这是启用所有并发功能的基础。 - Suspense 增强:Suspense 现在支持在服务端渲染(SSR)中使用,允许流式传输 HTML 和在客户端逐步渲染,减少“白屏时间”。
- 新的 Hooks:
useId
:用于生成在客户端和服务端之间保持唯一的 ID,常用于无障碍访问(a11y)。useSyncExternalStore
:供第三方状态管理库(如 Redux)集成并发特性。useInsertionEffect
:主要用于 CSS-in-JS 库动态注入样式。
6. 个人性能优化实践与效果
性能优化的效果因项目而异。常见的优化措施和可能的效果包括:
- 图片优化:将 PNG/JPG 转换为 WebP,并实施懒加载。效果:页面加载体积减少 40%-60%,LCP 时间提升 20%-40%。
- 代码分割与懒加载:使用 React.lazy 和动态 import 进行路由级和组件级拆分。效果:首屏资源体积减少 50%-70%,首次输入延迟(FID)有所改善。
- 第三方库优化:分析 bundle,移除未使用的代码(Tree Shaking),或用更轻量的库替代大型库(如用 date-fns 替换 moment.js)。效果:总体 bundle 大小减少 15%-30%。
- 缓存策略:为静态资源设置长期缓存(
Cache-Control: max-age=31536000
)。效果:重复访问页面加载速度极快,资源二次加载耗时接近 0。 - 性能监控:接入 Performance API 和 Web Vitals 监控,持续跟踪核心指标。效果:能持续发现并定位性能瓶颈,针对性进行优化。
优化效果衡量:优化前后应使用 Lighthouse、WebPageTest 等工具进行量化对比,重点关注 Core Web Vitals(LCP, FID, CLS)等指标的变化。