Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/面试/八股/mos

2进阶篇

66 分钟阅读 · Note

目录树 578 篇

            • 1基础篇
            • 2进阶篇
            • 4手写篇
            • 5AI面试
            • 888
            • 进阶篇题目列表
            • cache.manifest 文件内容
          • 进阶篇题目列表
          • DOM&浏览器 API
          • HTTP&网络
          • TS
          • Vue
        • 可投递企业
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗1基础篇同一路径↗4手写篇同一路径↗5AI面试同一路径↗888同一路径↗进阶篇题目列表同一路径↗cache.manifest 文件内容同一路径

进阶篇

HTML

  • Q: 如何理解HTML语义化 A: HTML语义化是指使用恰当标签表达内容含义而非仅控制样式。核心价值:1)提升可访问性,屏幕阅读器可准确解析内容结构;2)改善SEO,搜索引擎抓取时能识别标题层级、导航、主要内容等关键区域;3)代码可维护性更高,结构清晰降低团队协作成本。常用语义标签包括 <header>、<nav>、<main>、<section>、<article>、<aside>、<footer>、<figure> 等。注意 <div> 和 <span> 是无语义标签,仅应作为样式容器使用。HTML5 新增的语义标签本质上是块级元素,可通过 CSS 重置兼容旧浏览器。实践中应遵循"先语义后样式"原则,避免用 <div> 堆砌后全靠 class 命名来"假装语义化"。

  • Q: H5的新特性有哪些 A: HTML5 核心新增特性:1)语义标签:<header>、<nav>、<main>、<article>、<section>、<aside>、<footer>、<figure>、<figcaption> 等;2)多媒体:<audio> 和 <video> 原生支持音视频播放,无需 Flash;3)Canvas 和 SVG:<canvas> 提供 2D 绘图 API,适合像素级操作;SVG 适合矢量图形;4)表单增强:新增 input type(email、url、number、range、date、color 等)和属性(placeholder、required、pattern、autofocus);5)本地存储:localStorage(持久化)、sessionStorage(会话级)、IndexedDB(结构化大数据);6)WebSocket:全双工通信协议;7)Web Workers:后台线程执行 JavaScript;8)Geolocation:地理位置 API;9)History API:pushState/replaceState 实现 SPA 路由;10)拖放 API(Drag and Drop);11)requestAnimationFrame:更高效的动画方案;12)新的通信接口:postMessage、Server-Sent Events。H5 的理念是"丰富语义、强化 Web 应用能力"。

  • Q: 说一下 HTML5 drag api A: HTML5 拖放 API 允许页面元素和外部文件通过拖拽交互。核心机制:被拖拽元素设置 draggable="true",通过事件监听控制拖放行为。关键事件分两类——拖拽源元素事件:dragstart(设置拖拽数据)、drag(拖拽中持续触发)、dragend(拖拽结束)。放置目标元素事件:dragenter(拖入目标)、dragover(需阻止默认行为才能成为有效放置目标)、dragleave(离开目标)、drop(释放鼠标,实际接收数据的地方)。数据传输通过 event.dataTransfer 对象:setData(format, data) 在 dragstart 中设置数据,getData(format) 在 drop 中读取数据。支持 setDragImage() 自定义拖拽预览图,effectAllowed/dropEffect 控制光标样式。文件拖拽可将桌面文件拖入浏览器,通过 dataTransfer.files 获取 File 对象上传。需要注意:移动端兼容性有限,通常需结合 touch 事件实现。

  • Q: iframe有那些缺点 A: iframe 的缺点:1)性能问题:每个 iframe 都创建独立浏览器上下文,加载资源、构建 DOM 树均独立,显著增加内存和网络开销;2)SEO 不友好:搜索引擎爬虫对 iframe 内容索引不完整;3)阻塞主页面加载:<iframe> 的 onload 事件会延迟父页面 window.onload;4)通信复杂:跨域场景需 postMessage,同域可直接访问但仍有上下文切换成本;5)安全风险:嵌入第三方页面可能引入 XSS 或点击劫持,需配合 sandbox 属性限制权限;6)可访问性差:屏幕阅读器难以识别 iframe 内容层级;7)响应式困难:iframe 内页面按独立视口渲染,缩放适配需额外计算。实际场景中除非必须嵌入第三方内容(如支付、地图、视频),否则优先考虑 Ajax 加载或 Web Component 替代方案。

  • Q: 简述一下src与href的区别 A: src(source)和 href(hypertext reference)虽然都在 HTML 中引用外部资源,但语义和浏览器行为不同。src 表示"嵌入替代资源",浏览器遇到 <script src="...">、<img src="...">、<iframe src="..."> 时会暂停当前文档的解析/渲染,发送请求获取资源并执行/渲染后再继续。src 资源是文档的组成部分,会阻塞关键渲染路径。href 表示"建立关联关系",浏览器遇到 <link href="...">、<a href="..."> 时不会阻塞解析,只是标记文档与资源间的关联。href 是"引用"而非"嵌入",浏览器会在后台并行下载(如 CSS),且下载完成后不会阻塞解析。关键区别:src 是替换性嵌入(文档依赖资源),href 是关联性引用(文档指向资源)。这也是为什么 <link> 使用 href 而 <script> 使用 src。

  • Q: 知道的网页制作会用到的图片格式有哪些 A: Web 常用图片格式及特性:1)JPEG:有损压缩,不支持透明,24 位色,适合照片类复杂色彩图像,体积小但放大有锯齿;2)PNG:无损压缩,支持透明(PNG-8 索引透明/PNG-24 Alpha 透明),适合图标、LOGO、截图等边缘清晰的图像;3)GIF:支持动图,仅 256 色,适合简单动画和表情包;4)WebP:Google 推出,同时支持有损/无损/透明度/动图,同等质量体积比 JPEG 小 25-35%,浏览器兼容性已较好;5)SVG:矢量格式,基于 XML,无损缩放,适合图标、图形、LOGO,支持 CSS 和 JS 控制;6)AVIF:基于 AV1 视频编码,压缩率优于 WebP 约 20%,支持 HDR 和透明度,兼容性仍在提升;7)Base64(data URL):将图片编码为字符串内嵌在 CSS/HTML 中,减少 HTTP 请求,适合小图标(通常 < 10KB),但体积膨胀约 33%。选型原则:照片用 JPEG/WebP,图标用 SVG/PNG,动图用 GIF/WebP。

  • Q: script标签中defer和async的区别 A: defer 和 async 都用于控制外部脚本(带 src 属性)的加载和执行时机,核心区别在于执行顺序和 DOM 关联。默认情况下脚本加载和执行会阻塞 HTML 解析。defer:加载与 HTML 解析并行进行,但延迟到文档解析完(DOMContentLoaded 之前)按文档顺序依次执行。多个 defer 脚本保持加载顺序。适合需要操作 DOM 或依赖其他脚本的场景(如分析工具、DOM 操作脚本)。async:加载与 HTML 解析并行进行,但下载完成后立即执行(此时会阻塞解析),不保证执行顺序(谁先下载完谁先执行)。多个 async 脚本间无依赖顺序。适合独立无依赖的脚本(如广告、埋点)。关键点:defer 是"延迟到解析完有序执行",async 是"下载完立即无序执行"。没有这两个属性时,脚本是同步加载并阻塞解析。动态创建 &lt;script&gt; 并添加到 DOM 默认是 async 行为。

  • Q: 说一下 web worker A: Web Worker 允许在浏览器后台线程中运行 JavaScript,实现真正的并行计算,不阻塞 UI 线程。主线程通过 new Worker('script.js') 创建 worker 实例,通过 postMessage() 发送数据,通过 onmessage 接收结果。Worker 内无法访问 DOM、window、document、parent 等,但可使用 XMLHttpRequest、fetch、setTimeout、IndexedDB、WebSocket、importScripts()(加载额外脚本)。数据传输默认采用"结构化克隆算法"(structured clone),拷贝完整数据;对于巨大数据可使用 Transferable Objects(如 ArrayBuffer)实现零拷贝传输。专用 Worker(Dedicated Worker)仅被创建它的页面使用。Shared Worker 可被同源多页面共享。Service Worker 是特殊 Worker,充当网络代理,拦截 fetch 请求实现离线缓存和 PUSH 通知。使用场景:大计算(数据处理、加密、图像处理)、实时轮询、游戏物理计算等。注意 Worker 内无法使用 ES Module(需指定 {type: "module"})且存在浏览器兼容和调试困难。

  • Q: 用一个div模拟textarea的实现 A: 核心思路是使 div 成为可编辑区域:设置 contentEditable="true" 属性。此时 div 可像 textarea 一样输入文本,且高度会随内容自动增长(无需手动调整)。实现要点:1)样式上设置 user-select: text; white-space: pre-wrap; word-wrap: break-word; 保持文本换行和选择;2)默认占位符通过 CSS :empty::before 伪元素实现 placeholder 效果;3)如果需要模拟 textarea 的 value 属性,用 innerText 获取纯文本内容(而非 innerHTML,避免 HTML 标签);4)监听 input 事件实时获取内容;5)限制输入长度可用 maxlength 属性(部分浏览器支持 contentEditable 的 maxlength),或在 input 事件中 preventDefault + 截断处理;6)自动获得焦点后光标定位用 focus() + Range/Selection API。缺点:相比于原生 textarea,contentEditable div 在拼写检查、IME(输入法)兼容性、默认键盘行为(Tab、Enter)上存在细微差异,需额外处理。现代 vue/react 生态中建议优先用 textarea + CSS 实现自动高度。

  • Q: 介绍下资源预加载 prefetch/preload A: preload 和 prefetch 通过 &lt;link rel="preload/prefetch"&gt; 告知浏览器提前加载资源,是浏览器加载优化的重要手段。preload:告诉浏览器"当前页面肯定会用到这个资源,请提前加载",优先级较高。浏览器发现 preload 标签后会以高优先级立即请求资源,不会阻塞渲染(并行下载)。常用于提前加载关键 CSS、字体文件、首屏图片或 JS 模块。示例:&lt;link rel="preload" href="font.woff2" as="font" crossorigin&gt;,其中 as 属性必须指定(style/script/font/image/fetch 等),否则浏览器不执行预加载。prefetch:告诉浏览器"下一个页面可能用到这个资源,请空闲时加载",优先级极低,仅在浏览器空闲时(requestIdleCallback)发起。适合预加载下一页的资源。&lt;link rel="prefetch" href="next-page.js"&gt;。preconnect:提前建立连接(DNS+TCP+TLS),适合跨域关键资源,如 &lt;link rel="preconnect" href="https://api.example.com"&gt;。dns-prefetch:仅预解析 DNS,比 preconnect 开销更小。注意:preload 不当会浪费带宽,prefetch 的资源在下一个导航才会用到。

  • Q: 介绍下 viewport A: viewport 是浏览器用来渲染网页的可视区域。移动端浏览器默认使用"布局视口"(通常 980px)渲染页面,然后缩放适配屏幕,导致非响应式页面被缩小后文字过小。标准解决方案:&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;。核心参数:width=device-width 将布局视口宽度设为设备宽度(理想视口);initial-scale=1.0 初始缩放比例为 1;minimum-scale/maximum-scale 控制缩放范围;user-scalable=no 禁止缩放(不推荐,影响可访问性)。视觉视口(visual viewport)是用户当前看到的区域,可通过 window.visualViewport API 获取。布局视口(layout viewport)是 CSS 百分比布局的参照。移动端适配还需结合 CSS 媒体查询、rem/vw 单位灵活响应。关于 1px 物理像素问题,可结合 devicePixelRatio 和 initial-scale=1.0/dpr 的 meta 方案处理。

  • Q: target="_blank"有哪些问题? A: target="_blank" 的主要问题:1)安全漏洞——新打开的页面通过 window.opener 对象可以访问原页面的 window 对象(同源时),存在 tab-napping 攻击风险,恶意页面可通过 window.opener.location = '钓鱼链接' 重定向原页面。解决方案:加上 rel="noopener noreferrer"。noopener 阻止新页面访问 opener,noreferrer 同时隐藏 Referer 头。现代浏览器(Chromium 88+、Firefox 79+、Safari 15+)默认对 &lt;a target="_blank"&gt; 设置 rel="noopener",但仍建议显式声明。2)性能问题:打开新标签页会占用额外内存和进程。3)用户体验:未经用户同意打开新窗口可能造成困扰。4)无法通过 window.opener 通信(加了 noopener 后),若需双向通信改用 window.open() + postMessage 或直接同页跳转。最佳实践:所有外链 target="_blank" 都应添加 rel="noopener noreferrer";对重要操作慎用新窗口打开。

CSS

  • Q: 如何解决a标点击后hover事件失效的问题? A: 根本原因是 CSS 伪类优先级:LVHA 规则——:link -> :visited -> :hover -> :active。当点击 &lt;a&gt; 后该链接变为已访问状态,:visited 的样式覆盖了 :hover,导致 hover 状态不生效。解决方式:1)按 LVHA 顺序声明——a:link {} a:visited {} a:hover {} a:active {},这是最规范的解决方式;2)使用 :any-link 选择器(CSS Selectors Level 4)统一匹配所有链接(包括已访问),再配合 :hover;3)直接用 class 替代伪类,如 .nav-link:hover,避免伪类优先级冲突;4)对于导航类链接,无需区分 visited 状态,可统一用 a { color: ... } 覆盖默认样式后单独定义 :hover 和 :active。更深层思考:a:visited 的样式限制(只有 color、background-color、border-color 等少数属性可修改)是浏览器隐私保护措施,因此某些效果(如 display/transform)在 visited 上无效,用 class 方案更灵活。Safari 还对 visited 样式有额外限制。

JavaScript

  • Q: 原型链 A: JavaScript 通过原型链实现继承。每个对象都有一个内部 Prototype 属性(可通过 __proto__ 或 Object.getPrototypeOf() 访问),指向其构造函数的 prototype 对象。当访问对象的属性或方法时,JS 引擎先检查对象自身属性(own property),找不到则沿 Prototype 链向上查找,直到 Object.prototype(其 Prototype 为 null)。核心关系:function Foo() {} -> Foo.prototype.constructor === Foo、new Foo().__proto__ === Foo.prototype、Foo.prototype.__proto__ === Object.prototype。所有函数都是 Function 的实例:Foo.__proto__ === Function.prototype、Function.__proto__ === Function.prototype(Function 自举)。instanceof 操作符通过检查 constructor.prototype 是否在实例的 Prototype 链上来判定。ES6 class 本质上是原型链的语法糖。原型链的问题:1)引用类型属性被所有实例共享;2)无法向父构造函数传参;3)在查找链上修改原型可能导致意外变更。

  • Q: 闭包 A: 闭包是指一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。JavaScript 每个函数都有一个 Environment 内部槽,记录函数创建时的词法环境。即使外层函数执行完毕返回,内层函数仍保持对外层变量对象的引用,外层变量不会被垃圾回收。典型场景:1)封装私有变量——函数工厂返回含有私有状态的方法;2)模块模式——IIFE 实现命名空间隔离;3)回调函数中保存上下文——循环中通过闭包捕获索引值(或直接用 let 块级作用域);4)高阶函数——偏函数、柯里化、once、debounce 等。闭包的内存影响:被引用的外层变量不会释放,过度使用可能导致内存泄漏。面试中常考题目:for 循环内 setTimeout 打印 i 的问题——var 没有块级作用域,所有 setTimeout 共享同一 i,闭包或 let 解决。ES6 之后 let/const 的块级作用域大幅减少了闭包使用场景,但闭包仍是 JS 最核心的机制之一。

  • Q: Event Loop(浏览器与 Node 区别) A: Event Loop 是 JS 异步执行的机制。浏览器 Event Loop:每次循环先执行同步代码(一个宏任务),然后清空微任务队列(Promise.then/catch/finally、MutationObserver、queueMicrotask),然后执行 requestAnimationFrame(若到了渲染时机),然后渲染页面(样式计算->布局->绘制),最后检查 requestIdleCallback。宏任务来源:script 整体代码、setTimeout/setInterval、I/O、UI 交互事件、setImmediate(仅 IE/Node)。Node.js Event Loop(libuv 实现)分为六个阶段:timers(执行到期的 setTimeout/setInterval)、pending callbacks(执行系统操作回调)、idle/prepare(内部使用)、poll(轮询 I/O 事件、处理回调)、check(执行 setImmediate 回调)、close callbacks(关闭事件回调)。每个阶段结束后都会清空微任务队列。关键区别:1)浏览器渲染机制与微任务队列紧密相关,Node 无渲染;2)Node 有 setImmediate 和 process.nextTick(优先级高于微任务),浏览器无;3)Node 中微任务在每个阶段间清空,而非同步代码后立即清空;4)Node 的 setTimeout(fn, 0) 和 setImmediate 执行顺序在同一个 tick 中不确定。Node 11+ 后 setTimeout 等回调内微任务行为与浏览器趋于一致(每个回调后清空微任务)。

  • Q: 垃圾回收机制 A: JavaScript 的垃圾回收(GC)是自动的,最主流算法是"标记-清除"(Mark-and-Sweep),V8 引擎还结合了分代回收。基本原理:GC 从根对象(全局对象、当前执行上下文、栈中变量等)出发,标记所有可到达的对象,然后清除未被标记的对象。V8 将内存分为新生代(Young Generation)和老生代(Old Generation)。新生代采用 Scavenge 算法(Cheney 算法),将空间分为 From 和 To 两个半区,活动对象从 From 复制到 To,存活两次以上的对象晋升到老生代。老生代采用"标记-清除-整理"(Mark-Sweep-Compact):标记阶段暂停所有 JS 执行(Stop The World)遍历对象图标记活动对象,清除阶段回收非标记对象的内存,整理阶段将活动对象向一端移动避免内存碎片。V8 的并发标记(Concurrent Marking)和增量标记(Incremental Marking)允许 GC 与 JS 执行交替进行,减少主线程暂停时间。此外还有写屏障(Write Barrier)机制跟踪老生代到新生代的引用,优化跨代引用。引用计数(Reference Counting)是另一种思路,但有循环引用问题,现代引擎已不再使用(IE6/7 使用过)。

  • Q: 内存泄漏常见场景 A: 前端常见内存泄漏场景:1)全局变量——未声明的变量自动成为全局变量(严格模式可避免),或挂载到 window 的引用未被清除;2)闭包——外层函数变量被内层函数持续引用,即使外层已执行完毕;3)定时器和回调——setInterval/setTimeout 未取消,或事件监听未移除(removeEventListener),DOM 元素已移除但监听器仍持有引用;4)DOM 引用——JS 变量缓存了 DOM 元素引用,DOM 被移除后 JS 引用未置 null,GC 无法回收;5)脱离 DOM 树的节点——用 removeChild 移除元素但 JS 变量仍指向它;6)闭包中的 DOM 引用——闭包持有 DOM 元素引用导致整颗子树无法回收;7)Map/Set 中的引用——使用普通 Map/Set 存储 DOM 引用,元素移除后键仍存在;应使用 WeakMap/WeakSet;8)WebSocket —— 未关闭的 WebSocket 连接保持引用;9)第三方库/组件销毁不彻底——单页应用中组件卸载时未清理副作用。检测工具:Chrome DevTools Memory 面板、heap snapshot、Performance 录制分析 JS heap 增长趋势。

  • Q: 内存泄漏检测与场景 A: 检测方法:1)Chrome DevTools Performance 面板——录制时间线,观察 JS Heap 曲线的上升趋势,若 GC 后曲线不回落则疑似泄漏;2)Memory 面板 Heap Snapshot——拍摄多次快照,对比 Comparison 视图查看哪些对象数量增长未释放。重点关注 Detached DOM Tree(游离 DOM 节点),展开查看 retainers 链定位引用源;3)Allocation Instrumentation on Timeline——记录内存分配时间线,观察函数调用导致的内存持续增长;4)performance.memory 检查 JS heap 使用量。常见泄漏场景验证方法:重复执行某个操作(打开/关闭弹窗、路由切换),执行后手动触发 GC(Memory 面板的 Collect Garbage 按钮),对比前后快照,对象数量应恢复。如果 Detached DOM 或函数作用域(closure)持续增长,说明有引用未释放。工具链上也可使用 why-did-you-update(React)监控组件重渲染,memo-leak-check 等第三方库辅助检测。修复后通过多次快照对比验证修复效果。

  • Q: Promise 可以取消吗 A: 原生 Promise 本身不支持取消(设计目标是不可变状态:pending -> fulfilled/rejected,不可逆)。但可通过多种方式实现"取消"语义:1)AbortController——现代浏览器支持的取消原语,与 fetch 结合使用。const controller = new AbortController(); fetch(url, { signal: controller.signal }); controller.abort();。Promise 也可以传递 AbortSignal 手动检查 signal.aborted 状态;2)Promise.race + 取消令牌——用 race 竞速原始 Promise 和"取消 Promise",取消时 reject 特殊错误;3)包装 Promise 使其变成可取消——在 resolve/reject 前检查标记,如 promise.cancel = () =&gt; { cancelled = true; } 并在 then 回调中判断;4)Bluebird 库原生支持 .cancel()——但已被废弃;5)使用 Observable(RxJS)——支持取消订阅,更适合流式场景。生产建议:如果需要真正的中断网络请求,用 AbortController;如果是 UI 层面的"取消"(如用户关闭弹窗时忽略之前的请求结果),用标志变量 + 忽略 resolved 结果即可,不必真正取消 Promise。

  • Q: Async 原理 A: async/await 是 Generator + Promise 的语法糖,本质是利用生成器的暂停和恢复能力配合 Promise 实现同步风格异步代码。执行原理:async 函数返回一个 Promise。函数内部遇到 await 时,JS 引擎暂停函数执行,将 await 右侧表达式包装为 Promise(若不是 Promise 则用 Promise.resolve() 包装),然后将后续代码注册为该 Promise 的 .then() 回调。函数在 Promise settled 后恢复执行——若 resolve 则以 resolved value 赋值 await 表达式结果继续执行;若 reject 则在当前函数内抛出异常,可用 try/catch 捕获。底层是 V8 引擎对 Generator 的增强,类似 function*() { yield promise; } 配合自执行器(Thunk/co 库的机制)。async/await vs 原生 Promise:可读性更好(线性代码),错误处理更自然(try/catch 替代 .catch 链),但 await 在循环(forEach/map)中会串行执行导致性能下降,需注意用 Promise.all 并行优化。在微任务执行顺序上,await 后的代码属于微任务,和 .then() 的回调在同一个微任务队列中。

DOM / 事件

  • Q: 如何实现浏览器内多个标签页之间的通信 A: 同源多标签页通信有以下方案:1)BroadcastChannel——专用 API,支持同源多页面双向通信,new BroadcastChannel('channel-name'),通过 postMessage 发送、onmessage 接收。接口简洁,适合同源场景;2)localStorage ——利用 storage 事件(同源的其他标签页修改 localStorage 时触发),但仅在同源且非当前修改页触发;3)SharedWorker——多页面共享同一个 Worker 线程,通过 postMessage/onconnect 通信,需要 Worker 文件支持;4)postMessage + window.open——父页面 window.open 打开子页面,通过引用互相发消息;5)IndexedDB —— 通过轮询或监听 versionchange 事件感知其他标签页的数据变更;6)Cookie —— 通过 setInterval 轮询 Cookie 变化(低效不推荐);7)WebSocket —— 通过服务端中转(适用于跨域/多设备场景)。选型建议:同源简单场景首选 BroadcastChannel,兼容性不佳时降级为 localStorage + storage 事件。跨域场景用 postMessage。注意 BroadcastChannel 不支持 Safari < 15.4。

  • Q: 点击一个input依次触发的事件 A: 点击 input 元素时触发的事件顺序(鼠标操作):mousedown -> focus -> focusin(冒泡)-> mousedown(第二次)-> mouseup -> click -> 若 input 类型为 radio/checkbox 还可能触发 change。对于 mousedown 会触发两次的浏览器(Chromium),第一次是激活(activate)阶段,第二次是正常的 mousedown。更详细的顺序:pointerdown -> mousedown -> focus -> focusin -> pointerup -> mouseup -> click。如果 input 绑定了 keydown 等键盘事件,需用户按键后触发。上述顺序中,focus 不支持冒泡,focusin 支持冒泡;对应的 blur(不冒泡)和 focusout(冒泡)在失焦时触发。如果需要监听 input 内容变化,用 input 事件(实时)、change 事件(失焦且值变化)。注意 input 事件每次变动都会触发(包括粘贴、输入法确认等),change 仅在提交时触发。使用 event.isTrusted 可以判断事件是用户触发还是 JS 触发。

  • Q: 有写过原生的自定义事件吗 A: 通过 CustomEvent API 可创建并派发自定义事件。创建:const event = new CustomEvent('my-event', { detail: { key: 'value' }, bubbles: true, cancelable: true })。派发:element.dispatchEvent(event)。监听:element.addEventListener('my-event', handler)。detail 属性用于传递自定义数据,事件处理函数中通过 event.detail 访问。低级 API document.createEvent('CustomEvent') 已废弃,不推荐使用。Event 构造函数也可以创建自定义事件,但无 detail 属性。注意事项:1)自定义事件默认不冒泡,需显式设置 bubbles: true;2)cancelable: true 允许 event.preventDefault() 取消默认行为;3)composed: true 让自定义事件可以穿透 Shadow DOM;4)自定义事件名称建议用 kebab-case 或带命名空间前缀避免冲突(如 app:user-login)。典型使用场景:组件间通信(远离框架)、Web Component 内部事件、封装第三方库的回调机制。

  • Q: addEventListener和attachEvent的区别? A: addEventListener 是 W3C 标准事件监听方法,attachEvent 是 IE6-8 的专有实现。主要区别:1)参数不同——addEventListener(type, listener, options/useCapture),第三个参数控制捕获/冒泡阶段;attachEvent('on' + type, listener),无捕获支持(IE 低版本不支持捕获,仅在冒泡阶段触发);2)this 指向——addEventListener 回调中 this 指向绑定的元素,attachEvent 指向 window;3)阻止默认行为——标准用 event.preventDefault(),IE 用 event.returnValue = false;4)阻止冒泡——标准用 event.stopPropagation(),IE 用 event.cancelBubble = true;5)挂载事件前缀——attachEvent 需加 'on',如 'onclick';6)多次绑定——addEventListener 对同一元素同一事件多次绑定相同函数只会执行一次(函数引用去重),attachEvent 会重复绑定。兼容方案:特性检测,优先使用 addEventListener,降级使用 attachEvent。在处理旧 IE 兼容时通常使用封装函数或 jQuery 等工具库。

  • Q: addEventListener函数的第三个参数 A: 第三个参数可以是布尔值或对象。布尔值 useCapture(默认 false):true 表示在捕获阶段触发回调,false 表示在冒泡阶段触发。对象 options 包含:1)capture(boolean):等价于 useCapture,控制阶段;2)once(boolean):回调只执行一次后自动移除,等价于 removeEventListener;3)passive(boolean):true 表示回调中永远不会调用 preventDefault(),浏览器可立即知道不需要阻止默认行为,从而优化滚动性能。Chrome 等浏览器对 touchmove/wheel 事件默认 passive: true 以提升滚动流畅度;4)signal(AbortSignal):传入 AbortController.signal,可通过 controller.abort() 移除监听器,无需手动 removeEventListener。典型场景:{ once: true } 用于一次性点击,"{ passive: true } 用于滚动事件避免阻塞主线程,{ capture: true } 用于需要在捕获阶段拦截事件(如全局点击拦截)。同一元素绑定多个事件时,capture 相同的监听器按注册顺序执行。

  • Q: DOM事件流是什么? A: DOM 事件流描述了事件从页面中接收的顺序,W3C 标准规定事件流分为三个阶段:捕获阶段(Capture Phase)-> 目标阶段(Target Phase)-> 冒泡阶段(Bubbling Phase)。捕获阶段:事件从 window -> document -> &lt;html&gt; -> &lt;body&gt; -> 父元素 -> 子元素,逐层向下传递到目标元素。目标阶段:事件到达实际触发事件的元素。冒泡阶段:事件从目标元素逐层向上传递回 window。开发者通过 addEventListener 的第三个参数控制事件在哪个阶段触发(true=捕获,false/默认=冒泡)。捕获阶段的实际应用较少,主要用于全局事件拦截或复杂组件的事件隔离(如点击外部关闭弹窗)。冒泡阶段是常用阶段,事件委托即利用冒泡机制。某些事件(如 focus、blur、load、scroll、mouseenter、mouseleave)默认不冒泡。event.eventPhase 属性标识当前阶段(1=捕获,2=目标,3=冒泡)。理解事件流是解决事件冲突、优化性能的基础。

  • Q: 冒泡和捕获的具体过程 A: 假设 &lt;div id="outer"&gt;&lt;div id="inner"&gt;&lt;/div&gt;&lt;/div&gt;,点击 inner 元素:捕获过程:window -> document -> document.documentElement (html) -> body -> #outer -> #inner(到达目标)。冒泡过程:#inner -> #outer -> body -> html -> document -> window。如果 #inner 和 #outer 都绑定了点击事件,#outer 用捕获(第三个参数 true),#inner 用冒泡:捕获阶段先触发 #outer,到达目标阶段触发 #inner,冒泡阶段不再触发 #outer(因为已经过了)。如果两个都用冒泡:先 #inner 后 #outer。阻止传播的方法:event.stopPropagation() 阻止后续阶段的传播(捕获和冒泡都阻止);event.stopImmediatePropagation() 不仅阻止传播,还阻止同一元素上其他同类型监听器执行。注意:在目标阶段,捕获和冒泡的回调都会执行,先注册的先执行(不论捕获还是冒泡,因为都在目标阶段)。实际开发中最常用的是冒泡阶段的事件委托。

  • Q: 关于一些兼容性 A: 前端事件兼容性问题主要集中在旧版 IE(IE8 及以下)。核心兼容点:1)事件绑定:标准 addEventListener vs IE attachEvent;2)事件对象:标准回调参数 event vs IE window.event;3)目标元素:event.target vs IE event.srcElement;4)阻止冒泡:event.stopPropagation() vs IE event.cancelBubble = true;5)阻止默认行为:event.preventDefault() vs IE event.returnValue = false;6)事件类型前缀:IE 需要 'on' 前缀,如 'onclick';7)DOM 级别:event.currentTarget 在标准中可用,IE 旧版可能不可靠。8)dispatchEvent vs IE fireEvent。9)鼠标按键:event.button 的映射值不同(标准 0/1/2 对应左/中/右,IE 返回值不同)。现代开发中,通过 Babel + Polyfill(core-js)或工具库(如 jQuery)处理大部分兼容问题。如需手写兼容,先进行特性检测:if (el.addEventListener) { ... } else if (el.attachEvent) { ... }。企业项目建议声明最低兼容浏览器版本,不要无底线兼容。

  • Q: 如何阻止冒泡和默认事件(兼容写法) A: 阻止冒泡兼容写法:```js function stopPropagation(e) { var evt = e || window.event; if (evt.stopPropagation) { evt.stopPropagation(); } else { evt.cancelBubble = true; // IE < 9 } }

阻止默认事件兼容写法:
```js
function preventDefault(e) {
  var evt = e || window.event;
  if (evt.preventDefault) {
    evt.preventDefault();
  } else {
    evt.returnValue = false; // IE < 9
  }
}

也可直接 return false 在内联事件处理函数中阻止默认行为和冒泡,但 addEventListener 中 return false 无效。注意:preventDefault() 只阻止默认行为(如点击链接跳转、表单提交),不阻止冒泡。stopPropagation() 只阻止冒泡不阻止默认行为。两者可单独使用也可组合使用。stopImmediatePropagation() 还可阻止同一元素上其他同类型监听器。

  • Q: 所有的事件都有冒泡吗? A: 不是,部分原生事件不支持冒泡。常见不冒泡事件:focus、blur、load、unload、scroll(大部分浏览器中 scroll 不冒泡)、mouseenter、mouseleave、resize、abort、error(资源加载错误,如图片)、DOMContentLoaded、beforeunload。mouseenter 和 mouseleave 设计上就不冒泡,与其相对的 mouseover 和 mouseout 是冒泡的。focus 和 blur 不冒泡,但 focusin 和 focusout 会冒泡(IE 首先实现,后纳入标准)。scroll 事件在不同浏览器表现不一致。自定义事件默认不冒泡,需在 new CustomEvent 时设置 bubbles: true。不冒泡的事件无法使用事件委托,需直接绑定到目标元素或使用对应的冒泡替代事件(如 focusin 替代 focus)。注意 event.bubbles 只读属性可查询事件是否冒泡。

  • Q: 拖拽有哪些知识点 A: 拖拽知识点:1)HTML5 Drag & Drop API——源元素设置 draggable="true",监听 dragstart、drag、dragend;目标元素监听 dragenter、dragover(需 preventDefault() 以允许放置)、dragleave、drop(获取 dataTransfer.getData());dataTransfer.setDragImage() 自定义拖拽预览;2)鼠拖拽(不使用 Drag API)——mousedown -> mousemove(需记录偏移量) -> mouseup,注意鼠标过快移出元素监听 window 事件;touch 事件做移动端兼容;3)数据传递——event.dataTransfer.setData('text/plain', data) / getData;支持多种 MIME 类型;4)文件拖拽——外部文件拖入浏览器,dataTransfer.files 获取 FileList,可结合 FormData 实现拖拽上传;5)拖拽排序——使用 Drag API 或 mousedown/mousemove 计算偏移实现列表重排,react-dnd 或 sortablejs 等库封装了底层实现;6)拖拽范围限制——通过判断鼠标位置是否在容器边界内。注意事项:移动端不兼容 HTML5 Drag API,需用 touch 事件;dragstart 中设置 effectAllowed 控制拖拽视觉效果;dropEffect 控制放置光标样式;安全和隐私限制下跨域拖拽数据无法读取。

  • Q: offset、scroll、client的区别 A: 这组属性用于获取元素尺寸和位置,但含义不同:1)offsetParent、offsetTop/offsetLeft——相对于最近定位祖先元素(position 非 static)的偏移,offsetHeight/offsetWidth = content + padding + border(不含 margin),即元素的完整可视尺寸;2)clientTop/clientLeft = border 宽度(上/左),clientHeight/clientWidth = content + padding(不含 border、scrollbar 和 margin),即元素内部可视区域大小;3)scrollTop/scrollLeft——滚动条滚动的距离(可读写),scrollHeight/scrollWidth——内容总高度/宽度(包括溢出部分),clientHeight 和 scrollHeight 的关系可用于判断是否出现滚动条或是否滚到底部(scrollHeight - scrollTop === clientHeight)。总结对比:offset 往外包括 border(元素在布局中占用的完整尺寸),client 是内部可视区域(padding 内不含 border),scroll 是内容的总尺寸。获取视口尺寸:document.documentElement.clientWidth(不含滚动条) vs window.innerWidth(含滚动条)。获取页面滚动距离:document.documentElement.scrollTop 或 window.pageYOffset。

  • Q: children以及childNodes的区别 A: children 和 childNodes 都是父节点的子节点集合,核心区别在于类型筛选:children 仅包含元素节点(nodeType === 1),如 div、span、p 等 HTML 标签;childNodes 包含所有子节点——元素节点、文本节点(换行符/空格也是文本节点)、注释节点、处理指令节点等。性能差异:children 返回 HTMLCollection(实时集合,动态更新),内部实现只遍历元素节点;childNodes 返回 NodeList(部分浏览器返回实时集合),包含全部节点类型遍历成本略高。示例:&lt;div&gt;Hello &lt;span&gt;World&lt;/span&gt;&lt;!-- comment --&gt;&lt;/div&gt;,div.childNodes 包含三个节点:文本节点 "Hello "、元素节点 &lt;span&gt;、注释节点 " comment ";div.children 只包含一个元素节点 &lt;span&gt;。实际开发中,children 更常用——我们通常只关心元素节点;childNodes 在处理富文本编辑器(需要保留文本节点信息)或 DOM 遍历时才有用。firstChild/firstElementChild、nextSibling/nextElementSibling 的区别同理。

  • Q: HTMLCollection和NodeList的区别 A: HTMLCollection 和 NodeList 都是类数组对象(持有多个 DOM 节点),但有以下区别:1)包含类型——HTMLCollection 仅包含元素节点(HTML 标签),NodeList 可包含任意类型节点(元素、文本、注释等);2)获取方式——HTMLCollection 通过 getElementsByTagName、getElementsByClassName、children 获取;NodeList 通过 querySelectorAll、childNodes 获取;3)动态性——HTMLCollection 是实时(live)集合,DOM 变化自动更新集合内容;NodeList 大部分是静态(static)集合(querySelectorAll 返回静态快照,childNodes 返回实时),静态集合不会随 DOM 变化更新;4)遍历方法——NodeList 支持 forEach()、entries()、keys()、values() 等迭代器方法(非 IE);HTMLCollection 没有这些方法,需先 [...collection] 转为数组才能用数组方法;5)索引访问——都支持数字索引和 .item() 方法,HTMLCollection 还支持 name/id 索引(namedItem())。最佳实践:querySelectorAll 返回静态快照,性能好且行为可预测;getElementsByTagName 返回实时集合,适合需要动态跟踪 DOM 变化的场景。

  • Q: 事件委托的缺点 A: 事件委托优点突出(减少监听器数量、动态元素支持),但也有缺点:1)this 指向——委托到父元素后回调中的 this 指向父元素而非实际触发元素,需用 event.target 获取真实目标;2)stopPropagation 失效——若子元素调用 stopPropagation(),事件无法冒泡到委托元素,导致委托失效。因此某些组件(如弹窗点击外部关闭)与事件委托冲突;3)事件过滤开销——需要通过 matches、closest 或 tagName 判断目标元素是否匹配预期选择器,在大量子元素或频繁触发的场景(如 mousemove)中增加额外计算;4)无法委托不冒泡的事件——focus、blur、scroll、mouseenter、mouseleave、load 等不冒泡,不能直接用事件委托;5)复杂选择器限制——matches() 支持 CSS 选择器,但某些动态生成的 class 拼接复杂时不易匹配;6)调试困难——事件面板显示监听器绑在父元素上,查哪个子元素触发需查看回调逻辑。权衡:一般 3 层以内或仅 3-5 个子元素时直接绑定,动态列表或大量子元素时优先委托。

  • Q: 事件委托中 target 和 currentTarget 区别 A: event.target 和 event.currentTarget 在事件传播中有本质区别:target 指向触发事件的原始元素,即事件流中的"目标阶段"元素,在整个传播过程中不变;currentTarget 指向当前正在处理事件的元素,即绑定事件监听器的元素,会随事件传播阶段变化。事件委托的典型场景:ul.addEventListener('click', handler),点击某个 &lt;li&gt; 后,event.target 是被点击的 &lt;li&gt;(或更精确的子元素如 &lt;span&gt;),event.currentTarget 是 &lt;ul&gt;。如果 &lt;li&gt; 内还有 &lt;span&gt;,点击 &lt;span&gt; 时 target 是 <span> 而 currentTarget 仍是 &lt;ul&gt;。判断委托代码中常用 event.target.matches('li') 或 event.target.closest('li') 来精确匹配预期委托目标。注意:在目标阶段(target phase),target === currentTarget;在捕获和冒泡阶段,target 不变但 currentTarget 为当前经历的节点。event.target 和 event.srcElement(IE 兼容)等价。

浏览器

  • Q: 浏览器渲染原理 A: 从接收 HTML 到屏幕像素的完整过程:1)HTML 解析——浏览器将 HTML 字节流解析为 Token,构建 DOM(Document Object Model)树。遇到 CSS 外部资源会并行下载,遇到无 async/defer 的 script 会阻塞解析;2)CSS 解析——解析 CSS 为 CSSOM(CSS Object Model)树;3)合并渲染树——将 DOM 树与 CSSOM 树合并为 Render Tree(渲染树),只包含可见元素(display:none 和 &lt;head&gt; 子元素不在渲染树中);4)布局(Layout/Reflow)——计算每个可见元素的几何位置和尺寸,递归地从根渲染器计算盒子模型;5)绘制(Paint)——将布局阶段计算出的每个节点绘制为屏幕像素(填充颜色、绘制文本、边框、阴影等),通常分为多个图层(Layer);6)合成(Composite)——将不同图层按层级关系合并为最终画面,GPU加速合成。关键优化点:避免频繁读写偏移布局属性(强制同步布局、布局抖动);使用 transform/opacity 触发合成不触发布局或绘制;CSS 动画优先使用 GPU 合成属性。现代浏览器通过增量渲染和分层合成持续优化渲染性能。

  • Q: 为什么操作 DOM 慢 A: DOM 操作慢的根本原因是 JS 引擎和渲染引擎的隔离。浏览器中 JavaScript 引擎(V8)和渲染引擎(Blink/WebKit)运行在不同进程/线程中,操作 DOM 时需要在两个引擎间进行昂贵的跨进程/跨线程通信(IPC),涉及数据序列化/反序列化。具体原因:1)DOM 是 C++ 实现的数据结构,JS 每次访问 DOM 属性都会产生跨语言的"桥接调用"开销;2)强制同步布局——JS 修改样式后立即读取布局属性(如 offsetHeight),浏览器必须立即执行"急切的样式计算和布局"而非等待批量优化,导致布局抖动;3)重排/回流——修改 DOM 尺寸或位置触发布局树重新计算,影响范围可能扩散到整棵树甚至下游节点;4)重绘——修改颜色、背景等视觉属性需重新绘制像素;5)GC 压力——创建和销毁大量 DOM 节点增加垃圾回收频率。优化策略:1)批量 DOM 操作(DocumentFragment);2)离线 DOM(display:none 后修改);3)虚拟 DOM 减少实际 DOM 操作次数;4)使用 requestAnimationFrame 合并布局读写;5)避免频繁强制同步布局。

  • Q: 重绘和回流(Reflow / Repaint) A: Reflow(回流/重排)是指 DOM 几何属性变化(宽高、位置、display 显隐、字体大小等)导致浏览器重新计算元素布局的过程。Repaint(重绘)是元素外观变化但不影响布局(如 color、background-color、visibility、outline)时重新绘制像素的过程。回流一定会触发重绘,重绘不一定会触发回流。触发回流的操作:1)添加/删除可见 DOM 元素;2)元素位置变化(position、margin、padding);3)元素尺寸变化(width/height、font-size、border);4)浏览器窗口 resize;5)获取布局属性——offsetTop/Left/Width/Height、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、getBoundingClientRect()。Chrome 等现代浏览器会缓存布局变更,在 JS 同步执行完毕后统一计算(异步布局),但读取上述属性时会强制刷新队列(强制同步布局)。优化策略:1)批量修改样式(class 切换 vs 直接操作 style);2)读写分离——先读后写,避免交叉;3)使用 transform 替代 top/left 实现动画(transform 仅触发合成);4)opacity 替代 visibility + 透明度变化;5)will-change 提示浏览器创建独立图层;6)离线 DOM 操作(DocumentFragment、display:none)。Chrome DevTools Performance 面板可录制并直观看到 Reflow 和 Repaint 耗时。

  • Q: 浏览器缓存机制 A: 浏览器缓存分为强缓存和协商缓存。强缓存:资源过期前直接从本地读取,不发送 HTTP 请求。通过 Cache-Control(HTTP/1.1)控制:max-age=31536000 表示有效期秒数;public/private 是否可被中间缓存;immutable 表示资源永不变化(用于版本化资源)。兼容方案:Expires(HTTP/1.0)指定绝对过期时间。Cache-Control 优先级高于 Expires。协商缓存:资源过期后发送条件请求验证资源是否可用。方式一:Last-Modified/If-Modified-Since——服务器返回资源最后修改时间,浏览器下次请求时带上 If-Modified-Since,服务器比较决定返回 304 Not Modified 或新资源。缺点:精确到秒、修改时间变化但内容可能未变。方式二:ETag/If-None-Match——服务器返回资源哈希值,更精确。ETag 优先级高于 Last-Modified。Service Worker 可编程拦截请求实现更灵活的缓存策略(如 Stale-While-Revalidate)。策略选择:版本化资源(JS/CSS)用强缓存 max-age=31536000, immutable,HTML 用 no-cache(每次都协商验证)或 no-store(完全禁止缓存)。强缓存状态码 200(from memory cache/from disk cache),协商缓存 304。

安全

  • Q: XSS 防御 A: XSS(跨站脚本攻击)指攻击者向 Web 页面注入恶意脚本。三类:1)反射型——恶意脚本在 URL 参数中,服务端未过滤直接返回;2)存储型——恶意脚本存储到数据库,其他用户访问时执行,危害最大;3)DOM Based——客户端 JS 动态拼接数据到 DOM 时注入。防御手段:1)输出编码——HTML 上下文转义 &lt;&gt;&"',JS 上下文转义 \n\r'",URL 上下文使用 encodeURIComponent;2)CSP(Content Security Policy)——通过 HTTP 头 Content-Security-Policy 限制脚本执行来源,白名单策略如 script-src 'self' 禁止内联脚本;3)HttpOnly Cookie——标记 Cookie 为 HttpOnly 防止 document.cookie 窃取;4)输入验证——对用户输入进行类型、长度、格式校验,拒绝可疑内容;5)使用框架的自动转义机制——React JSX 默认转义、Vue 的插值表达式默认转义(v-html 需谨慎);6)DOM 操作安全——不直接 innerHTML,优先 textContent 或框架模板;7)Trusted Types——Chrome 支持的 API,强制执行安全的 HTML 赋值。纵深防御原则:后端也必须做过滤和转义,前端防御只是第一道防线。

  • Q: CSRF 防御 A: CSRF(跨站请求伪造)利用用户已登录的身份,诱导用户在不知情下向目标网站发起恶意请求。根本原因是 Cookie 自动附带机制。防御方式:1)SameSite Cookie 属性——Lax 模式(GET 请求自动带 Cookie,POST 需要同站),Strict 模式(所有跨站请求都不带 Cookie),Chrome 80+ 默认 Lax。可用作基础防御,但不能完全依赖;2)CSRF Token——服务端生成随机 Token 嵌入表单或 HTTP 头,请求时校验。Token 应绑定用户 Session、随机且一次一密。攻击者无法获取 Token 内容(同源策略保护);3)双重 Cookie 验证——请求头中携带一个自定义字段(值为 Cookie 中的随机值),服务端比对。比 Token 实现简单但安全性稍弱(需确保 Cookie 不被子域覆盖);4)Referer/Origin 校验——检查请求来源是否合法。但 Referer 可能被隐藏或篡改;5)关键操作二次验证——支付/改密等操作要求输入密码或验证码(图形验证码、短信验证)。最佳实践:现代 Web 首选 SameSite=Lax + CSRF Token 组合。前后端分离项目使用 Token 认证(JWT)时通常放在 Authorization 头中而非 Cookie,天然免疫 CSRF。

  • Q: 点击劫持防御 A: 点击劫持(Clickjacking)通过透明 iframe 覆盖在目标页面之上,诱导用户点击看似无害区域但实际上触发了目标页面的操作。原理:攻击页面用 &lt;iframe src="目标URL" style="opacity:0; position:absolute; z-index:999;"&gt;&lt;/iframe&gt; 覆盖在诱饵按钮上。防御方式:1)X-Frame-Options HTTP 响应头——DENY(禁止所有 iframe 加载)、SAMEORIGIN(仅同源页面可嵌入 iframe)。这是最原始但有效的方案,但存在浏览器兼容性限制;2)Content-Security-Policy frame-ancestors 指令——CSP Level 2 方案,更细粒度控制:frame-ancestors 'self' 只允许同源、'none' 禁止所有、或指定具体域名。优先级高于 X-Frame-Options;3)Frame Busting(框架破解)——前端 JS 判断 if (top !== self) { top.location.href = self.location.href; }。但攻击者可通过 sandbox iframe 的 allow-top-navigation 限制或 onbeforeunload 事件阻止跳转,作为辅助方案而非主防御;4)SameSite Cookie 可部分缓解(攻击页面中请求因非用户操作不带 Session Cookie)。推荐方案:CSP frame-ancestors 作为主防御,配合 X-Frame-Options 做传统浏览器兼容。

框架(Vue / React)

  • Q: Vue2 / Vue3 双向绑定原理 A: Vue2 通过 Object.defineProperty() 递归重写 data 对象的 getter/setter 实现响应式。每个组件维护一个 Dep 实例(依赖收集器),getter 中执行依赖收集(将 Watcher 加入 Dep),setter 中通知所有 Watcher 更新。核心流程:Observer -> Dep -> Watcher。缺点:1)数组索引/长度变化无法检测(Vue2 重写了 7 个数组方法进行 hack);2)对象新增/删除属性无法检测(需 Vue.set/Vue.delete);3)递归遍历所有属性,对象层级深时初始化性能开销大;4)不能监听 Map/Set。Vue3 使用 ES6 Proxy 代理整个对象而非属性,reactive() 返回原始对象的 Proxy。Proxy 可拦截对象所有操作(get/set/has/deleteProperty/ownKeys 等 13 种),无需递归遍历——get 时当访问嵌套属性才递归响应化(惰性代理)。配合 Reflect 确保默认行为正确。数组和动态属性新增天然支持,无需特殊 API。Vue3 还引入了 ref()(将基本类型包装为 { value: reactive })、computed()、effect() 等。副作用管理上 Vue3 用 WeakMap 存储依赖关系,依赖粒度更细,内存表现更好。性能对比:Vue3 响应式初始化速度提升约 2 倍,内存占用减少约 50%。同样,Vue3 的编译时优化(PatchFlag、Block Tree)使运行时更新更高效。

  • Q: React 合成事件原理 A: React 的合成事件(SyntheticEvent)是跨浏览器原生事件的一层封装,目的:1)抹平浏览器差异(事件对象属性、阻止冒泡/默认行为 API);2)性能优化——使用事件委托统一管理。16/17 版本将事件监听器委托到 document(17 改为 root container),利用事件冒泡捕获所有事件,内部维护事件池和回调队列。事件触发流程:原生 DOM 事件冒泡到根节点 -> React 收集路径上所有 Fiber 节点的监听器 -> 按事件阶段顺序构造合成事件对象 -> 批量执行回调。合成事件的特点:1)event.__proto__ 指向 SyntheticEvent,包装了 nativeEvent 属性(原生事件对象);2)事件池机制(V17 移除)——事件对象在回调后被回收复用,异步访问需要 event.persist();3)stopPropagation() 阻止合成事件传播,但不一定能阻止原生事件传播(取决于注册阶段)。React 18 中对合成事件进行了进一步优化,与并发模式(Concurrent Mode)兼容。注意:onScroll 不委托(scroll 不冒泡),直接绑定在元素上。

  • Q: Vue 组件渲染流程 A: Vue 组件完整的渲染流程:1)模板编译——Vue 的 $mount 触发编译,将 template 编译为 render 函数,包含三类操作:parse(模板字符串解析为 AST)、optimize(标记静态节点进行 Diff 优化)、generate(AST 生成 render 函数字符串)。Vue3 提升更激进(静态提升、patch flag、树级标记);2)响应式初始化——执行 data/computed 等,Vue2 用 Object.defineProperty Vue3 用 Proxy 建立响应式追踪;3)创建 VNode——render 函数执行返回 VNode Tree,期间访问的响应式属性触发依赖收集(Watcher/Effect 注册);4)Patch/Diff——__patch__ 对比新旧 VNode Tree,Diff 算法同层比较,使用 key 优化列表重排。Vue2 采用双端比较(4 指针),Vue3 采用最长递增子序列算法优化移动;5)DOM 更新——根据 Diff 结果执行 DOM 增删改操作;6)生命周期钩子——在这个过程中触发对应的生命周期(beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted)。初次渲染只走创建挂载流程(无 Diff),后续响应式数据变化触发重新渲染时走更新流程。Vue3 的编译优化(Block Tree、静态提升、PatchFlag)大幅减少非必要的 VNode 比较,提升运行时效率。

  • Q: Vue2 vs Vue3 响应式 A: Vue2 响应式基于 Object.defineProperty,Vue3 基于 Proxy。核心对比:1)API 层面——Vue2 通过 data() 返回对象,Vue3 提供 reactive()(对象)、ref()(基本类型+对象)、computed()、readonly() 等组合式 API;2)检测能力——Vue2 无法检测属性新增/删除(需 Vue.set/delete)、无法检测数组索引赋值和 length 变化(需重写 7 个数组方法);Vue3(Proxy)可捕获所有操作,天然支持动态增删属性和数组操作;3)初始化性能——Vue2 递归遍历所有属性执行 defineProperty,对象层级越深开销越大;Vue3 采用惰性代理,只有属性被 get 时才递归代理子对象,初始化更快;4)内存——Vue2 每个属性都有一个 Dep,对象多时开销大;Vue3 用 WeakMap 按对象维度存储副作用,内存更紧凑;5)TypeScript 支持——Vue3 用 TS 重写,类型推断更好;6)副作用管理——Vue2 Watcher 与组件绑定,Vue3 的 effect 更灵活,配合 watchEffect 自动追踪;7)Set/Map/WeakSet/WeakMap——Vue3 可直接响应化,Vue2 不支持。迁移建议:新项目直接 Vue3 + Composition API,旧项目升级需评估第三方库兼容性。

  • Q: Redux / Zustand / MobX 区别 A: 三种状态管理库的设计哲学不同。Redux:函数式、不可变数据、单一 Store。通过 dispatch action -> reducer 返回新 state -> selector 派生数据。流程严格、可预测、时间旅行调试强大。但样板代码多(action type/creator/reducer),需配合 Redux Toolkit 简化。异步用 redux-thunk/saga。适合大型项目、需要严格状态管理和追踪的场景。Zustand:极简 API,基于 Hooks,单一 Store 但可拆分。通过 create(set =&gt; ({...})) 创建,状态更新直接 set({ key: newVal }),支持不可变更新(immer 中间件)。无需 Provider 包裹,按需订阅(selector),不会引起无关组件重渲染。体积仅 ~1KB。适合中小型项目或追求简洁开发体验的团队。MobX:面向对象、响应式、可变数据。通过 observable 标记状态、action 修改、computed 派生。基于 Proxy 实现依赖追踪,修改数据自动更新引用它的组件(类似 Vue)。自由度更高、代码更简洁,但追踪不透明导致调试困难,不可变数据支持较弱。适合偏爱 OOP 或对响应式更熟悉的团队。选型建议:大型严格项目选 Redux Toolkit,追求开发体验选 Zustand,倾向响应式编程选 MobX。React 内置 Context + useReducer 也可替代简单 Redux 场景。

  • Q: useContext 实现原理 A: useContext 是 React 读取 Context 值的 Hook。原理:React 内部维护一个 Context 栈(Fiber 树中每个节点关联 context 依赖列表)。通过 React.createContext(defaultValue) 创建 Context 对象(包含 Provider 和 Consumer)。Provider 组件将 value 存入 Fiber 节点的 dependencies 链表中。useContext(ContextObject) 在渲染阶段执行时,React 从当前 Fiber 节点向上遍历找到最近的 Context Provider,获取其 value。若未找到则返回 defaultValue。每次 Provider 的 value 变化时,React 标记该子树需要重新渲染。React 内部使用 readContext 函数实现:在 begin work 阶段当前 Fiber 的 contextDependencies 链表上注册对该 Context 的订阅。当 Context value 改变时,从 Provider 所在 Fiber 节点开始向下遍历标记所有订阅了该 Context 的子 Fiber 节点为"需要更新"(即使父组件跳过渲染,子树也能感知变化)。注意:useContext 使组件成为"纯 UI 组件"仍然能在 Context 变化时重渲染。性能优化:当 context value 变化时,所有使用了该 Context 的组件都重渲染,应拆分 Context 粒度或使用 useMemo 包裹 value。

  • Q: Hooks vs Class 对比 A: Hooks(16.8+)和 Class 组件是 React 定义组件的两种方式,Hooks 是渐进增强方案而非替代。核心对比:1)状态逻辑复用——Class 无原生复用手段,需 HOC/render props 导致嵌套过深(Wrapper Hell);Hooks 通过自定义 Hook 扁平化复用逻辑;2)生命周期——Class 将逻辑分割在不同方法(componentDidMount/componentDidUpdate/componentWillUnmount)容易产生相关逻辑分散;Hooks 通过 useEffect 将关联逻辑聚合同一处,以"关注点"而非"时间点"组织;3)this 指向——Class 中方法需要绑定 this 或使用箭头函数,Hooks 直接在函数作用域内捕获 Props 和 State;4)Tree Shaking——Class 组件包含完整生命周期方法体积大,Hooks 按需引入更利于 Tree Shaking;5)编译优化——Class 内部方法名不会被 uglify/压缩,Hooks 作为闭包函数可压缩;6)TS 支持——Hooks 类型推断更自然;7)学习曲线——Class 需理解 this 绑定,Hooks 需理解闭包陷阱和 useEffect 依赖数组;8)性能——Class 通过 shouldComponentUpdate/PureComponent;Hooks 通过 React.memo + useMemo/useCallback。未来趋势:React 官方推荐 Hooks,Class 组件仍可使用但不会再有新特性支持。

  • Q: VDOM vs Fiber A: Virtual DOM 是 DOM 的轻量 JS 表示(嵌套 JS 对象),通过 Diff 算法对比新旧 VNode 最小化真实 DOM 操作,解决了"声明式 UI 描述"与"命令式 DOM 操作"的鸿沟。但递归 Diff 不可中断(调用栈深度与 VNode 树深度成正比),可能导致主线程长时间阻塞。Fiber 是 React 16 引入的新协调引擎,是 VDOM 的升级实现。Fiber 将每个虚拟节点转化为 Fiber 节点(链表结构),并引入可中断的增量渲染。核心机制:1)任务分片——每个 Fiber 节点代表一个最小工作单元,可被暂停/恢复/终止;2)双缓冲——current fiber tree(当前已渲染)和 workInProgress fiber tree(正在构建),构建完成后切换指针;3)优先级调度——通过 lane 模型区分更新优先级(高优先级任务打断低优先级任务);4)并发渲染——渲染过程分阶段:render 阶段(构建 Fiber 树,可中断)和 commit 阶段(提交 DOM 变更,不可中断)。Fiber 实现了"时间切片",将大任务拆为小任务在浏览器帧空闲时执行,避免掉帧。Fiber 不仅是架构变更,还为 Suspense、并发模式提供了基础。

  • Q: Suspense 实现原理 A: React Suspense 用于处理异步依赖(动态加载组件、数据获取)的加载状态显示。核心原理:1)挂载 Suspense 组件后,其子树渲染过程中若某个组件抛出一个 Promise(表示正在等待异步数据),React 会捕获该 Promise,暂停该子树渲染,转而展示 fallback 内容;2)当 Promise resolve 后,React 重新尝试渲染该子树,此时数据就绪,组件正常渲染;3)Suspense 内部的渲染状态由 Suspense 边界(Suspense Boundary)管理,一个组件可以包含多个 Suspense,层级嵌套。React 18 新增的 SuspenseList 控制多个 Suspense 的显示顺序。数据获取框架(如 Relay、SWR、React Query)通过抛出 Promise 集成 Suspense。实现细节:throw promise 被 Component 内的 hooks 或 render 函数抛出,React 的 reconciliation 过程中检查到抛出的 thenable,则标记当前 Suspense 边界进入挂起状态,提交 fallback 到 DOM。并发模式下,Suspense 可与 Transition API 结合(startTransition),旧内容在加载新内容时保持可见(避免全屏 Spinner)。Suspense 的"展示 fallback -> 数据就绪 -> 渲染真实内容"流程需要框架层(如 React Router 的 lazy loading)配合实现,而非自动对所有异步操作生效。

  • Q: useEffect 清理函数使用场景 A: useEffect 返回的清理函数在组件卸载或依赖变化重新执行前调用。典型场景:1)取消订阅——取消 WebSocket、EventSource、addEventListener 注册的全局事件(如 resize/scroll)、RxJS 订阅等,防止内存泄漏和状态更新已卸载组件;2)清除定时器——setInterval/setTimeout 未清除会在组件卸载后继续执行导致内存泄漏或误更新;3)取消未完成的异步请求——使用 AbortController 在清理时 abort() 或设置标志变量 let cancelled = false,避免竞态(如快速切换 Tab 时旧请求响应更新已卸载组件的状态);4)关闭连接——关闭 WebSocket、IndexedDB 事务等;5)清理动画——requestAnimationFrame 或动画库实例的暂停/清除;6)恢复默认状态——恢复 body 样式(如 modal 打开时设置 overflow:hidden 需关闭时恢复)、移除全局 class。清理函数执行时机:组件卸载时、依赖变化时(先清理前一次 Effect,再执行新 Effect)。React 18 严格模式开发环境下,Effect 会触发两次(挂载-卸载-挂载)帮助检测清理函数逻辑缺失。

  • Q: useMemo / useCallback 在 SSR 中有什么问题 A: SSR 环境中 useMemo 和 useCallback 的行为与 CSR 不同:1)服务端渲染时组件只执行一次(无交互),useMemo 的缓存仅在单次渲染中有效——服务器生成 HTML 流后内存即释放,每个请求独立,useMemo 无法跨请求缓存。实际上 SSR 中 useMemo 和 useCallback 退化为普通的计算和函数定义,完全不会带来性能收益(因为不存在多次重渲染);2)反而增加了序列化和内存开销——服务端需要序列化状态传给客户端(hydration),多余的 memoization 和闭包增加了 JavaScript 字符串的体积;3)若 useMemo 内部有副作用(不推荐但可能发生),SSR 中会执行一次,hydrate 时再执行一次,可能导致结果不一致。最佳实践:在 SSR 中避免使用 useMemo/useCallback 做优化(它们本就不是 SSR 优化手段),保留仅为了代码逻辑一致性。SSR 中更有效的优化是流式渲染(streaming)、组件级缓存(如 React.cache)、数据缓存(CDN/Redis)。未来 React Server Components 在服务端完全不区分 useMemo(服务器组件中不可用 hook)。

  • Q: Hooks 为什么不能写在循环条件里 A: React Hooks 的调用顺序是 Hook 机制的核心依赖。React 内部使用链表存储组件所有 Hook 的状态:每个 Hook(useState/useEffect/useRef 等)在 Fiber 节点的 memoizedState 链表中按调用顺序对应一个节点。每次渲染时 React 按完全相同的顺序遍历链表读取/写入 Hook 状态。如果 Hook 写在条件或循环中,条件分支变化会导致某次渲染跳过某个 Hook 调用,后续所有 Hook 的索引错位——链表节点与 Hook 类型/数据对不上。例如:第一次渲染调用了 useState('A') -> useState('B'),链表存了 [A, B]。第二次渲染条件导致跳过第一个 useState,只剩 useState('B'),React 读取链表第一个节点拿到的是 A 的值(应为 B 的状态),状态完全错乱。React 无法恢复这种错误,只能报错。ESLint 的 react-hooks/rules-of-hooks 规则强制 Hooks 按"静态可分析"的方式调用(不在 if/for/while 中、不在 return 后、不在回调中)。注意:自定义 Hook 内部同理,也必须按固定顺序调用。

  • Q: 为什么 React 19 的 use 可以写在循环 / 条件里 A: React 19 引入的 use() API 不遵循 Hooks 规则——它可以写在条件、循环或提前 return 之后。原因:use() 不是一个 Hook,而是一个新型异步原语。use(promise) 直接读取 Promise 的值,本身不创建任何状态链表或副作用队列。它的实现不依赖调用顺序:use() 不向 Fiber 的 memoizedState 链表注册节点,而是直接读取传递的 Promise 或 Context,通过 Suspense 边界处理 pending/error 状态。当 use(promise) 被调用且 Promise 未 resolve 时,use() 会"抛出"(类似于 throw 一个 thenable)被 Suspense 捕获,渲染回退 UI。Promise resolve 后重新渲染组件,use() 直接从缓存读取结果,不会重复创建状态。由于不依赖调用顺序索引,use() 可以放在任何位置。这与传统 Hooks 的链式索引机制本质不同。use() 是 React 对数据获取和异步原语的更底层抽象,标志着 React 开始从"严格顺序依赖"向"值依赖"演进。注意:use() 目前仅在 React 19 canary 中可用,需配合 Suspense 使用。

  • Q: useEffect 闭包陷阱 A: useEffect 闭包陷阱指 Effect 回调中捕获的是旧 props/state 值而非最新值的现象。根本原因:每次渲染都会创建独立的 Effect 函数闭包,闭包捕获的是该次渲染时的 props 和 state(快照值)。当依赖数组未更新时,Effect 回调中的值仍指向旧的快照。示例:useEffect(() =&gt; { setInterval(() =&gt; setCount(count + 1), 1000) }, [])——count 始终为初始值 0,因为回调闭包只捕获了第一次渲染的 count。解决方法:1)正确填写依赖数组——将所有被引用的外部变量加入 [],但可能导致 Effect 频繁执行;2)使用 useRef 保持最新引用——ref.current 总指向最新值,不会被闭包捕获旧值;3)使用函数式更新——setCount(prev =&gt; prev + 1) 不依赖外部 count;4)使用 useCallback/useMemo 配合 useRef 维持引用稳定;5)在 Effect 清理函数中清理旧的闭包相关操作(清除旧定时器)。核心理解:React 遵循"渲染快照"语义,闭包陷阱不是 Bug 而是 React 声明式设计的结果,需要理解机制后正确使用 Hooks。

  • Q: React 父子组件通信方式 A: React 组件通信方式:1)Props 传递(父 -> 子)——最常见的下行数据流,父组件通过属性传递数据,子组件通过函数参数接收。传递回调函数实现子传父;2)Context API——React.createContext + useContext/Context.Consumer,跨层级传递数据(主题、语言、认证信息等),避免 props drilling;3)useImperativeHandle + forwardRef——父通过 ref 调用子组件的方法,适用于暴露子组件内部操作(如表单校验、焦点控制)。父调用 ref.current.validate();4)状态提升——子组件需要共享状态时提升到最近的公共父组件统一管理;5)Callback Props——父传递回调函数给子,子触发时带数据回传,实现子 -> 父通信;6)Event Bus——通过发布订阅模式(mitt、EventEmitter)在非父子组件间通信,但不利于数据流追踪,不推荐;7)第三方状态管理——Redux/Zustand/MobX,适用于跨组件/跨页面的全局状态。注意:React 是单向数据流,强调数据向下、事件向上。除非特别必要,优先提升状态而非使用全局状态管理。在组件树中使用 Context 时应注意 value 引用稳定性,避免不必要的重渲染。

  • Q: React Native vs Web 区别 A: React Native(RN)和 Web React 共享相同的声明式编程模型和 JSX 语法,但核心区别:1)渲染目标——Web 渲染到 DOM(&lt;div&gt; &lt;span&gt; 等);RN 通过原生桥接渲染到原生 UI 组件(&lt;View&gt; -> UIView/android.view)。RN 的组件映射到原生组件,非 HTML 元素;2)样式系统——Web 用 CSS(层叠、继承、选择器);RN 用 JS 对象样式(类似 style={})、Flexbox 布局为主、无级联/继承/全局选择器、像素单位自动换算为 dp/pt;3)导航——Web 使用 URL 路由(React Router、Next.js);RN 使用原生导航库(React Navigation),管理原生栈导航器和转场动画;4)线程模型——Web 单线程(JS 引擎 + 渲染引擎);RN 有三个线程:JS 线程(业务逻辑)、原生线程(UI 渲染)、Shadow 线程(布局计算 Yoga)。JS 和原生线程通过异步桥接通信;5)调试——Web 浏览器 DevTools;RN 使用 Chrome DevTools(remote JS debugging)或 Flipper;6)性能——RN 的桥接通信(JSON 序列化/反序列化)是性能瓶颈,新架构(JSI/Fabric/TurboModules)通过直接内存访问替代桥接;7)跨平台——RN 一套代码可运行 iOS 和 Android,Web 可用 React Native Web 辅助但非一等支持。RN 适合需要原生性能且共享代码逻辑的移动应用。

  • Q: React Navigation Stack 层数限制 A: React Navigation Stack 没有硬性的源码定义的数字限制,但在实践中底层原生导航实现(UINavigationController 或 FragmentManager)和系统可用内存构成了实际约束。iOS UINavigationController 的设计原则是扁平导航(层级不超过 3-4 层),深层嵌套会导致:1)内存累积——每个 Screen 保持其视图层级和状态,回退时虽然会释放部分内存,但状态依然保留在导航状态树中;2)动画卡顿——深层 Push/Pop 的转场动画叠加可能导致 GPU 压力;3)手势返回——深层栈的右滑返回手势可能出现响应延迟。建议:1)遵循 iOS HIG(3-4 层以内),超过时用 Modal、Tab 或嵌套导航拆分;2)使用 reset 而不是一直 navigate 避免栈无限增长;3)深层栈超过 50 层在 Android 上可能触发 FragmentManager 溢出或 ANR;4)如果确实需要深层导航(如 Feed 流无限详情页),考虑用 Zustand/Redux 管理页面状态,以单一页面承载不同内容(类似 Web 的路由参数变化而非 push 新页面)。实际项目中 push 到 20+ 层时就需要审视导航架构设计了。

工程化

  • Q: npm / yarn / pnpm 原理对比 A: 三者都是 Node.js 包管理器,核心区别在依赖管理策略和性能。npm v1/v2:嵌套 node_modules,依赖深度嵌套导致路径过长,Windows 上可能超出文件路径限制。npm v3/yarn v1:扁平化方案,将所有依赖提升到顶层 node_modules,解决了嵌套问题但导致"幽灵依赖"——项目可访问未在 package.json 中声明的包;不同版本依赖冲突时仍需嵌套安装。yarn v1 引入 lock 文件(yarn.lock)确定性安装,离线缓存和并行下载提升速度。npm v5+ 也引入了 package-lock.json。pnpm:使用硬链接 + 符号链接实现高效磁盘利用。依赖存储在全局 store 中,项目通过硬链接引用,同版本依赖只存一份。node_modules 结构与 package.json 严格对应(严格隔离),不会出现幽灵依赖。每个包的依赖通过符号链链接到自己的 node_modules 下再找到下级依赖,保持嵌套结构但不重复存储。pnpm 的优势:1)磁盘空间省约 70%(全局共享);2)安装速度更快(硬链接零拷贝);3)严格隔离,避免隐式依赖问题;4)Monorepo 原生支持(workspace 协议)。yarn v2/v3(PnP)尝试通过 .pnp.cjs 映射文件消除 node_modules,但兼容性问题较多。当前趋势:新项目推荐 pnpm(速度、磁盘、安全性、monorepo 支持俱佳)。

  • Q: Sentry 日志监控 A: Sentry 是开源的错误追踪和性能监控平台。前端接入方式:使用 @sentry/react(或 @sentry/vue)SDK,在应用入口初始化:Sentry.init({ dsn: '...', environment: 'production', tracesSampleRate: 0.2 })。核心功能:1)错误捕获——自动捕获未处理异常、Promise rejection、React 错误边界(ErrorBoundary);2)手动上报——Sentry.captureException(error)、Sentry.captureMessage('message', 'level');3)性能追踪——通过 tracesSampleRate 控制采样率,记录页面加载、路由切换、API 请求等 Span 形成 Trace;4)Source Map——上传 Source Map 实现生产环境错误堆栈反解到源码;5)Breadcrumbs——记录事件序列(网络请求、点击、导航等)帮助定位错误上下文;6)Release 追踪——绑定版本号,关联错误与发布版本;7)Session Replay——录制用户操作回放(Beta)。最佳实践:1)配置 beforeSend 过滤没必要上报的错误;2)错误处理中附带自定义上下文 Sentry.setContext('user', { id });3)在 CI/CD 流程中自动化上传 Source Map 和 release。注意:Source Map 上传后应确保生产环境不公开 Source Map 文件,避免源码泄露。

  • Q: SSR / ISR 原理 A: SSR(Server-Side Rendering):请求时在服务端渲染 HTML 字符串,发送给客户端。流程:浏览器请求页面 -> 服务端执行组件代码获取数据 -> 生成完整 HTML -> 返回给浏览器(内容立即可见)-> 浏览器加载 JS -> Hydration(事件绑定和状态恢复)。优势:首屏内容快速呈现、SEO 友好。劣势:每个请求都需服务端渲染、TTFB 较长、服务端负载高。ISR(Incremental Static Regeneration):在 SSG(Static Site Generation)基础上,实现按需增量更新静态页面。首次构建时生成静态 HTML 到 CDN,设置 revalidate 时间(如 60 秒)。当用户请求在 revalidate 时间内——直接返回缓存 HTML(瞬间响应)。超过 revalidate 时间——服务器返回缓存版本,同时在后台重新生成新页面并更新缓存。ISR 结合了 SSG 的速度和 SSR 的实时性。注意:revalidate 不是定时过期策略,而是"过期后下一个请求触发重新生成"。on-demand revalidation(Next.js 12.1+)允许通过 API 手动触发特定页面重新生成(如 CMS 发布内容时)。ISR 需要在 Next.js 中配合 getStaticProps 的 revalidate 属性和 res.revalidate(path) API 实现。适合电商商品页、博客文章等高缓存命中率但有更新需求的场景。

  • Q: Next.js SSR 原理 A: Next.js SSR 核心机制:页面级 getServerSideProps 在每次请求时运行,返回的 props 注入到页面组件。完整流程:1)服务端接收到 HTTP 请求 => 匹配路由 => 执行 getServerSideProps(同步或异步获取数据)=> 将 props 注入页面组件 => React 在服务端渲染生成 HTML 字符串 => 嵌入到 HTML document 模板返回;2)浏览器收到 HTML(首屏内容立即可见)=> 加载 JS bundle => React Hydrate:将服务端渲染的静态 HTML 变为可交互的 SPA(事件绑定、状态初始化);3)后续页面跳转通过 Next.js 的客户端路由(next/link)实现 CSR,不刷新页面。SSR 的挑战:1)window/document 不可用(服务端无 DOM),需动态导入或 useEffect 条件执行;2)数据请求需在服务端完成(getServerSideProps 中的 fetch base URL 需完整);3)性能——每个 SSR 请求都需调用 API 和渲染 React 组件,大量请求时需考虑缓存策略(CDN 缓存、页面级缓存 ISR、API 响应缓存)。Next.js 也支持 Streaming SSR(React 18):服务端渲染的 HTML 分块发送,首字节更早到达,配合 Suspense 实现逐步加载。注意:getServerSideProps 仅服务端运行,不会打包到客户端 JS 中。

  • Q: WebSocket 握手 A: WebSocket 握手使用 HTTP Upgrade 机制从 HTTP 协议升级为 WebSocket 协议。握手过程:1)客户端发起 HTTP GET 请求,包含特殊请求头:Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key: &lt;base64随机16字节&gt;(用于服务器确认协议支持)、Sec-WebSocket-Version: 13(指定协议版本)、可选 Origin/Sec-WebSocket-Protocol(子协议);2)服务端验证请求头,若支持 WebSocket 则返回状态码 101 Switching Protocols,包含:Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Accept: &lt;base64(SHA1(Sec-WebSocket-Key + 固定 GUID))&gt;。客户端本地用相同方式计算并对比 Sec-WebSocket-Accept 值验证服务端理解 WebSocket 协议;3)握手完成后,HTTP 连接升级为全双工的 WebSocket 连接,此后通信使用 WebSocket 帧格式(而非 HTTP 请求-响应模型)。每个帧包含 FIN/opcode/mask/payload length/payload data 等字段。浏览器端使用 new WebSocket('ws://...') 自动触发握手,通过 onopen 确认连接建立。Node.js 端(ws 库)处理握手细节。注意:WebSocket 握手通过 HTTP 代理转发时需配置代理支持 Upgrade 请求(如 Nginx 需 proxy_set_header Upgrade $http_upgrade;)。

性能优化

  • Q: 首屏优化 A: 首屏优化核心目标是缩短 FCP(First Contentful Paint)和 LCP(Largest Contentful Paint)。策略分维度:1)网络层面——启用 HTTP/2 多路复用减少连接数,CDN 加速静态资源分发,Preconnect 预连接第三方源,dns-prefetch 预解析 DNS,资源预加载(preload 关键资源/prefetch 非关键资源);2)资源体积——代码分割(Code Splitting)按路由懒加载,Tree Shaking 移除未使用代码,JS/CSS 压缩(Terser/CSSNano),Gzip/Brotli 压缩传输,图片格式选 WebP/AVIF,图片尺寸适配视口,字体子集化;3)渲染优化——Critical CSS 内联首屏样式,SSR/SSG 减少客户端渲染等待,骨架屏减少白屏感知,图片懒加载(IntersectionObserver),避免同步大脚本阻塞解析;4)缓存优化——强缓存版本化静态资源,Service Worker 缓存页面壳子,HTML 用协商缓存;5)性能度量——使用 Lighthouse/LightHouse CI 设定性能预算(Performance Budget),LCP 目标 < 2.5s,FID < 100ms,CLS < 0.1。首屏优化应结合业务特点:电商重图片、重列表,内容站重文本、重 SSR。同构 SSR 目前是首屏优化最有效的手段之一(Next.js/Nuxt.js)。

  • Q: 虚拟列表滚动原理 A: 虚拟列表(Virtual List)解决大量数据渲染的性能问题——只渲染可视区域内的元素,上下超出部分用空白占位。原理:1)计算可视区域可容纳的行数(viewportHeight / itemHeight);2)监听 scroll 事件(或 scrollTop 变化),计算当前滚动偏移对应的起始索引 start = Math.floor(scrollTop / itemHeight);3)渲染 [start, start + visibleCount] 范围内的元素,多余空间用上下 padding(或占位 div)填充;4)维护 startOffset——将渲染列表绝对定位或通过 transform: translateY 偏移到正确位置。关键实现细节:1)固定高度实现简单,动态高度需组件渲染后回调上报真实高度,维护位置映射表;2)缓冲冗余——多渲染上下几行(overscan)预防快速滚动时出现白屏;3)缓存位置信息——动态高度需缓存每个元素的高度或位置;4)滚动容器——通常用固定高度的外层容器 + overflow-y: auto,或 window 作为滚动容器。优化点:使用 transform 而非 top 避免回流;will-change: transform 提示 GPU 加速;IntersectionObserver 替代 scroll 事件提升性能。常用库:react-window、react-virtuoso(React)、vue-virtual-scroller(Vue)。

  • Q: 图片懒加载方案 A: 图片懒加载延迟加载屏幕外图片,减少初始请求数和带宽。核心实现方案:1)IntersectionObserver(推荐)——监听图片元素与视口的交叉状态,进入可视区时设置 src 属性。示例:const observer = new IntersectionObserver(entries =&gt; { entries.filter(e =&gt; e.isIntersecting).forEach(e =&gt; { e.target.src = e.target.dataset.src; observer.unobserve(e.target); })}); observer.observe(img)。优势:纯异步、性能好、不触发滚动事件;2)传统 scroll + getBoundingClientRect()——监听 scroll/resize 事件(需 debounce),判断 img.getBoundingClientRect().top &lt; window.innerHeight 后加载。缺点是频繁触发计算,需性能优化;3)loading 属性——原生属性 &lt;img loading="lazy"&gt; Chrome 76+ 支持,最简单的方案但无法自定义占位和触发时机;4)占位处理——加载前用低质量模糊图(LQIP)或纯色背景过渡;5)组件封装——封装 &lt;LazyImage&gt; 组件,内部使用 IntersectionObserver,提供 placeholder/error 状态。注意事项:首屏图片不建议懒加载(应直接加载),影响 LCP;预留图片尺寸(width/height 或 aspect-ratio 占位)避免 CLS(布局偏移);注意 loading="lazy" 可能延迟 LCP 加载,对首屏图片慎用。

  • Q: 轮播图实现思路 A: 轮播图(Carousel/Slider)核心实现思路:1)布局结构——外层容器 overflow: hidden 固定宽高,内部容器 display: flex,每个 slide 等宽排列,通过 transform: translateX(-Npx) 或 margin-left 控制偏移;2)自动播放——setInterval 定期切换到下一张,鼠标悬停时暂停(mouseenter 清除/ mouseleave 恢复);3)无限循环——首尾各克隆一个虚拟 slide(前后各一张),实际索引切换到末尾时瞬间跳转到克隆位置(无动画),制造循环假象。或用 CSS scroll-snap-type 实现原生平滑滚动;4)指示器——小圆点导航,点击跳转到对应 slide;5)手势支持——移动端监听 touch 事件(touchstart -> touchmove 计算位移差 -> touchend 判定滑动阈值决定是否翻页),缩放适配;6)性能优化——使用 transform + requestAnimationFrame 驱动动画,避免回流;图片懒加载(非可见 slide 延迟加载);7)常用过渡——淡入淡出(opacity transition)、3D 翻转(perspective + rotateY);8)无障碍——设置 role="listbox"、aria-label、支持键盘左右键切换。复杂轮播(多个同时可见、无限嵌套、可变宽度)建议用 swiper.js 或 Embla Carousel 等成熟库。

综合 / 场景

  • Q: 技术选型怎么做 A: 技术选型需综合考虑:1)业务需求——项目规模(小工具 / 中后台 / 高并发 C 端)、更新频率、性能要求、目标平台(PC/移动/混合/跨端);2)团队能力——团队对技术栈的熟悉程度、学习成本、招聘难度。React 生态 > Vue 生态 > Angular 生态的招聘宽度。选择团队现有经验栈可降低交付风险;3)技术生态——社区活跃度、文档完善度、第三方库丰富程度、版本迭代稳定性。选主流框架(React/Vue)比小众框架更安全;4)框架特性——虚拟 DOM 适合高度交互 SPA(React/Vue 都行),SEO 需要 SSR(Next.js/Nuxt),大量 DOM 操作不如直接操作 DOM 的场景用 Svelte/Solid;5)运行时性能——轻量应用所有框架差异不大,大列表用 Solid/Svelte 更优;6)包体积和加载性能——非 SPA 场景(如 MPA)不要硬上重型框架;7)状态管理——大应用需要 Redux/Zustand/MobX,小应用 Context + useReducer 足够;8)工程化——构建工具(Vite/Webpack)、代码规范(ESLint/Prettier)、测试工具链、CI/CD 集成;9)迁移成本——老项目重构需考虑平滑迁移方案(微前端、渐进式重构)。方法论:列出候选方案 -> POC 验证关键技术点 -> 输出对比矩阵(功能/性能/开发效率/成本)-> 团队投票决策。

  • Q: 大文件上传 A: 大文件上传核心思路:分片上传 + 断点续传。实现方案:1)分片——前端将大文件用 File.slice() 切分为固定大小(如 5MB)的切片。每个切片独立上传,用 FormData 发送;2)并发控制——控制同时上传的切片数(如 3-5 个),避免浏览器连接数限制和内存溢出。使用 Promise Pool 或自定义队列管理;3)断点续传——上传前先请求后端获取已上传的切片列表,只上传缺失的切片;4)哈希计算——使用 spark-md5 或 Web Worker 中计算文件 MD5,作为切片唯一标识。大文件哈希计算可能耗时较长,放在 Worker 中避免阻塞 UI;5)进度展示——XMLHttpRequest 的 upload.onprogress 或 fetch(不支持进度)结合分片数量百分比计算整体进度;6)通知后端合并——所有切片上传完成后,请求后端合并接口;后端接收所有切片后按顺序合并为完整文件;7)失败重试——单个切片失败自动重试(3 次),重试失败后标记为失败状态;8)暂停/恢复——记录已上传切片列表,暂停时中止未完成的上传,恢复时重新检查上传进度。常用库:plupload、uppy、resumable.js,也可基于 axios 自实现。

  • Q: Token 过期,正好在提交时怎么处理 A: 核心方案:请求拦截器 + Token 静默刷新 + 请求队列。实现流程:1)请求发出前检查 Token 过期时间(exp),若即将过期同步刷新 Token;2)若未预检而请求时发现已过期(401),拦截响应,进入 refresh 流程:a. 暂停当前请求(不 reject,保存回调到队列);b. 检查是否有正在进行的 refresh 请求(防止重复刷新),若无则发 refreshToken 请求获取新 accessToken;c. refresh 成功后用新 token 重放队列中的所有请求;d. refresh 失败(如 refreshToken 也过期)则跳转登录页。关键代码模式:axios 响应拦截器中判断 error.response.status === 401,使用 Promise 创建请求队列。若有多个并发请求同时返回 401,只需一次 refresh 请求,后续所有 401 都等待该 refresh 完成后自动重试。注意:1)refreshToken 存储在 HttpOnly Cookie 中最安全,或 localStorage(有 XSS 风险);2)refresh 请求本身也需要排除拦截(避免死循环);3)使用 axios.interceptors.response.use 中的 config._retry 标记避免重试死循环;4)Token 即将过期时预先刷新(exp - currentTime &lt; 5min)可避免请求高峰期的 401 批量触发。这种"透明刷新"的 UX 最好,用户在提交时无感知 Token 过期。