sqb&ks二面(准备)

你对这些前端面试题目的把握相当准确,很多地方都切中了要点。我来帮你逐一梳理和补充,希望能助你面试一臂之力。

面试题目 核心考察点 关键答案
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 而非 setIntervalsetTimeout 来控制动画循环。它能保证回调频率与浏览器刷新率一致,并在页面不可见时暂停,节省资源。

🔐 2. Canvas绘制验证码详情

你已掌握了前端绘制的要点(随机字符、扭曲、干扰线)。这里重点补充安全与后端交互

  • 核心原则:前端仅负责展示,验证逻辑必须在后端

    • 流程
      1. 用户请求页面时,后端 生成随机验证码文本(如A7X9)。
      2. 后端将该文本存入当前用户的Session或缓存(如Redis),并与其Session ID关联。
      3. 后端根据该文本生成图像数据(可能用Node.js Canvas库)或调用前端API,将图像返回前端。
      4. 前端Canvas仅负责展示该图片
      5. 用户输入验证码并提交。
      6. 后端比对用户输入的验证码与Session中存储的是否一致,并返回验证结果。
      7. 无论成败,立即使Session中的旧验证码失效(防止重复使用或暴力破解)。
  • 为何不能仅前端验证:前端代码和验证码答案对用户是透明的,攻击者可以轻易绕过Canvas展示环节,直接读取验证码答案或提交请求。

📝 3. CKEditor原理

CKEditor 5 采用了自定义数据模型(Custom Data Model) 的架构,这与直接操作DOM的传统编辑器有根本区别。

  • 数据模型与视图分离
    • 数据模型(Model) :一个结构化的、线性的数据表示,存储文档内容(如段落、文本、图片及其属性),不直接关心视觉呈现。它确保了数据的完整性和一致性。
    • 视图(View) :负责将数据模型渲染为用户可见的DOM内容。它使用虚拟DOM技术来进行高效的差分更新,只更新必要的DOM节点,而不是重绘整个编辑器。
  • 操作原子性与撤销/重做 :所有对文档的修改都通过一个 ModelWriter 接口完成,并在一个事务(Transaction) 中进行。这确保了每个操作要么完全成功,要么完全失败,使得撤销(Undo)和重做(Redo)功能可以完美实现。
  • 获取光标位置 :你的记忆是正确的!对于可编辑区域(如contenteditable的div)或文本框(<textarea><input>),可以通过 selectionStartselectionEnd 属性来获取光标位置或选中的文本范围。

📊 4. 大数据量表格的处理方法

  • 分页(Pagination)最直接、最常用的方法 。每次只请求和渲染一页数据(如20条)。优点 :实现简单,减轻前后端压力。缺点:无法一次性浏览所有数据。
  • 虚拟滚动(Virtual Scrolling)处理超大数据量的利器 。只渲染可视区域及其附近 的表格行(DOM节点),随着滚动动态回收不可见的节点并填充新的数据。优点 :无论数据有多少,实际DOM节点数量恒定,性能极佳。缺点:实现较为复杂,需要精确计算滚动位置和行高。
  • 懒加载(Lazy Loading):可以结合虚拟滚动使用。当滚动到底部时,才去加载下一页或下一批数据。

⚙️ 5. 前端一次性拿到大量数据并过滤

一次性拿到10万条数据,前端过滤的关键是避免主线程阻塞

  • 数据分片(Data Slicing):将大数据分成多个较小的块(Chunks)。
  • 使用 Web Workers :这是核心解决方案 。Web Worker 允许你在后台线程 中运行脚本,不会阻塞主线程的UI渲染。
    • 流程
      1. 主线程将数据分片,并分配给多个Web Worker并行进行过滤操作。
      2. 每个Worker处理自己那一份数据片,返回过滤结果。
      3. 主线程等待所有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() 函数直接执行字符串形式的代码。
  • 直接通过 innerHTMLouterHTML 属性插入未转义的HTML字符串,其中的 <script> 标签会被执行。
  • 动态创建 <script> 标签并将其 src 属性指向不可信的URL。
  • 反序列化来自用户或第三方不可信来源的JSON数据时,如果使用 eval() 进行解析(应使用 JSON.parse())。

🍪 4. 问什么可以让攻击者能够在点击一个链接会获取到cookie

攻击者主要通过反射型XSS攻击来实现这一点:

  1. 构造恶意链接 :攻击者找到一个存在XSS漏洞的网站,构造一个特殊的URL,参数中包含恶意脚本,例如 http://example.com?search=<script>alert('XSS')</script>
  2. 诱导点击:通过社交工程学(如伪装成中奖信息、重要通知等)诱使用户点击此链接。
  3. 服务器返回恶意脚本:存在漏洞的网站未对搜索参数进行过滤,直接将恶意脚本嵌入到返回的HTML页面中。
  4. 浏览器执行脚本:用户的浏览器解析并执行了返回页面中的恶意脚本。
  5. 窃取Cookie :恶意脚本中通常包含类似 document.cookie 的代码,它能读取当前站点的Cookie信息。然后脚本可能通过向攻击者控制的服务器发送一个HTTP请求(如在 Image 对象的 src 属性中附加Cookie信息)将数据窃取走。

🔍 5. 假设在浏览器中输入www.taobao.com到页面渲染的过程

这个过程大致可分为以下步骤:

  1. DNS解析 :浏览器解析域名 www.taobao.com 对应的IP地址。查询顺序为:浏览器缓存 → 系统缓存 → 路由器缓存 → ISP DNS服务器 → 根域名服务器 → .com顶级域名服务器 → 淘宝权威DNS服务器。
  2. 建立TCP连接 :浏览器获得IP后,与服务器通过三次握手 建立TCP连接。由于淘宝使用HTTPS,还会进行 TLS握手 协商加密密钥。
  3. 发送HTTP请求 :浏览器通过已建立的连接向服务器发送HTTP GET请求,请求头中包含资源路径、主机、Cookie等信息。
  4. 服务器处理请求并返回响应:服务器可能经过负载均衡器,将请求分发到后端应用服务器处理,生成HTML文档作为HTTP响应返回。
  5. 浏览器解析与渲染
    • 解析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 animationtransition 应用的元素在动画过程中会持续重绘。
  • 布局抖动(Layout Thrashing):JavaScript频繁交替读写DOM样式,迫使浏览器不断重新计算布局和渲染。
  • 第三方库或插件:某些库可能在内部执行循环任务或监听器,导致持续渲染。

🔔 9. DOM还没有点击操作时,哪些情况会触发这个reRender

即使没有点击操作,以下情况也可能触发重新渲染:

  • 定时器setIntervalsetTimeout 中的代码修改了DOM或样式。
  • 异步请求回调:Ajax或Fetch请求完成后,在回调函数中更新数据导致UI刷新。
  • CSS动画和过渡 :CSS animationtransition 会触发重绘。
  • 媒体查询变化:视口大小改变或设备方向旋转导致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 等。

执行顺序原则:同步代码 > 微任务 > 宏任务。在同一次事件循环中,微任务总在下一个宏任务之前执行。

分析技巧

  1. 先找出所有同步代码并执行。
  2. 识别异步代码,区分宏任务和微任务。
  3. 每个宏任务执行后,都会检查微任务队列并执行所有微任务。
  4. process.nextTick (Node.js) 的优先级高于 Promise.then。

经典示例

javascript 复制代码
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() (传统方式)

此方法返回元素的大小及其相对于视口的位置。

javascript 复制代码
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 (现代方式)

这是一个性能更优的解决方案,无需监听滚动事件,浏览器会自动回调。

javascript 复制代码
// 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 内存分配。

使用步骤

  1. 打开 Chrome DevTools (F12) → 切换到 Performance 面板。
  2. 点击 Record (圆形图标) 开始录制。
  3. 在页面上进行你想要分析的操作(如滚动、点击)。
  4. 点击 Stop 停止录制。
  5. 在生成的报告中分析各项指标:
    • 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)。
  • 减少请求数量和传输体积
    • 合并文件:将小文件合并(如雪碧图),减少 HTTP 请求次数。
    • 开启 Gzip/Brotli 压缩:在服务器端压缩文本资源(JS, CSS, HTML)。
    • 使用 HTTP/2:利用多路复用特性,提升资源加载效率。
  • 优化渲染路径
    • 内联关键 CSS:将渲染首屏内容所需的核心样式直接内嵌在 HTML 中,减少请求。
    • 延迟加载非关键 JS/CSS :使用 asyncdefer 属性加载非阻塞脚本。
  • 利用缓存
    • 配置浏览器缓存 :通过设置 Cache-ControlETag 等 HTTP 头,让浏览器缓存静态资源。
    • 使用 Service Worker:实现离线缓存和更精细的缓存策略。

5. Vue中的 nextTick 有什么用

Vue.nextTick() 或实例方法 this.$nextTick() 用于在 下次 DOM 更新循环结束之后 执行延迟回调。

核心作用 :当你修改了 Vue 组件的数据后,DOM 并不会立即更新。Vue 会开启一个异步队列 来缓冲同一事件循环中的所有数据变更,然后批量更新视图以提高性能。使用 nextTick 可以确保你的回调函数在 DOM 更新完成后执行。

主要使用场景

  1. 获取更新后的 DOM :当你改变数据后,需要立即操作依赖于新数据的 DOM。

    javascript 复制代码
    this.message = 'Hello!'; // 修改数据
    // DOM 还未更新
    this.$nextTick(() => {
      // 这里可以获取到更新后的 DOM
      const element = this.$el.textContent; // 现在 element 的值是 'Hello!'
    });
  2. created 生命周期钩子中操作 DOM:created 时 DOM 还未渲染,任何 DOM 操作都应放在 nextTick 中。

  3. 与第三方插件集成:在 Vue 更新 DOM 后,调用需要操作更新后 DOM 的第三方库。

原理简述 :Vue 内部会尝试使用原生的 Promise.then(微任务)、MutationObserver,降级到 setImmediatesetTimeout(宏任务)来异步执行 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. 从零设计一个组件,比如说弹窗组件,会考虑什么?如果需要自定义样式,可以怎么做?

设计考虑因素

  1. 功能核心 (Core Functionality):确保基本功能,如打开/关闭、显示内容、遮罩层。
  2. 用户体验 (UX & Accessibility)
    • 可访问性 (A11y) :支持键盘事件(ESC 关闭、Tab 键聚焦在弹窗内)、适当的 ARIA 属性(role="dialog", aria-modal="true")。
    • 交互:点击遮罩层是否关闭、是否支持拖动。
  3. 灵活性 (Flexibility & Configurability)
    • 内容:支持传入字符串、HTML 片段、甚至 Vue/React 组件。
    • 配置化:通过 Props 控制是否显示、标题、按钮文字、自定义回调函数等。
  4. 样式与动画 (Style & Animation)
    • 结构稳定,不会因内容多少而破裂。
    • 提供打开/关闭的动画效果。
  5. 技术实现 (Technical Implementation)
    • 挂载方式 :是否脱离当前组件层级,通常使用 appendChild 或 Vue/React 的 Portal 功能挂载到 body 下,避免父组件样式影响。
    • 控制状态 :使用 v-model/value@input/onChange 实现双向绑定。
    • 阻止滚动穿透:弹窗打开时,锁定背景页面滚动。
  6. 兼容性与稳定性:兼容不同浏览器,做好边界情况处理。

自定义样式方案

  1. Props 参数化 :通过 Props 传入自定义类名 (customClass)、内联样式 (customStyle) 或控制特定样式(如 titleColor)。

    vue 复制代码
    <template>
      <div class="my-modal" :class="customClass" :style="customStyle">
        <!-- ... -->
      </div>
    </template>
    <script>
    export default {
      props: {
        customClass: String,
        customStyle: Object
      }
    }
    </script>
  2. 插槽 (Slots) :提供具名插槽(如 header, body, footer),让用户完全自定义内部结构和样式。

    vue 复制代码
    <template>
      <div class="modal">
        <div class="modal-header">
          <slot name="header"><!-- 默认头部内容 --></slot>
        </div>
        <div class="modal-body">
          <slot name="body"><!-- 默认主体内容 --></slot>
        </div>
      </div>
    </template>
  3. CSS 变量 (CSS Custom Properties) :暴露一系列 CSS 变量,用户可以通过在外部修改这些变量来定制主题。

    css 复制代码
    /* 组件内部 */
    .my-modal {
      background-color: var(--modal-bg-color, #fff); /* 第二个值为默认值 */
      border-radius: var(--modal-border-radius, 8px);
    }
    css 复制代码
    /* 用户使用 */
    .my-wrapper {
      --modal-bg-color: #f0f0f0;
      --modal-border-radius: 4px;
    }
  4. 样式覆盖:提供结构良好、语义清晰的类名,并保持较低的 CSS 特异性,方便用户通过 CSS 选择器进行覆盖。但这种方式耦合度较高,需谨慎使用。

9. 项目中为什么要同时用Vite + Webpack

这是一个关于构建工具的问题。虽然搜索结果未直接提供答案,但基于它们的特性,可以理解这种搭配的常见原因:

  • Vite :基于原生 ES 模块,开发服务器启动速度极快 ,HMR(热更新)响应迅速,提供优秀的开发体验。它更现代、更轻量。
  • Webpack :非常成熟、稳定 ,拥有极其丰富的插件和 loader 生态,能处理各种复杂的构建需求,打包优化策略(如代码分割、摇树)经过多年考验。

共存的可能场景

  1. 渐进式迁移:旧项目使用 Webpack,部分新模块或开发流程逐步迁移到 Vite 以提升开发效率,构建生产包时仍使用经过稳定测试的 Webpack。
  2. 微前端架构:主应用和子应用可能分别采用不同的构建工具。一个应用使用 Vite,另一个老应用使用 Webpack,它们可以共存。
  3. 差异化利用:开发阶段使用 Vite 获得飞速体验,生产构建时可能使用 Webpack 以利用其更成熟的打包优化和插件生态来处理复杂项目。
  4. 特定需求:项目中某个环节可能需要依赖一个只有 Webpack 才有的特定插件。

10. 函数式编程,面向对象编程和过程式编程之间的区别是什么

这是一个编程范式的问题。搜索结果中未提供直接答案,以下是基于常见知识的解释:

编程范式 核心思想 主要特点 典型语言
面向对象编程 (OOP) 将程序视为一系列对象的交互,对象是数据和操作数据的方法的集合。 封装继承多态。强调状态和行为在对象内的绑定。 Java, C++, Python, JavaScript
函数式编程 (FP) 将计算视为数学函数的求值,避免状态改变和可变数据。 纯函数 (相同输入永远得到相同输出,无副作用)、不可变性高阶函数、函数是一等公民。 Haskell, Lisp, Scala, JavaScript
过程式编程 (Procedural) 步骤过程(函数)为中心,从上到下线性执行指令。 程序由一系列可重用的子程序(函数或过程)调用组成。关注"如何做"的步骤。 C, Pascal, BASIC

JavaScript 的特殊性 :JS 是一种多范式语言,它支持原型继承 (OOP)、函数是一等公民(FP),也可以写出过程式的代码。现代 JavaScript 开发(特别是 React 生态)越来越多地融入函数式思想,如不可变数据、纯组件。

11. 手撕代码

拍平数组并且去重和排序

javascript 复制代码
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]

括号匹配

javascript 复制代码
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。

代码实现

javascript 复制代码
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: "谁第一个完成就听谁的" 。适用于竞速超时控制场景,如为网络请求设置超时时间。

超时控制示例

javascript 复制代码
// 利用 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 缓存)

这是一个经典的前端面试题,过程可分为以下几个阶段:

  1. URL 解析:浏览器解析 URL,提取出协议、主机名、端口、路径等信息。
  2. DNS 解析(域名解析) :这是关键步骤。浏览器需要将主机名(如 www.example.com)转换为服务器的 IP 地址。DNS 缓存 就发生在这个阶段,其查找顺序为:
    • ① 浏览器缓存:浏览器会缓存之前解析过的域名。
    • ② 操作系统缓存(本机缓存) :检查本地的 Hosts 文件 (C:\Windows\System32\drivers\etc\hosts/etc/hosts) 和系统的 DNS 缓存。
    • ③ 路由器缓存:查询本地路由器的缓存。
    • ④ ISP DNS 缓存(递归查询) :向互联网服务提供商(ISP)的 DNS 服务器发起查询。这台服务器会代表你进行递归查询 ,从根域名服务器(.) -> 顶级域名服务器(如 .com) -> 权威域名服务器(如 example.com)一步步查找,并缓存结果。
  3. 建立 TCP 连接:获取到 IP 后,通过"三次握手"与服务器建立 TCP 连接。
  4. 发送 HTTP 请求:浏览器构建 HTTP 请求报文并通过 TCP 连接发送给服务器。
  5. 服务器处理请求并返回响应:服务器处理请求,生成 HTTP 响应报文发回浏览器。
  6. 浏览器渲染页面:浏览器解析 HTML、CSS,构建 DOM 树和 CSSOM 树,合并成渲染树,然后布局(Layout)和绘制(Paint),最终将页面呈现给用户。
  7. 关闭 TCP 连接:页面渲染完成后,通过"四次挥手"断开 TCP 连接。

5. 协商缓存

协商缓存是浏览器缓存策略的一种。当强缓存(通过 Cache-ControlExpires 控制)失效后,浏览器会向服务器验证本地缓存是否依然有效。这个过程就是协商缓存,旨在减少不必要的数据传输。

  • 工作原理

    1. 浏览器请求资源时,如果发现本地有该资源的缓存且强缓存已过期,它会携带缓存资源的"标识符"向服务器发起请求。
    2. 服务器根据这个标识符判断资源是否发生变化。
    3. 如果未变化 ,服务器返回 304 Not Modified 状态码,响应体为空。浏览器收到 304 后,会直接使用本地缓存。
    4. 如果已变化 ,服务器返回 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 规则中,使用 excludeinclude 字段,明确指定处理目录,避免对 node_modules 等不必要的文件进行处理。
    • 使用更快的工具
      • esbuildswc 替代 Babel 进行代码转换,它们通常比 Babel 快一个数量级。Vite 在开发环境就利用了这一点。
  • 优化输出体积(减小 Bundle Size)

    • 代码分割 (Code Splitting)
      • 使用 SplitChunksPlugin 提取公共代码和第三方库(如 react, lodash)到单独文件,避免重复打包。
      • 利用动态导入 (import()) 实现路由懒加载或组件懒加载,将代码拆分成多个 chunk,按需加载。
    • Tree Shaking
      • 移除未被使用的代码(Dead Code)。这需要采用 ES2015 模块语法(import/export),因为它的依赖关系是静态的。在 package.json 中设置 "sideEffects": false 来帮助 Webpack 安全地摇树。
    • 压缩
      • 使用 TerserWebpackPlugin 压缩和混淆 JavaScript 代码。
      • 使用 CssMinimizerWebpackPlugin 压缩 CSS。
    • 分析工具
      • 使用 webpack-bundle-analyzer 可视化分析打包结果,找出体积过大的模块,针对性优化。

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中使用includeexclude来精确指定loader的处理范围,避免对node_modules等不必要的文件进行处理。
  • 缓存 :利用Webpack 5内置的持久化缓存 (cache: { type: 'filesystem' }),或使用babel-loadercacheDirectory选项,避免重复编译。
  • 多进程/并行处理 :使用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 : 用于在函数组件中添加状态。

    javascript 复制代码
    const [count, setCount] = useState(0);
  • useEffect : 用于处理副作用操作(如数据获取、订阅、手动修改DOM等),可以模拟componentDidMount, componentDidUpdate, componentWillUnmount等生命周期。

    javascript 复制代码
    useEffect(() => {
      // 副作用逻辑
      const subscription = source.subscribe();
      // 清理函数(可选)
      return () => subscription.unsubscribe();
    }, [dependency]); // 依赖数组
  • useRef : 返回一个可变的ref对象,其.current属性被初始化为传入的参数。常用于访问DOM节点或存储任何可变值,且更改它不会引发组件重新渲染。

    javascript 复制代码
    const inputEl = useRef(null);
    useEffect(() => { inputEl.current.focus(); }, []);
  • useMemo : 用于缓存昂贵的计算结果,只有在依赖项发生变化时才会重新计算。

    javascript 复制代码
    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useContext : 接收一个context对象(React.createContext的返回值)并返回该context的当前值。

    javascript 复制代码
    const theme = useContext(ThemeContext);

Hooks为什么不能打乱顺序使用

React Hooks必须总是在React函数组件或自定义Hook的顶层调用,不能在循环、条件判断或嵌套函数中调用。这是因为:

  • React 依赖于Hooks的调用顺序来正确管理多个Hook的状态。在内部,React为每个组件维护了一个"Hook链表",每次调用Hook时,它都会移动指针指向下一个Hook。
  • 如果Hook的调用顺序在多次渲染之间发生变化(例如,某个Hook在条件语句中有时调用有时不调用),React就无法确定每次渲染时Hook状态的对应关系,从而导致状态混乱和错误。
  • 这个规则保证了React能在多次useStateuseEffect调用之间保持Hook状态的正确性。

3. Typescript的范型interface

TypeScript中的泛型(Generics)允许我们创建可重用的组件,一个组件可以支持多种类型的数据。

泛型接口(Generic Interface)是应用了泛型的接口,它增加了接口的灵活性。

typescript 复制代码
// 定义一个泛型接口
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。问最后的空间分配如何?

解答

  1. 计算总溢出空间

    子元素总基准大小 = 500px + 400px = 900px

    父容器可用空间 = 600px
    溢出空间 = 900px - 600px = 300px

  2. 计算收缩权重

    收缩权重 = 子元素基准大小 × flex-shrink值

    元素A收缩权重 = 500 × 1 = 500

    元素B收缩权重 = 400 × 2 = 800
    总收缩权重 = 500 + 800 = 1300

  3. 计算各元素需收缩的空间

    元素A需收缩空间 = (500 / 1300) × 300px ≈ 115.38px

    元素B需收缩空间 = (800 / 1300) × 300px ≈ 184.62px

  4. 计算各元素最终大小

    元素A最终大小 = 500px - 115.38px ≈ 384.62px

    元素B最终大小 = 400px - 184.62px ≈ 215.38px

延伸:假如元素分配到的收缩空间超过了元素本身大小怎么办?

  • 在CSS Flexbox规范中,元素不会收缩到小于其最小内容尺寸 (即min-content的大小,大致是元素中单词或不可断行内容的最长长度)。
  • 如果计算出的收缩尺寸小于元素的min-content大小,浏览器会以min-content作为该元素的最终尺寸。
  • 然后,剩余的溢出空间会重新分配 给其他具有flex-shrink属性且未达到最小内容限制的元素。
  • 在实际开发中,可以使用min-widthmin-height属性来显式设置Flex项目的最小尺寸,从而更好地控制收缩行为。

5. ES6模板字符串

ES6模板字符串(Template Strings/Literals)是用反引号(`````)标识的字符串,允许嵌入表达式、多行字符串和标签模板。

基本用法:

javascript 复制代码
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)

模板字符串可以紧跟在一个函数名后面,该函数(标签函数)会被调用来处理这个模板字符串。

javascript 复制代码
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标签可以获取字符串的原始版本,忽略转义字符。

javascript 复制代码
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和回溯):

javascript 复制代码
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)是关键。

jsx 复制代码
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 的整数再转为两位十六进制。

javascript 复制代码
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)。

html 复制代码
<!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):

javascript 复制代码
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:

javascript 复制代码
// 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:

javascript 复制代码
// 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 事件机制的核心概念。

  1. 事件捕获(Capturing)和事件冒泡(Bubbling)

    • 当一个事件发生在某个元素上时,它会经历三个阶段:
      • 捕获阶段 (Capture Phase) : 事件从 window 向下传播到目标元素的路径。
      • 目标阶段 (Target Phase): 事件到达目标元素。
      • 冒泡阶段 (Bubble Phase) : 事件从目标元素向上冒泡回 window
    • 你可以通过 addEventListener 的第三个参数(useCapture)来选择在哪个阶段监听事件:true(捕获阶段)或 false(默认,冒泡阶段)。
  2. 事件对象 (Event Object)

    • 当事件发生时,浏览器会创建一个事件对象,它包含了事件的相关信息(如类型、目标、坐标、按键等)。这个对象会作为参数传递给事件处理函数。
    javascript 复制代码
    element.addEventListener('click', function(event) { // `event` 就是事件对象
      console.log(event.type); // 'click'
      console.log(event.target); // 实际触发事件的元素
    });
  3. 事件代理(委托)(Event Delegation)

    • 原理 :利用事件冒泡机制,将事件监听器绑定在父元素 上,通过事件对象的 event.target 属性来识别真正触发事件的子元素,然后执行相应的操作。

    • 优点

      • 减少内存消耗:只需要一个事件监听器处理多个子元素的事件。
      • 动态添加的子元素无需再单独绑定事件监听器。
    • 示例

      javascript 复制代码
      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到页面显示发生了什么

这个过程是前端性能优化和排查问题的基础,可分为以下几个阶段:

  1. URL解析 :浏览器解析URL,提取出协议主机名端口路径查询参数等信息。
  2. DNS解析 :浏览器将主机名转换为IP地址。查找顺序为:
    • 浏览器缓存 → 操作系统缓存(如本地hosts文件) → 路由器缓存 → ISP的DNS服务器 → 若未找到,则进行递归查询(根域名服务器→顶级域名服务器→权威域名服务器)。
  3. 建立TCP连接 :浏览器通过三次握手(SYN, SYN-ACK, ACK)与服务器建立TCP连接,确保可靠的数据传输。若是HTTPS,还会进行TLS握手,交换密钥,建立安全加密通道。
  4. 发送HTTP请求:浏览器构建HTTP请求报文(包含请求行、请求头、请求体),并通过TCP连接发送给服务器。
  5. 服务器处理请求并返回响应:服务器处理请求(可能涉及应用逻辑、数据库查询),然后返回HTTP响应(状态码、响应头、响应体)。
  6. 浏览器解析和渲染
    • 解析HTML :构建DOM树(文档对象模型)。
    • 解析CSS :构建CSSOM树(CSS对象模型)。
    • 合并成渲染树 :将DOM和CSSOM合并为渲染树(Render Tree),排除不可见元素。
    • 布局(Layout):计算渲染树中每个元素在视口内的确切位置和大小(重排)。
    • 绘制(Paint):将布局后的节点转换为屏幕上的实际像素(重绘)。
    • 合成(Composite):将各层绘制结果合成为最终页面(如有分层)。
  7. 加载和执行JS :HTML解析过程中遇到JS会暂停DOM构建(除非脚本是asyncdefer),执行完毕后继续。
  8. 连接结束:页面渲染完成后,可能保持TCP连接以供复用,或通过四次挥手关闭连接。

2. 缓存机制(强制、协商)

浏览器缓存可显著提升加载速度,减轻服务器压力,主要分为强缓存协商缓存

特性 强缓存 (Strong Caching) 协商缓存 (Negotiated Caching)
核心思想 直接从本地读取资源,不向服务器发送请求 向服务器询问资源是否过期,由服务器决定是否使用缓存
触发时机 缓存未过期时 强缓存失效后
HTTP状态码 200 (from disk cache)200 (from memory cache) 304 (Not Modified)
是否与服务器交互
相关HTTP头 Cache-ControlExpires Last-Modified / If-Modified-SinceETag / If-None-Match

工作流程

  1. 浏览器请求资源时,先检查强缓存 。若Cache-Controlmax-ageExpires设置的过期时间未到,则直接使用缓存。
  2. 若强缓存失效,浏览器会携带缓存标识 (如之前的Last-ModifiedETag值)向服务器发起请求,进行协商缓存
  3. 服务器验证缓存标识(对比资源最后的修改时间或唯一标识)。若资源未改变 ,返回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. 闭包怎么理解?

闭包 是指一个函数能够记住并访问其所在的词法作用域,即使该函数是在当前词法作用域之外执行。

形成条件

  1. 函数嵌套。
  2. 内部函数引用了外部函数的变量或参数。
  3. 内部函数被外部函数返回或在外部作用域被持有引用。

简单示例

javascript 复制代码
function outer() {
  let count = 0; // outer 函数的局部变量
  function inner() { // inner 函数,一个闭包
    count++; // 引用了外部函数的变量 count
    console.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),如果滥用或使用不当,可能导致:

  • 内存泄漏 :大量变量被闭包引用,无法释放,内存占用不断升高,可能导致页面卡顿甚至崩溃。
    • 解决方法 :在不需要闭包时,及时将引用它的变量置为nullclosureFn = null),打破引用,以便GC回收。

5. 手撕:防抖

防抖的核心是:事件触发后,等待一段时间再执行函数 。若在等待期内事件再次被触发,则重新计时,直到等待期内无新触发,才执行函数。常用于搜索框输入、窗口resize等。

基础实现

javascript 复制代码
function debounce(func, wait) {
  let timeoutId; // 使用闭包保存定时器ID
  return 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。若不使用applyfunc中的this会指向全局对象(非严格模式)或undefined(严格模式),而非事件触发者。
  • 传递正确参数 :事件处理函数(如event)或其他参数需要通过args传递给原函数。
  • apply方法能显式设置函数执行时的this值并以数组形式传递参数。

带立即执行选项的防抖

javascript 复制代码
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的三种模式

  1. Hash模式 :使用URL的hash#后面的部分)。变化不会导致浏览器向服务器发送请求,通过监听hashchange事件响应变化。兼容性最好
  2. History模式 :利用HTML5 History API(pushState, replaceState)操作浏览器会话历史记录,生成美观的无#URL。但需服务器配置支持,避免直接访问子路由返回404。
  3. Abstract模式 :主要用于非浏览器环境(如Node.js、SSR、移动端Native),其路由历史记录在内存中维护。

自己实现一个简单的Router(Hash模式)思路

  1. 定义一个Router

    javascript 复制代码
    class MyRouter {
      constructor(options) {
        this.routes = {}; // 存储路由路径和对应的组件/回调
        this.currentUrl = ''; // 当前URL
        options.routes.forEach(route => {
          this.routes[route.path] = route.component;
        });
        this.init(); // 初始化
      }
    }
  2. 初始化及监听hash变化

    javascript 复制代码
    init() {
      window.addEventListener('load', () => this.updateView(), false);
      window.addEventListener('hashchange', () => this.updateView(), false);
    }
  3. 更新视图

    javascript 复制代码
    updateView() {
      this.currentUrl = window.location.hash.slice(1) || '/'; // 获取#后的路径
      const component = this.routes[this.currentUrl];
      if (component) {
        // 简单起见,假设component是渲染函数或组件模板
        document.getElementById('app').innerHTML = component;
      }
    }
  4. 编程式导航

    javascript 复制代码
    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劫持数据属性的gettersetter。需要递归遍历数据对象,且对数组方法需特殊处理。无法检测属性的添加和删除。
  • 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.10.2这样的十进制小数在转换为二进制浮点数时是无限循环 的,就像1/3在十进制中是0.333...一样。由于存储空间有限,计算时就会发生精度丢失。

解决方法

  1. 显示时格式化(0.1 + 0.2).toFixed(1) // "0.3"(注意结果是字符串)。
  2. 转换为整数计算后再转回(0.1 * 10 + 0.2 * 10) / 10 === 0.3
  3. 使用第三方数学库(如decimal.js)处理精确计算。

11. 如何跟多个后端对接?如果后端接口格式不一致(后端有问题)你要怎么办?

协作流程

  1. 前期沟通:参与接口评审,明确数据格式、错误码规范、联调时间等。
  2. 文档管理:使用Swagger/OpenAPI等工具维护接口文档,确保双方理解一致。
  3. Mock数据:前期根据文档Mock数据,并行开发,不阻塞进度。
  4. 统一请求层封装:在项目中统一封装HTTP请求工具(如基于axios),处理公共逻辑(如基地址、超时、认证、错误处理)。

接口格式不一致问题

  1. 主动沟通:首先与相关后端开发者沟通,说明不一致性带来的问题(如前端难以统一处理),推动对方遵循约定规范。

  2. 适配器模式 :如果短期内无法统一,在前端设计适配层 (Adapter)。为每个不一致的接口编写特定的数据转换函数,将不同格式转换为前端统一的格式。

    javascript 复制代码
    // 适配器示例
    const adapterForBackendA = (data) => {
      return {
        id: data.user_id,
        name: data.user_name
        // ... 将后端A的数据结构转换为前端通用结构
      };
    };
    const adapterForBackendB = (data) => {
      return {
        id: data.id,
        name: data.name
        // ... 后端B的数据结构可能更接近通用结构,转换较少
      };
    };
  3. 向上反馈:若问题普遍且沟通无效,及时向项目经理或技术负责人反馈,从更高层面推动规范制定和执行。

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调度器

实现思路

  1. 维护一个等待队列(存储待执行的异步任务函数)。
  2. 维护一个当前并发数计数器。
  3. 提供一个方法,用于添加任务。添加时,若当前并发数未达上限(3),则立即执行;否则放入队列等待。
  4. 任务执行完毕后,并发数减1,并检查队列中是否有等待任务,有则取出执行。

代码实现

javascript 复制代码
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)。

思路 :使用滑动窗口(双指针)。

  1. 初始化两个指针start(当前递增序列的开始)和end(当前遍历位置),以及变量maxStartmaxLength记录最长序列的起始索引和长度。
  2. 遍历字符串(从第2个字符开始):
    • 如果当前字符比前一个字符的ASCII码大(即递增),则继续扩展窗口(end++)。
    • 否则,说明递增序列中断。计算当前序列长度end - start,若大于maxLength则更新maxStartmaxLength。然后将start移动到end处,开始新的序列。
  3. 遍历结束后,再最后计算一次当前序列长度并更新结果。
  4. 根据maxStartmaxLength返回子串。

代码实现

javascript 复制代码
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-changetransform: 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 时间消耗分布。其核心特征如下:

特征 说明 分析价值
宽度 表示函数执行时间占比 识别性能瓶颈
高度 表示调用栈深度 理解调用关系
颜色 通常表示不同模块或库 区分系统/用户空间
排序 按字母顺序或采样计数 便于查找特定函数

生成火焰图的基本步骤

  1. 数据采集 :使用 perf record 等工具收集 CPU 调用栈信息。

    bash 复制代码
    perf record -F 99 -g -p <pid> -- sleep 30
  2. 处理数据 :将采集的数据转换为适合生成的格式。

    bash 复制代码
    perf script > out.perf
  3. 生成SVG :使用 FlameGraph 工具链生成交互式火焰图。

    bash 复制代码
    ./stackcollapse-perf.pl out.perf > out.folded
    ./flamegraph.pl out.folded > flamegraph.svg

如何分析火焰图

  • 查找最宽的平顶山:这些是消耗 CPU 时间最多的函数,是优化的首要目标。
  • 关注调用栈深度:过深的调用链可能意味着存在过度封装或复杂的逻辑。
  • 识别常见模式
    • 宽平顶:高耗时函数,计算密集型热点。
    • 长调用链:过度封装。
    • 重复锯齿:低效循环。

📊 Performance API 详解

Performance API 是浏览器提供的一组用于测量和监控网页性能的接口。它提供了丰富的性能数据,如页面加载时间、资源加载性能、用户交互延迟等。

常见使用场景

  • 测量页面加载时间

    javascript 复制代码
    const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
    console.log(`页面加载时间: ${loadTime}ms`);
  • 获取资源加载性能

    javascript 复制代码
    performance.getEntriesByType('resource').forEach(resource => {
      console.log(`${resource.name} 的加载时间: ${resource.duration}ms`);
    });
  • 测量代码执行时间

    javascript 复制代码
    const start = performance.now();
    // 执行某些操作
    const end = performance.now();
    console.log(`耗时: ${end - start}ms`);
  • 监控用户交互延迟 (如点击延迟):

    javascript 复制代码
    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 CLIVite(尤雨溪同时是 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 状态,并提供了缓存、轮询、防抖、节流、依赖刷新、屏幕聚焦重新请求等强大功能,极大简化了数据请求的逻辑。

    javascript 复制代码
    const { data, error, loading } = useRequest(getUserInfo, {
      onSuccess: (data) => { console.log(data); },
      refreshOnWindowFocus: true, // 屏幕聚焦重新请求
    });
  • useAntdTable :与 Ant Design 表格组件深度结合,专为处理表格数据请求和分页而设计,自动管理分页参数、筛选条件,并与 useRequest 无缝集成。

  • useSize :用于监听 DOM 节点的尺寸变化,获取其宽高信息。基于 ResizeObserver API 实现。

    javascript 复制代码
    const size = useSize(document.getElementById('container'));
    console.log(size); // { width: 100, height: 50 }
  • useDebounce / useThrottle:对值或函数进行防抖或节流处理,常用于处理频繁触发的事件,如搜索输入、窗口滚动等。

  • useLocalStorageState / useSessionStorageState :方便地在组件状态和 localStoragesessionStorage 之间同步数据。

5. React 18 新特性

React 18 是一个主要版本,引入了许多重要特性,专注于并发特性和性能提升

  • 并发特性 (Concurrent Features) :React 18 的核心是引入了并发渲染器,它允许 React 在渲染过程中进行中断、暂停和恢复,以便浏览器能够优先处理用户交互等高优先级任务,从而提升应用的响应速度和用户体验。
  • 自动批处理 (Automatic Batching) :React 18 通过在更多场景(如 setTimeoutPromise 等)中自动将多个状态更新合并为一次重新渲染,减少了不必要的渲染次数,提升了性能。
  • Transitions :提供了 useTransitionstartTransition 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 监控,持续跟踪核心指标。效果:能持续发现并定位性能瓶颈,针对性进行优化。

优化效果衡量 :优化前后应使用 LighthouseWebPageTest 等工具进行量化对比,重点关注 Core Web Vitals(LCP, FID, CLS)等指标的变化。


相关推荐
木心操作2 小时前
js生成excel表格进阶版
开发语言·javascript·ecmascript
谢尔登2 小时前
【Webpack】模块联邦
前端·webpack·node.js
前端码虫2 小时前
2.9Vue创建项目(组件)的补充
javascript·vue.js·学习
Bottle4142 小时前
深入探究 React Fiber(译文)
前端
夜宵饽饽2 小时前
上下文工程实践 - 工具管理(上篇)
javascript·后端
汤姆Tom2 小时前
JavaScript Proxy 对象详解与应用
前端·javascript
BillKu2 小时前
Vue3中app.mount(“#app“)应用挂载原理解析
javascript·vue.js·css3
xiaopengbc2 小时前
在 React 中如何使用 useMemo 和 useCallback 优化性能?
前端·javascript·react.js