面灵AI→

小红书前端开发岗面经合集:Vue 响应式、HTTP 缓存与 React 渲染机制

轮次
多轮面试合集
时间
2026-10
来源
牛客网

《面试题目》

面经 01 · 小红书 前端开发岗(2025-09-17)

  1. 为什么微信多次分享的时候 URL 会变?
  2. 什么情况下会返回 HTTP 304?服务端需要在请求头上做什么配置?

面经 02 · 小红书 前端开发岗(2025-09-16)

  1. Ref 和 reactive 的区别是什么?什么场景下用 reactive 比较好?
  2. 什么时候用 ref、什么时候用 reactive?什么情况下用 props、emits 这种父子传值?用户数据应该怎么传值?
  3. CSS 怎么进行水平居中?为什么 left 之后还要用 margin-left 微调?行内文本(比如 text)怎么进行垂直居中(line-height 与父子元素高度一致)?
  4. 怎么对图片进行旋转 45 度(transform: rotate(45deg))?
  5. 了解互动渲染相关的东西吗?

面经 03 · 小红书 前端开发岗(2025-09-12)

  1. 如果页面请求比较多、代码有上千行,由此导致的性能问题如何解决?
  2. Vue 的响应式原理是什么?再往更深的层面讲讲。
  3. CORS 解决跨域,如果要携带 cookie,怎么设置?

面经 04 · 小红书 前端开发岗(2025-09-02)

  1. Vue3 和 React 的差异是什么?技术选型的时候如何选择?
  2. React 中父组件想调用子组件的方法,应该怎么做?
  3. 实习过程中需求开发的流程是怎样的?
  4. git 相关操作有哪些?
  5. 闭包了解吗?手写一个防抖。
  6. 从 URL 输入到页面渲染的流程是什么?
  7. 三次握手、四次挥手的过程是什么?为什么不能两次握手?
  8. 解析 HTML 文件时遇到 JS 会怎么样?
  9. 说一下常见的 HTTP 状态码。
  10. 对于 304,具体说一下强缓存和协商缓存。
  11. 如何让请求每次都是强缓存?
  12. 手撕:树中路径和等于 target 的路径(包括从根节点到叶子节点、从根节点出发的任意路径、从任意节点出发的任意路径)。

面经 05 · 小红书 前端开发岗(2025-09-02)

  1. Promise 与它的执行顺序是怎样的?
  2. 跨端的 React Native 和 React 有什么区别?
  3. 做过哪些性能优化?
  4. 项目遇到的难点是什么?
  5. 了解过鸿蒙开发吗?对它的态度是什么?
  6. React 底层原理手撕。
  7. 两个字符串的最长公共子序列。

面经 06 · 小红书 前端开发岗(2025-09-01)

  1. 聊聊低代码平台和实习经历,说说实习里用的 jotai 状态管理是怎么回事?
  2. React 的底层:fiber 机制、diff 算法、调度机制分别是什么?
  3. 组件间通信详细讲讲,父子孙之间怎么通信、具体在哪里用什么方式,数据流可以单向也可以双向。
  4. JS 和 TS 的区别是什么?也往详细了说。

面经 07 · 小红书 前端开发岗(2025-09-01)

  1. 介绍一下你在小米的业务。
  2. 虚拟滚动怎么做的?怎么减少白屏时间?
  3. React 渲染的过程是什么样的?
  4. 渲染的时候能够中断吗?
  5. React 有什么渲染机制上的优化?
  6. 做题:Promise。
  7. 做题:树的层序遍历,倒序输出。
  8. 如何减少首页白屏时间?
  9. iframe 在大量数据交换的时候会有什么问题?
  10. 小程序的 iOS 输入盘遮盖问题是什么?
  11. 宏任务会阻塞渲染进程吗?

面经 08 · 小红书 前端开发岗(2025-08-28)

  1. Vue 响应式原理具体是怎么实现的?
  2. 场景题:多层级怎么进行组件通信?结合具体例子说。
  3. 跨域是什么?
  4. 事件循环机制是什么?
  5. 304 状态码、协商缓存,以及浏览器有哪些缓存策略?
  6. 结合实习,你觉得大模型能够对业务有什么帮助?
  7. Web 端和 H5 端开发你觉得区别在哪里?

面经 09 · 小红书 前端开发岗(2025-08-13)

  1. 项目:登录机制的设计、权限控制是怎么做的、某些功能是怎么实现的?
  2. Vue3 响应式(Proxy)怎么实现的?
  3. JS 的事件循环,外加一道题。
  4. em 和 rem 有什么区别?
  5. 浏览器拿到 HTML 之后是怎么解析、渲染的?
  6. 算法题:二叉树的最大深度。
  7. Vue 的生命周期分几个阶段,每个阶段做什么事?
  8. 单页应用和多页应用的区别?
  9. 算法:数组转树。
  10. 职业规划、实习时间是怎么安排的?

面经 10 · 小红书 前端开发岗(2025-06-15)

  1. 什么时候开始学习前端,学了多长时间?
  2. 什么时候开始学习 Vue,学了多长时间?
  3. Vue2 和 Vue3 的区别是什么?
  4. computed 和 watch 的区别,分别是干嘛的?
  5. Vue3 分别是怎样实现 watch 侦听的?两者的区别是什么?
  6. Vue3 用 Proxy 实现 watch 的缺点是什么?
  7. ES6 常用的语法有哪些?
  8. Promise 是如何实现的?
  9. Set 和 Map 的区别是什么?
  10. 如何改变 this 指向?(提示了 call、apply、bind)

《参考解析》

Vue3 响应式:ref 与 reactive 的取舍,以及 Proxy 到底缺什么

reactive 只接受对象(数组、Map、Set 也可以,Vue3 对集合类型用了单独的 handler),返回原对象的一个 Proxy,代理的是「对象本身」。所以一旦把引用整体换掉(state = {...}),或者解构出基本类型字段(const { count } = state),响应式就断了。ref 的设计正好绕开这一点:它永远是一个对象容器(RefImpl),通过 .value 读写,值挂在内部字段上,因此原始值也能被追踪;传参、放进数组、被解构时传的都是容器引用,不会丢响应,模板里还会自动解包,组合式函数返回 ref 也方便调用方再解构。选型有一个好用的判据:一组相互关联、不会被整体替换的状态用 reactive 更省事;需要跨函数传递、会被解构、可能整体替换、或者是原始类型,就用 ref。必须用 reactive 又要解构时配 toRefs;整体替换的场景通常用 Object.assign 合并回原对象来规避。

底层实现上,Proxy 拦截 get / set / deleteProperty / has / ownKeys 等操作:get 里做依赖收集(track),把当前运行的 effect(渲染函数、watch、computed 的 ReactiveEffect)按 target → key 存进一个 Set;set 里做派发(trigger),把相关 effect 放进调度队列。调度器会对队列去重、按 effect id 排序(保证父组件先于子组件更新),再用微任务批量 flush,这就是 nextTick 的由来。与 Vue2 的 Object.defineProperty 相比,Proxy 不需要在初始化时递归遍历所有属性做转换(Vue3 是访问到哪一层才递归包装哪一层),能监听新增和删除属性、数组索引与 length,原生支持 Map/Set;代价是不支持 IE、对内置对象的内部槽要单独处理、对 class 实例与私有字段要小心,而且原始值只能靠 ref 包一层。被问「Proxy 的核心缺陷」时,把这几点说全比只说「IE 不兼容」完整。

React 渲染:fiber、可中断与调度

fiber 是把渲染结果树重写成的一棵「工作树」:每个节点记录 type、props、对应的 DOM,以及 child / sibling / return 三个指针,于是递归变成可跳转的链表遍历,每一步都是一个可以暂停的工作单元。React 把渲染拆成 render 与 commit 两个阶段:render 阶段(beginWork / completeWork、diff、生成 effect list)可以被打断、丢弃、重做;commit 阶段改 DOM、执行布局副作用,必须一次做完。可中断靠调度器实现——它在宏任务里跑工作循环,每处理完一个单元用 shouldYield() 判断时间片(约 5ms)是否用完,用完就把控制权交还浏览器去处理输入和绘制,下一帧接着来。所以「渲染的时候能够中断吗」的答案是:能,但只在 render 阶段。

追问「渲染机制的优化」时,可以从「减少不必要的工作」和「让工作更可中断」两条线答:React.memo / useMemo / useCallback 减少重渲染(memo 是浅比较,props 里每次新建的对象或函数会让它失效),列表用稳定 key 让 diff 能复用节点,状态尽量下沉到真正使用它的组件、避免顶层 state 变化带动整棵树,长列表用虚拟滚动从根上减少 fiber 数量,非紧急更新用 startTransition / useDeferredValue 降优先级,首屏用 React.lazy + Suspense 做代码分割。diff 的启发式也值得点一句:只在同层比较、type 不同直接重建子树、用 key 匹配同层节点,所以把 key 写成数组下标,会在插入删除时退化成大面积重建。

TypeScript 的价值,以及 JS 与 TS 的区别

被问到「JS 和 TS 的区别」以及「项目为什么用 TS」时,答案要落在编译期而不是语法:TS 在编译阶段做静态类型检查,把一大批「跑到那一行才炸」的错误提前暴露,同时给编辑器提供补全、跳转与重构能力,开发体验的提升是顺带的结果;对大中型项目和多人协作,类型声明本身是带校验的文档,重构时改了接口能立刻看到所有受影响的位置;再往上一层才是泛型、严格模式、类型推导这些工程化能力,它们让代码更健壮也更容易复用——一个泛型容器能同时服务多种数据类型,而不用退化成 any。代价也要一起说:构建多一步类型检查、类型体操写过火会牺牲可读性、第三方库缺声明文件时要自己补;any 和 as 滥用会把整套类型系统退化成装饰。

同一场被顺带问到的 JS 基础属于同一族考点:类型转换要分清显式(String()、Number()、Boolean())与隐式(+ 拼接、比较时的 ToPrimitive)两套规则;== 会先做类型转换再比较(null == undefined 为真、'1' == 1 为真),=== 不转换,所以工程上默认用 ===;typeof null === 'object' 是 JS 早期用低位标记类型留下的历史遗留(null 的标记位与对象相同),已经无法在不破坏兼容的前提下修掉,只能靠 x === null 单独判断。

HTTP 缓存:304 从哪来,服务端要配什么

304 是协商缓存命中后的响应。前提是服务端在第一次 200 响应里给出了校验字段:ETag(内容指纹,强校验)或 Last-Modified(最后修改时间,弱校验,精度只到秒)。浏览器缓存这条响应后,下次请求带上 If-None-Match 与 If-Modified-Since;服务端比对,没变就回 304(不返回 body,只剩响应头),变了就回 200 和新内容。所以「服务端要做什么配置」是两条:一是首包必须输出 ETag 或 Last-Modified(Nginx 对静态文件默认就带),二是收到条件请求要正确比对并返回 304,同时可以在 304 里带上新的 Cache-Control 刷新新鲜度。ETag 优先于 Last-Modified,因为内容没变而 mtime 变了(部署、拷贝)时会误判。

强缓存与协商缓存的区别在「要不要发请求」:Cache-Control: max-age / Expires 命中的强缓存期间浏览器直接从内存或磁盘出内容,连请求都不发,服务端收不到日志也没机会判断;协商缓存每次都要发一个不带 body 的条件请求,适合入口 HTML 和接口。回到「如何让请求每次都是强缓存」:给静态资源配 Cache-Control: max-age=31536000, immutable,并让 URL 随内容变化(文件名带内容 hash),内容更新就是新 URL,不存在失效问题——这正是构建产物 index.[hash].js 的用意。边界也要说清:地址栏回车吃强缓存,普通刷新(F5)浏览器通常会对主文档做一次校验、能观测到 304,强制刷新(Ctrl+F5)直接绕过缓存。常见的追问还有 no-cache 与 no-store:前者是不用强缓存但可以缓存、每次必须校验(等价于强制协商),后者是完全不许存。

跨域与凭证:CORS 带 Cookie 的三个条件

跨域是浏览器的同源策略在拦:协议、域名、端口任一不同就算跨源,请求其实发出去了,但响应被挡在 JS 之外——所以要分清「服务端没收到」和「收到了但前端拿不到」。CORS 是标准解法,由服务端用响应头授权:简单请求比对 Access-Control-Allow-Origin;非简单请求(自定义头、非简单 Content-Type、PUT/DELETE 等)先发一个 OPTIONS 预检,服务端要用 Access-Control-Allow-Methods、Access-Control-Allow-Headers 回应。前端能做的很少,主要就是带凭证时的显式声明。

携带 Cookie 要三个条件同时满足:前端 fetch 传 credentials: 'include'(XHR 是 withCredentials = true),服务端 Access-Control-Allow-Credentials: true,并且 Access-Control-Allow-Origin 必须是具体源、不能是通配符,预检响应也要带同样的头。此外 Cookie 自身的 SameSite 也在管:跨站发送需要 SameSite=None; Secure,否则默认的 Lax 会拦掉跨站子请求。为什么 JSONP 不行:它靠 script 标签请求一段可执行的 JS(回调名塞在 query 里),所以只能 GET、拿不到状态码和响应头、超时与失败只能靠约定猜、必须把对方返回的字符串当代码执行(有 XSS 与回调污染风险,也不利于用 CSP 收紧),而且与现代工程体系(打包、类型、并发编排)几乎没法配合。CORS 已全平台支持,JSONP 只属于历史兼容。

从 URL 到渲染:链路、阻塞点与握手

链路可分四段:网络(URL 解析与缓存检查 → DNS → TCP 三次握手 → TLS → HTTP 请求响应,中间可能经过 CDN、网关)→ 解析(HTML 构建 DOM,CSS 构建 CSSOM,遇到 <script> 默认停下解析去下载并执行)→ 渲染(DOM 与 CSSOM 合成渲染树 → 布局算几何 → 绘制 → 分层合成上屏)→ 资源(图片、字体、异步请求,字体还会引发 FOUT/FOIT)。被追问「遇到 JS 会怎样」要答全:普通 <script> 阻塞 DOM 解析与后续资源发现,defer 是并行下载、文档解析完后按顺序执行,async 是并行下载、下载完立刻执行(顺序不保证),ES module 默认相当于 defer。CSS 阻塞渲染但不阻塞解析,关键 CSS 内联是常见解法。

「为什么不能两次握手」的标准答法是:三次才能让双方都确认自己的发送和对方的接收都正常,并且避免历史失效连接——两次的话服务端一收到 SYN 就建连,若这个 SYN 是网络里滞留的旧请求,服务端会白白建一条没人要的连接并一直等数据。四次挥手是因为 TCP 全双工、需要半关闭:主动方发 FIN 表示没数据了,被动方先 ACK(此时仍可能继续发),等它也发完再发 FIN + ACK,主动方进入 TIME_WAIT 等 2MSL 才真正关闭,为的是保证最后那个 ACK 能到达,并让本次连接的残留报文在网络里消散。宏任务会不会阻塞渲染:宏任务(脚本、定时器回调、事件回调)都在渲染主线程上同步执行,浏览器只在任务之间才有机会排版和绘制,所以一个长宏任务确实会顶住渲染(掉帧、点击无响应);微任务则在当前任务结束后、下一次渲染机会之前被一次性清空,所以把长计算切成微任务没用,要切成小宏任务(或用 requestIdleCallback、Worker)才让得出主线程。

性能优化:虚拟滚动、白屏与指标

虚拟滚动要解决的是「长列表一次渲染上万个节点会把 DOM 和 fiber 都撑爆」。做法是外层容器固定高度做滚动条(用一个撑高的占位元素),只渲染视口加缓冲区(上下各多一两屏)的那一小段,滚动时根据 scrollTop 算出起始下标,用 transform: translateY 或 padding-top 把渲染块顶到正确位置。定高列表只需要除法;不定高要先估算、再对已渲染项做测量并缓存高度与偏移(ResizeObserver 观测),否则会滚动跳跃。代价是滚动位置恢复、键盘可达性、浏览器内查找失效都要额外处理,key 也不能用下标。白屏优化按「减少要看的东西」和「更早开始下载」两条线:路由级代码分割、首屏依赖用 preload 提前、<link rel="preconnect"> 提前建连、SSR 或骨架屏先给结构、关键 CSS 内联、图片 loading="lazy" 并固定宽高比避免抖动、首屏不用的第三方脚本延后。指标要分 lab 与 field:FCP/LCP 看加载与首屏内容,CLS 看稳定性,TBT/INP 看交互响应,TTFB 反映后端与网络;实验室数据用 Lighthouse 或 Performance 面板测,真实用户数据看 CrUX 或自己用 web-vitals 上报。只报一个 LCP 而不说来源(p75、哪一端、什么网络)很容易被追问。

手撕:防抖、树路径和与最长公共子序列

防抖的骨架是闭包里的一个定时器:每次调用先 clearTimeout 再 setTimeout,延迟结束才真正执行;保留 this 与参数要用 apply,做「立即执行版」就加标志位区分首次触发与冷却期,工程上再补一个 cancel() 方便组件卸载时清理。常被追问「和节流的区别」:防抖是等安静下来执行一次(搜索联想、输入校验),节流是固定频率执行(滚动监听、拖拽),也可以合成「首次立即 + 尾随」的组合实现。在 React 里每次渲染都会新建闭包,要用 useMemo 或 useRef 把 debounce 后的函数存下来,并在 useEffect 的清理里取消。

树路径和等于 target,要先跟面试官对齐「路径」的定义,这题列了三种:从根到叶子,用回溯遍历、到叶子时判断累加和;从根出发到任意节点,可以在前序位置就判断;任意节点到任意节点,等价于「路径上两段前缀和之差等于 target」,一次 DFS 配 Map<前缀和, 出现次数> 就能降到 O(n)。回溯模板固定:进入节点压栈并累加,退出时弹出并减回去,别用每次拷贝数组的写法(一定会被追问空间)。最长公共子序列是送分题但不能写错:dp[i][j] 表示两个前缀的答案,字符相等时取 dp[i-1][j-1] + 1,不等时取 max(dp[i-1][j], dp[i][j-1]);要主动说清它和「最长公共子串」的区别(子串必须连续,不匹配时清零),空间可以用滚动数组压到一维。写完跑一个例子,比写完就说「好了」更让人放心。