尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

小红书前端笔试题卷三复盘:事件循环到性能优化的核心考点

小红书前端笔试题卷三复盘:事件循环到性能优化的核心考点 很多人背了一堆八股文结果拿到小红书2020校招前端笔试题卷三的时候第一反应是“这题怎么这么眼熟但我就是写不对”。这份卷子最有意思的地方在于它没有故意出偏题怪题几乎所有题目都在主流前端知识范围内但通过换场景、加限制、卡边界把“背过”和“真的懂”区分得非常清楚。我花了两天时间把这份卷子重新梳理了一遍包括每道题背后的考点、容易踩的坑、以及从面试官视角来看的评分倾向这篇复盘的文章就按这个思路来写。无论你是准备明年春招还是想检验一下自己的前端底子这份拆解应该都能帮上忙。1. 卷三的整体难度画像为什么刷了三个月题还会翻车1.1 小红书前端业务特点与出题逻辑的对应小红书前端团队的业务场景集中在内容分发、图片视频展示、用户互动、电商交易这几个方向。所以这份卷子里几乎没有脱离业务的纯算法题而是大量围绕“列表渲染”“异步请求”“图片资源处理”“移动端适配”来出题。这和很多大厂喜欢出“通用计算机基础纯LeetCode”的风格有明显区别。如果你在这份卷子上翻车大概率不是基础不扎实而是没有把基础知识和真实业务场景结合起来。举个例子同样考Promise普通题目会问“Promise.all怎么用”这份卷子可能会考“多个图片异步加载完成后统一更新进度条你会怎么设计”。考点还是那一个但思考维度完全不同。从题目的整体分布来看JavaScript异步与事件循环、手写代码、CSS布局与移动端适配、网络与缓存策略、开放设计题这五个模块构成了卷三的主体。每个模块的题目数量不多但每一道都值得花时间深挖。1.2 题型配比与典型分数布局根据我拿到的回忆版信息以及和几位参加过这轮笔试同学的交流卷三的大致结构可以总结为题型题量考察重点建议用时单选题8道左右JS基础、浏览器原理、网络协议10-15分钟多选题4道左右CSS、工程化、框架概念边界8-10分钟手写代码题3道异步控制、数组对象处理、算法25-30分钟场景设计题2道性能优化、架构设计思路20-25分钟开放性论述题1道方案选型、团队协作、代码质量意识10-15分钟这个时间分配很关键。很多同学在前面的选择题上反复纠结结果最后两道设计题只能糊弄过去。但设计题往往是最能拉开分数差距的部分——面试官在改卷时看的就是你的思路完整度和业务敏感度。2. 从异步题到状态管理高频基础题的考场答题逻辑2.1 事件循环输出顺序题别被宏任务微任务混排吓住卷三中有一道典型的事件循环题代码大概是下面这种变体console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); new Promise((resolve) { console.log(promise executor); resolve(); }).then(() { console.log(promise then 1); }).then(() { console.log(promise then 2); }); queueMicrotask(() { console.log(microtask); }); console.log(script end);这道题本身并不算难但在考场上会有很多人写错。主要错在两个地方一是把promise executor当成微任务二是搞不清多个微任务之间的执行顺序。正确输出顺序是script start promise executor script end promise then 1 promise then 2 microtask setTimeout我们逐步拆一下。console.log(script start)和new Promise里的执行器函数都是同步代码所以最先执行。script end也是同步代码在微任务之前。同步代码全部执行完之后微任务队列开始工作.then注册的回调依次执行然后queueMicrotask的内容也进微任务队列按照先进先出的顺序处理。最后才轮到宏任务队列里的setTimeout回调。这里有一个细节值得注意.then链上的两个回调其实是两个独立的微任务第一个.then回调执行完后会立刻把第二个.then回调推进微任务队列所以它们会连续执行。而queueMicrotask是在Promise链创建之后才注册的所以会排在promise then 2之后。如果你把queueMicrotask写在new Promise之前输出顺序又会不一样。这类题目考的不是背答案而是对事件循环机制顺序的敏感度。实际业务中异步加载、状态更新、DOM渲染的先后关系几乎天天都在和这套机制打交道。我建议在准备时不要只看“顺序表”而是自己画一遍执行栈、宏任务队列、微任务队列的变化过程多写几个变体直到不再依赖背诵为止。2.2 手写Promise.all考察的不是记忆力是边界感手写Promise.all是校招前端笔试中的常客卷三也有一道类似的题目。但这里要注意面试官并不只是想看你“背不背得出来”而是看你有没有考虑到各种边界情况。一个比较完整的参考实现是function myPromiseAll(promises) { return new Promise((resolve, reject) { if (!promises || typeof promises[Symbol.iterator] ! function) { throw new TypeError(argument is not iterable); } const result []; let remaining 0; let settled false; let index 0; for (const item of promises) { const currentIndex index; remaining; Promise.resolve(item).then((value) { result[currentIndex] value; remaining--; if (remaining 0 !settled) { settled true; resolve(result); } }, (error) { if (!settled) { settled true; reject(error); } }); index; } if (remaining 0) { settled true; resolve(result); } }); }这个实现有几个关键点。第一入参检查。很多同学直接写for (const item of promises)但如果传入的promises是null或者undefined代码会直接抛错。更严谨的写法是先用Symbol.iterator判断是否是可迭代对象。第二用Promise.resolve(item)包裹每一项。这样做是因为Promise.all接收的数组里的元素不一定是Promise对象可能是普通值。Promise.resolve可以统一处理。第三结果数组要按输入顺序存储而不是按异步完成的顺序存储。这是Promise.all最容易被忽略的语义。我们不能在then里直接result.push(value)而要用currentIndex来定位。第四空数组的情况。如果传入的数组是空的remaining是0就需要直接resolve([])。上面代码里的if (remaining 0)分支就是处理这个情况的同时也要确保后面不会重复resolve。在试卷上答题时即使不要求写出完整代码也建议把关键点用注释给出来。这样面试官至少能看到你考虑到了边界问题而不仅仅是默写了一个模板。2.3 防抖与节流一道题背后的状态管理思维卷三有一道防抖节流的应用题题干大意为搜索框输入时需要通过接口请求联想词要求尽可能减少请求次数同时保证用户操作体验顺畅。请写出一种实现方案。这道题90%的人能写出基本实现但能拿到高分的很少因为大多数人的答案里缺了两样东西一是有没有区分防抖和节流的适用场景二是有没有考虑到输入内容变化的边界。防抖的核心思想是“在事件触发后等待一段时间再执行如果在这段时间里再次触发就重新计时”。典型的应用场景就是搜索联想、按钮提交。节流的核心思想是“固定时间内只能执行一次”典型场景是滚动事件监听、窗口大小调整。对于搜索联想正确的选择是防抖因为用户连续输入时我们并不希望每敲一个字母就发一次请求而是要等用户停下来了再发。一般建议300毫秒左右这个时间既能保证请求量可控又不会让用户觉得卡顿。如果只写到“用防抖就完事”分数只能算及格。高分答案还需要补充几个点第一次输入时要不要立即执行如果用户在搜索框里已经有内容的情况下再次聚焦可能需要立即请求一次。leading和trailing如何处理这涉及防抖函数是否要在首次触发时立即执行一次。竞态问题。如果前一次请求响应慢后一次请求先返回了界面上的数据可能被旧请求覆盖。需要在请求时带上请求序号或者取消上一次的AbortController。网上能找到的防抖节流实现版本很多核心思路基本一致区别主要体现在细节上。比如有的版本会考虑参数传递、this绑定、取消防抖状态等。建议你在准备时不要只记模板而是把“防抖是被动的等待节流是主动的限制”这个本质记清楚才能应对各种变形题。3. 小红书业务味最浓的一道题长列表与图片性能优化3.1 瀑布流与滚动加载从条件渲染到IntersectionObserver小红书的内容展示以瀑布流为主图片多、滚动频、移动端占比高。卷三里有一道题专门考这个场景在一个无限滚动的图片瀑布流中如何实现流畅的加载体验请谈谈你的设计方案。如果你只是回答“用懒加载”或“用滚动监听”基本就是中等偏下。真正需要展开的是一整套“滚动性能解决方案”至少包含以下几个层次滚动事件本身要节流或使用passive: true避免频繁触发布局抖动。判断元素是否进入可视区首选IntersectionObserver而不是scroll事件里手动计算getBoundingClientRect。图片未进入可视区前不要渲染真实图片可以使用占位占位元素配合loadinglazy属性和>const loadMoreObserver new IntersectionObserver((entries) { if (!entries[0].isIntersecting) return; loadNextPage(); }, { root: null, rootMargin: 200px 0px 200px 0px, threshold: 0.01 }); const target document.querySelector(.load-more-trigger); loadMoreObserver.observe(target);rootMargin设置成200px的作用是提前预加载让用户还没滑到底部就开始请求下一页数据减少白屏等待时间。这个细节如果你能在答题时写出来面试官会觉得你确实做过真实项目。3.2 图片优化策略的完整回答框架图片加载在瀑布流场景里是避不开的话题。卷三这道题的第二问通常是问图片资源本身如何优化。这个点特别能看出一个人是“背过知识点”还是“真正处理过线上的图片问题”。我整理了一个比较完整的回答框架按优先级排列格式选择内容型图片用WebP或AVIF图标类资源使用SVG。WebP在同等画质下体积比JPEG小25%-35%AVIF更小但兼容性稍差。尺寸裁剪不要直接缩放原图而是通过CDN的图片处理参数按实际展示尺寸裁切。比如列表里的缩略图尺寸是300px宽就不应该加载原图地址。清晰度适配通过srcset配合sizes属性让浏览器根据设备分辨率自动选择合适倍率的图片。移动端1倍图、2倍图不能一把梭。加载策略首屏立即加载非首屏懒加载快速滑过的图片要及时取消加载避免占用带宽。占位与骨架未加载完成时用轻量级占位色块或模糊图过渡提升感知性能。有一个常见误区是“把所有图片都懒加载”。实际上首屏里的Logo、主视觉这些内容应该立即加载懒加载只适合视口之外的资源。如果首屏资源也走懒加载可能会出现快速白屏闪烁体验反而更差。试卷上如果能按照“体积优化、数量优化、加载时机优化”三个维度来作答结构就非常清晰了。每个维度再展开对应的手段就是一份能拿高分的答案。3.3 从题目延伸到框架设计虚拟列表的实现要点聊完瀑布流和图片优化卷三这道题往往还会延伸一个问题如果列表有几千条数据是否还能继续用“滚动加载”。答案是可以但要考虑新的方案。从技术角度讲几千条图片数据如果在DOM里一次性渲染即使所有图片都懒加载也会因为DOM节点数量过多导致首屏渲染性能严重下降。这时候需要虚拟列表Virtual List方案。虚拟列表的核心思想是只渲染可视区内的DOM节点其他节点用占位符撑起滚动高度。具体实现要点包括计算可视区域能容纳的行数visibleCount。根据滚动偏移量计算起始渲染索引startIndex。渲染startIndex到startIndex visibleCount之间的数据。用paddingTop和paddingBottom模拟剩余高度让滚动条保持正确比例。一个简化的实现思路function VirtualList({ allItems, itemHeight, containerHeight }) { const [scrollTop, setScrollTop] useState(0); const visibleCount Math.ceil(containerHeight / itemHeight); const startIndex Math.floor(scrollTop / itemHeight); const endIndex Math.min(startIndex visibleCount, allItems.length); const visibleItems allItems.slice(startIndex, endIndex); const totalHeight allItems.length * itemHeight; return ( div style{{ height: containerHeight, overflowY: auto }} onScroll{(e) setScrollTop(e.target.scrollTop)} div style{{ height: totalHeight, position: relative }} {visibleItems.map((item, index) ( div key{item.id} style{{ position: absolute, top: (startIndex index) * itemHeight, height: itemHeight, width: 100% }} {item.content} /div ))} /div /div ); }这个简化版本假设每一项高度相同。如果每一项的高度不固定比如评论内容长度不一就需要在渲染后动态测量并缓存每一项的偏移量复杂度会高出不少。考试时如果时间紧张不需要手写完整代码但至少要把“固定高度虚拟列表”的思路讲清楚。如果还能提到“动态高度的处理方式是为每一项维护一个位置缓存渲染后测量实际高度并修正偏移量”就已经超过大多数候选人了。4. 手写题和代码题的隐藏评分点不只是跑通就得分4.1 一道LRU缓存题能看出多少编码习惯卷三的手写题里有一道很有意思的题实现一个LRU缓存。题面很简单但能考察的东西非常多。LRU的全称是Least Recently Used即“最近最少使用”。它的规则是缓存空间满时淘汰最长时间没有被访问的数据。实现目标有两个操作——get(key)和put(key, value)要求两个操作的时间复杂度都是O(1)。在O(1)复杂度下光用数组是不行的因为数组查找需要遍历。经典方案是“哈希表双向链表”哈希表负责快速定位节点双向链表负责记录访问顺序。每次访问一个节点就把它移动到链表头部淘汰时直接删除链表尾部节点。class LRUCache { constructor(capacity) { this.capacity capacity; this.map new Map(); this.head { prev: null, next: null }; this.tail { prev: null, next: null }; this.head.next this.tail; this.tail.prev this.head; } get(key) { if (!this.map.has(key)) return -1; const node this.map.get(key); this.moveToHead(node); return node.value; } put(key, value) { if (this.map.has(key)) { const node this.map.get(key); node.value value; this.moveToHead(node); return; } if (this.map.size this.capacity) { const tailNode this.tail.prev; this.removeNode(tailNode); this.map.delete(tailNode.key); } const newNode { key, value, prev: null, next: null }; this.addToHead(newNode); this.map.set(key, newNode); } moveToHead(node) { this.removeNode(node); this.addToHead(node); } removeNode(node) { node.prev.next node.next; node.next.prev node.prev; } addToHead(node) { node.prev this.head; node.next this.head.next; this.head.next.prev node; this.head.next node; } }在笔试场景下这道题的隐藏评分点主要有三个。第一是否选择了合理的数据结构。如果直接写数组版本或者Map 数组排序版本复杂度上就没法看。面试官一眼就能判断你是“见过这道题”还是“懂这道题”。第二边界处理是否到位。比如capacity为0的情况get不存在的keyput已存在的key时value要不要更新满了之后要先删最久未用的节点再插入新节点这些细节很能体现编码功底。第三代码可读性。模块拆成moveToHead、removeNode、addToHead三个方法比全部内联在一个方法里更清晰。大家平时写业务代码时也许不觉得但在笔试场景里面试官看到这种结构的代码第一反应就是“这人写代码有条理”。4.2 准备笔试应该养成的三个编码习惯结合卷三的手写题情况我总结了三个对应试非常有帮助的编码习惯这些习惯同样适用于实际的开发工作。第一个习惯是“先注释核心思路再写代码”。很多同学考试时拿到题目直接开写写到一半才发现方向有问题。如果先在代码顶部用注释写下“用Map存节点引用双向链表维护顺序”一方面帮自己理清思路另一方面也让改卷人一眼看到你的思考路径。第二个习惯是“动手之前先问清输入输出”。如果题目条件不明确比如数组里是否有负数、字符串是否可能为空、参数数量是否固定可以先把边界情况在代码里用注释标注出来。不要规定但可以礼貌地在答案开头写“假设输入满足以下条件……”。这比闷头写一个能处理所有情况的巨无霸函数要靠谱得多。第三个习惯是“写完留时间自查边界”。我自己参加面试和模拟面试时发现很多候选人代码主体没问题但会在简单的边界判断上翻车。比如数组长度为1、对象属性不存在、整型溢出这些情况。自查时把边界分支跑一遍能避免非常多的低级失分。5. 面对开放性设计题怎么答才不像背答案5.1 “如何设计一个评论区的实时展示方案”的拆解思路卷三的开放性设计题据回忆和“评论区实时展示”有关用户在笔记下方发表评论后其他用户能否尽快看到新评论。要求你设计一个完整的前端展示方案不限制技术栈。很多同学看到“实时”两个字第一反应就是WebSocket。但这道题更关键的点在于“实时展示”和“一致性”的权衡。一个真正的实时方案并不是无脑全量推送而是要考虑不同场景下的最优解。我的答题思路可以总结为四个步骤第一步分析需求。评论区的实时展示涉及两个关键角色发表评论的用户和浏览评论的用户。发表者需要即时看到自己的评论反馈浏览者需要在不刷新页面的情况下看到新评论。这两个需求的实时性要求并不相同发表者那条是强即时性浏览者那条可以根据场景适度放宽。第二步明确约束。公开笔记可能有一万人在同时浏览而普通笔记可能只有几个人在互动。如果所有笔记的评论都全量推送服务器压力会成倍增加。因此需要按热度分级处理而不是一把梭用WebSocket。第三步方案对比。这里可以展开WebSocket、SSEServer-Sent Events、轮询这三种常见机制的优缺点。WebSocket是全双工通信功能最完整但服务端实现成本高、连接数多、维护复杂度大。SSE是单向通道服务端可以主动推送客户端只需用EventSource接口接收实现成本远低于WebSocket非常适合“评论广播”这种服务端单向推送的场景。轮询最简单但实时性差、请求量大适合低热度的笔记兜底。第四步给出最终方案。最稳妥的回答结构是热度高的笔记用SSE推送新评论热度低的笔记用“新评论计数手动刷新”的降级方案配合发表评论后本地乐观更新。这个方案既考虑了实时性又没有把所有场景都压在高成本的通信机制上。面试官听到这里基本就能确定你是真的做过类似功能。5.2 答题结构需求澄清、方案对比、落地步骤、风险兜底开放性设计题最容易出现的问题是“一上来就写方案”。题目本身可能只有一句话但方案空间极大直接写方案等于默认自己的假设成立风险很高。更职业的做法是先用一段话把题目的需求拆解清楚再逐步展开。我常用的答题模板是需求澄清先用自己的话复述题目列出需求方、使用者、核心场景、非核心场景。关键指标用什么标准衡量方案好坏首屏速度请求次数体感流畅度可选方案把能想到的方案都列出来并简述各自的优缺点。不要只写自己倾向的那个。最终选择与理由说明为什么选A不选B回扣场景特点。风险与兜底线上出现问题怎么办如何监控如何降级这个结构不只在笔试卷上适用在实际技术方案评审时也基本是这样。你在练习设计题时最忌讳的是只看别人的“标准答案”而是要学会自己套这个逻辑走一遍形成肌肉记忆。另外有个小技巧设计题的答案里要体现出“分阶段”。比如“对白屏的优化第一版可以先做骨架屏第二版再做服务端渲染第三版做流式返回”。面试官要看的不是你一次性能不能做到完美而是你有没有“迭代思维”知道不同阶段该解决什么问题。6. 复盘之后给准备校招前端的同学一份可落地的刷题清单6.1 按优先级整理的核心知识点通过复盘卷三我们可以整理出一份不需要依赖具体题目也能复用的知识点清单。按优先级排序如下。第一梯队必须完全掌握事件循环机制宏任务、微任务、执行栈的执行顺序。Promise核心APIall、race、allSettled、finally的实现和语义。手写防抖、节流以及它们对应的应用场景。数组和对象的高频操作去重、扁平化、排序、深拷贝。CSS布局Flex、Grid、两栏/三栏布局、水平垂直居中。浏览器缓存强缓存、协商缓存、Service Worker的基本思路。第二梯队高频出现需要理解原理虚拟列表的实现思路和复杂度分析。虚拟DOM与Diff算法。React/Vue的渲染流程与状态更新机制。前端性能优化指标FCP、LCP、CLS以及对应的优化手段。工程化Webpack构建流程、Loader和Plugin的区别、常见优化手段。第三梯队加分项体现深度微前端方案的原理与适用场景。状态管理库的设计思路Redux、Mobx、Vuex的对比。大文件上传的切片与断点续传方案。移动端适配方案rem、vw、动态font-size。TypeScript类型体操中的常用模式。我建议各位按这个梯队准备而不是抓到什么学什么。校招笔试能考察的知识范围是有限的把第一梯队弄扎实通过笔试的概率就已经很高了。第二梯队决定你能不能拿高分第三梯队则是面试官在面试环节深挖的素材笔试阶段出现频率不高。6.2 考场时间分配与答题顺序实战建议时间分配这个事容易说得空泛实际上差别很大。拿卷三来讲我的建议时间是前15分钟集中做选择题和多选题。这些题目的特点是“会就会、不会就蒙”不值得反复纠结。如果一道题超过2分钟还没方向先按第一印象选上做个标记等所有题目完成后如果有时间再回头。然后立刻跳到手写代码题。这部分需要注意力非常集中而且通常要写较长的代码块。趁脑子清醒时把它们解决掉后面就能从容很多。之后做场景设计题。这类题需要写一定量的文字描述这时候可以用代码题节省出来的时间好好组织语言。最后留10到15分钟给开放性论述题和检查。开放性论述题不需要长篇大论重要的是结构清晰。一个更具体的建议是拿到试卷先花3分钟通读一遍了解题目分布标出自己最有把握的题和最没把握的题。这样做的好处是能建立心理预期避免在翻车题上死磕而影响后面的节奏。个人还有一个小习惯在正式作答前我会在草稿纸上把大题的答题要点写成一个“三行提纲”比如设计题的“需求、方案对比、风险兜底”手写代码题的“边界条件、数据结构、主流程”。确定提纲之后再动笔答案会更有逻辑也不会写着写着跑偏。7. 一些容易被忽略但影响得分的小细节抛开具体题目笔试中还有很多细节会悄悄影响最终得分。这些点往往不在评分标准里但真实存在。第一代码风格的一致性。缩进、变量命名、分号统一这些看起来“无所谓”的东西在阅卷人眼里代表的是工程素养。一个代码缩进混乱但功能正确的答案和一个结构清晰的答案在主观分上会有明显差距。建议平时写代码时就养成好习惯而不是只在考试时“表演”。第二是否主动写出思路过程。很多同学写手写题上来就直接写代码没有一句解释。但面试官看卷子时更想知道你是怎么想的。如果能在代码前面加两行注释比如“本方案使用Map双向链表get和put均为O(1)”传递的信息量会大得多。第三对待边界情况的态度。题目里没说“输入保证有效”就默认可能有边界情况。你在代码里处理负数、空值、极值面试官并不会觉得你啰嗦反而会觉得你经验丰富。第四适当留白。如果时间不够用不要在某一题上死磕。写一个“思路正确但不完整”的答案远好过在一题上花了40分钟导致后面全空着。很多笔试的评分逻辑是“看到正确思路就给大部分分”空着才是零分。第五答案里不要写“我不会但我觉得”。要么认真答要么写思路不要用模棱两可的话凑字数。这种答案会让阅卷人觉得你基本功不扎实连一个清晰的立场都没有。我自己在校招季的模拟笔试里曾经因为没写注释、没处理边界条件被面试官在评价里点名“代码风格不够干净”。从那以后我开始养成“写完代码回头检查边界条件”的习惯确实在后续的笔试中减少了很多不必要的失分。小红书2020校招前端笔试题卷三整体不算特别难但它很考验一个人能不能把“学过的知识”转化成“解决问题的方案”。如果你现在还在刷题阶段不妨先把这份复盘里的几个重点模块过一遍再去找类似风格的模拟题练手。准备面试这件事最终比的是谁能更早地从一个“背答案的人”变成一个“有解决方案的人”。
返回列表