前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:快手
🕐面试时间:近期,用户上传于2026-03-29
💻面试岗位:前端一面
⏱️面试时长:未提及
📝面试体验:挂了
❓面试问题:
- 面试官自我介绍
- 自我介绍一下
- 通信模块WebSocket有遇到什么难点或亮点
- 心跳包字段具体是怎么设计的?
- 聊天室场景实现了哪些消息体?
- 有实现撤回功能吗?撤回是怎么做的?撤回请求是 HTTP 还是 WebSocket?
- 消息的长列表有遇到什么性能问题吗?做了什么优化吗?
- 虚拟列表在什么时候才会有正向收益?
- 虚拟列表的原理,虚拟列表为什么能优化性能?
- Vue 和 React 的响应式原理,优缺点
- Proxy 相对 Object.defineProperty 的优点
- Proxy 的局限性
- 什么是闭包,闭包的作用和危害?
- JS 的原型链和事件循环
- 什么是异步?
- 获取 LocalStorage、浏览器 URL 的参数算异步吗?
- requestAnimationFrame 属于微任务还是宏任务?
- 聊一下你认知中的 CSS 移动端适配手段
- Flex 布局
- 算法:无重复字符的最长子串
来源:牛客网 一曝十寒P
💡 木木有话说(刷前先看)
快手这场一面,传统八股偏多,整体我个人认为难度不大,但是对于实习的小伙伴们如果没有相关项目经验可能会在压力面下比较痛苦。因为不理解场景,这个需要积累。
📝 快手前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 项目深挖型 + 基础全面型 + 实战追问型 |
| 难度评级 | ⭐⭐⭐(三星半,项目细节追问深,基础覆盖面广) |
| 考察重心 | WebSocket实战细节、长列表性能优化、响应式原理、JS核心、CSS适配 |
| 特殊之处 | 从项目细节(心跳字段、撤回方式)一路追问到原理层面,考察"是否真的做过" |
🔍 逐题深度解析
一、面试官自我介绍
应对思路:认真听,记住面试官的姓名和所在团队,后面回答问题时可以自然呼应(如"就像您刚才提到的XX团队的业务场景...")。这是基本的职业素养。
二、自我介绍一下
回答思路:结构化表达,控制在2-3分钟。
建议结构:
- 基本信息:学校/年级/专业
- 技术栈:熟悉什么框架/工具,程度如何
- 项目经历 :挑1-2个最有代表性的,说清楚你做了什么 + 解决了什么难点 + 取得了什么成果
- 实习/比赛经历(如有)
- 为什么适合这个岗位:简短总结
注意:不要背简历,要挑重点,引导面试官对你感兴趣的方向提问。
三、WebSocket遇到的难点或亮点
回答思路:这是开放式问题,需要结合项目实际。可以讲"连接稳定性""心跳保活""消息可靠性"等。
常见难点:
- 断线重连:网络切换、服务端重启时如何自动重连,重连后如何恢复状态
- 心跳保活:如何设计心跳机制,检测假连接
- 消息可靠性:如何保证消息不丢、不重复
- 消息顺序:并发消息如何保证顺序
- 扩展性:单机WebSocket如何支撑大规模连接
示例回答 :
"我在聊天室项目中,最大的难点是连接稳定性。移动端网络切换频繁,WebSocket容易断开。我的解决方案是:
- 心跳机制:客户端每30秒发送ping,服务端响应pong,连续3次无响应则主动重连
- 指数退避重连:重连间隔从1s开始,每次翻倍,最大30s,避免频繁请求
- 消息队列:断线期间的消息先存在本地IndexedDB,重连后批量发送
- 会话恢复:重连时带上lastMessageId,服务端补发未收到的消息
这个方案上线后,连接成功率从92%提升到99.5%。"
四、心跳包字段具体是怎么设计的
回答思路:上一题的细化,考察设计细节。
心跳包结构示例:
json
{
"type": "ping", // 消息类型:ping/pong
"timestamp": 1701234567890, // 客户端时间戳
"seq": 12345 // 序列号,用于匹配请求和响应
}
设计要点:
- 双向心跳:客户端发ping,服务端回pong;也可以服务端主动发ping
- 超时判断:客户端记录发送时间,超过阈值未收到pong则判定连接异常
- 序列号:匹配请求和响应,避免延迟响应误判
- 携带额外信息:可在ping中携带客户端状态(如当前房间ID),让服务端感知
五、聊天室场景实现了哪些消息体
回答思路:列举实际实现的消息类型。
常见消息体:
json
// 文本消息
{ "type": "text", "content": "hello", "sender": "uid", "timestamp": 123 }
// 图片消息
{ "type": "image", "url": "https://...", "width": 100, "height": 80 }
// 系统通知
{ "type": "system", "content": "用户xxx进入房间" }
// 撤回消息
{ "type": "recall", "messageId": "msg_123", "recalledBy": "uid" }
// 已读回执
{ "type": "read_receipt", "messageId": "msg_123", "reader": "uid" }
// 状态同步(正在输入)
{ "type": "typing", "sender": "uid", "isTyping": true }
设计考虑 :消息体需要包含id(唯一标识)、sender、timestamp、type等公共字段,便于统一处理。
六、撤回功能的实现与请求方式
回答思路:撤回涉及权限校验、时效性、同步更新。
撤回流程:
- 用户点击撤回 → 前端发送撤回请求
- 服务端校验:是否有权限(本人或管理员)、是否在撤回时限内(如2分钟)
- 服务端修改消息状态,广播撤回通知给所有在线用户
- 各客户端收到通知,将本地消息标记为"已撤回"
撤回请求用HTTP还是WebSocket?
- 推荐用WebSocket:因为撤回需要实时通知其他在线用户,通过WebSocket可以直接广播
- 如果用HTTP,则需要服务端收到HTTP请求后再通过WebSocket推送撤回通知,多一次转发
示例:
javascript
// 通过WebSocket发送撤回指令
socket.send(JSON.stringify({
type: 'recall',
messageId: 'msg_123',
roomId: 'room_456'
}))
七、长列表性能问题及优化
回答思路:结合聊天场景,讲实际遇到的问题和优化方案。
问题:
- 消息数量增多(几百上千条),DOM节点过多导致内存占用高、滚动卡顿
- 初次渲染慢
- 频繁更新(如撤回、已读状态)导致重绘
优化方案:
- 虚拟滚动:只渲染可视区域的消息
- 消息合并:连续的同类型消息(如同一个人连续发言)合并显示
- 懒加载:历史消息滚动到顶部时再加载
- 使用
requestAnimationFrame:批量更新DOM - 消息复用 :使用
React.memo或Vue的v-once避免不必要的重渲染
八、虚拟列表在什么时候有正向收益
回答思路:不是所有场景都适合虚拟列表,面试官想考察你是否有量化思维。
正向收益的条件:
- 列表项数量 > 100(具体阈值取决于设备性能)
- 列表项高度相对固定(可变高度会增加复杂度,收益降低)
- 滚动频繁(如聊天室、无限滚动)
没有收益甚至负收益的场景:
- 列表项很少(<50),虚拟列表的占位计算、动态渲染反而增加开销
- 每个列表项高度差异极大且需要实时测量
- 列表项内容频繁变化(如实时数据流),虚拟列表的缓存策略可能不适用
九、虚拟列表的原理与性能优化原因
回答思路:解释核心机制,说明为什么能优化性能。
原理:
- 计算可视区域能容纳多少个列表项(
visibleCount = containerHeight / itemHeight) - 监听滚动事件,计算当前滚动位置对应的起始索引(
startIndex = Math.floor(scrollTop / itemHeight)) - 只渲染从
startIndex到startIndex + visibleCount + overscanCount的列表项 - 通过
padding或transform占位,保持滚动条正确
为什么能优化性能:
- DOM节点数量恒定:无论数据量多大,DOM节点始终只有可见区域+缓冲区的数量(如20-30个)
- 内存占用降低:节点少,内存占用少
- 重排重绘范围小:每次滚动只更新少量节点,而非整个列表
十、Vue和React响应式原理及优缺点
回答思路:对比两者实现方式和特点。
| 维度 | Vue2 | Vue3 | React |
|---|---|---|---|
| 实现方式 | Object.defineProperty | Proxy | 显式调用setState,immutable |
| 监听粒度 | 属性级 | 属性级 | 组件级 |
| 依赖收集 | 自动(getter) | 自动(Proxy) | 手动(useEffect依赖数组) |
| 新增属性 | 需要Vue.set | 自动响应 | 需要immutable更新 |
| 数组监听 | 重写方法 | 原生支持 | 需要immutable更新 |
| 性能 | 初始化递归遍历,开销大 | 懒递归,初始化快 | 需要手动优化(memo等) |
优缺点:
- Vue:优点------自动依赖收集,写法简单;缺点------defineProperty有局限,Vue2中数组/新增属性处理麻烦
- React:优点------纯函数式,可预测性强,适合大型应用;缺点------需要手动优化避免不必要渲染
十一、Proxy相对Object.defineProperty的优点
回答思路:这是Vue2升级到Vue3的核心原因。
- 监听能力更强:可监听13种操作(get/set/deleteProperty/has等),而defineProperty只能监听get/set
- 新增属性自动响应 :
proxy.newProp = value可直接触发,defineProperty需要Vue.set - 数组变化原生支持:无需重写数组方法
- 删除属性可监听 :
delete proxy.prop可被拦截 - 懒递归:只在访问时才递归代理嵌套对象,初始化性能更好
十二、Proxy的局限性
回答思路:客观分析Proxy的不足。
- 无法处理基本类型:Proxy只能代理对象,基本类型(string/number)需要包装(Vue3中ref的实现)
- 兼容性问题:不支持IE(Vue3放弃IE支持的原因)
- 性能开销:Proxy本身有额外性能开销,但Vue3通过懒递归整体性能优于Vue2
- 无法代理已代理对象:重复代理会返回同一对象,但可能导致预期外的行为
- 调试体验:控制台显示的代理对象不如普通对象直观
十三、闭包的定义、作用与危害
回答思路:先定义,再分点说作用和危害。
定义:函数可以访问其外部作用域的变量,即使外部函数已执行完毕。
作用:
- 封装私有变量(模块模式)
- 函数工厂(生成特定功能的函数)
- 回调函数中保存状态
- 防抖/节流中保存timer
危害:
- 内存泄漏:闭包引用的外部变量不会被垃圾回收,如果DOM元素被闭包引用,可能导致内存泄漏
- 性能:不必要的闭包会增加内存占用
javascript
// 内存泄漏示例
function leak() {
const element = document.getElementById('btn')
element.onclick = () => {
console.log(element.id) // 闭包引用element,即使btn被移除,element也不会被回收
}
}
// 解决方法:置null
function fix() {
const element = document.getElementById('btn')
element.onclick = () => {
console.log(element.id)
}
element = null // 主动释放
}
十四、原型链和事件循环
回答思路:这是两个独立知识点,分别简要说明。
原型链:
- JavaScript中对象通过
__proto__指向其原型,原型也是对象,形成链 - 属性查找沿原型链向上,直到找到或到达
null - 构造函数有
prototype属性,实例的__proto__指向构造函数的prototype - 作用:实现继承和共享方法
事件循环:
- JS单线程,依靠事件循环处理异步
- 执行顺序:同步代码 → 清空微任务 → 执行一个宏任务 → 再清空微任务,循环
- 宏任务:setTimeout、I/O、UI渲染
- 微任务:Promise.then、MutationObserver
十五、什么是异步
回答思路:简洁明了,可举例说明。
定义:异步是指代码不按顺序立即执行,而是先注册回调,等待某个操作完成后(如网络请求、定时器)再执行。
对比同步:同步代码按顺序执行,如果某行耗时久(如读取大文件),会阻塞后续代码执行。
示例:
javascript
console.log('1')
setTimeout(() => console.log('2'), 0) // 异步,不阻塞
console.log('3')
// 输出:1,3,2
十六、获取LocalStorage、URL参数算异步吗
回答思路:考察对异步API的理解。
答案 :不算异步 。localStorage.getItem()和window.location参数获取都是同步操作,立即返回结果,不涉及事件循环。
易混淆点:
- localStorage的读写是同步的,会阻塞主线程
- 而IndexedDB的操作是异步的(Promise)
十七、requestAnimationFrame属于微任务还是宏任务
回答思路:这是一个细节考点。
答案 :宏任务 。但它比较特殊,在渲染前执行,优先级高于setTimeout等普通宏任务。
执行顺序(一帧内):
- 执行宏任务(setTimeout等)
- 执行微任务队列
- requestAnimationFrame回调
- 执行渲染(重绘/重排)
- 执行requestIdleCallback
常见误区:有人误以为它是微任务,其实它是宏任务,只是执行时机在渲染前。
十八、CSS移动端适配手段
回答思路:列举主流方案及其原理。
方案:
- rem适配 :设置根元素字体大小,所有长度用rem。动态设置
<meta name="viewport">+ JS计算根字体(淘宝flexible方案) - vw/vh适配 :直接使用视口单位,
1vw = 1%视口宽度。配合PostCSS自动转换px到vw - 百分比适配:宽度用百分比,高度用固定值,适合简单布局
- 媒体查询:针对不同屏幕尺寸写不同样式(响应式)
- 缩放适配(不推荐):整体缩放页面,会导致字体模糊
现代推荐:vw + 固定比例(高度用vw),或rem + vw混合。
十九、Flex布局
回答思路:简要说明核心概念,不展开所有属性。
核心概念:
- 容器属性 :
display: flex、flex-direction(主轴方向)、justify-content(主轴对齐)、align-items(交叉轴对齐)、flex-wrap(换行) - 项目属性 :
flex-grow(放大比例)、flex-shrink(缩小比例)、flex-basis(基准尺寸) - 经典用法 :水平垂直居中(
justify-content: center; align-items: center)
二十、算法:无重复字符的最长子串
题目:给定一个字符串,找出其中不含有重复字符的最长子串的长度。
解法(滑动窗口):
javascript
function lengthOfLongestSubstring(s) {
let left = 0
let maxLen = 0
const map = new Map() // 存储字符最近出现的位置
for (let right = 0; right < s.length; right++) {
const char = s[right]
if (map.has(char) && map.get(char) >= left) {
// 出现重复,左指针跳到重复字符的下一位
left = map.get(char) + 1
}
map.set(char, right)
maxLen = Math.max(maxLen, right - left + 1)
}
return maxLen
}
复杂度:时间O(n),空间O(min(n, 字符集大小))
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| WebSocket | 心跳保活、断线重连、消息可靠性、撤回实现 |
| 长列表优化 | 虚拟滚动、消息合并、懒加载、收益场景分析 |
| 虚拟列表原理 | 可视区计算、恒定DOM节点、占位技术 |
| 响应式原理 | Vue2(defineProperty) vs Vue3(Proxy) vs React(setState) |
| Proxy | 监听更强、数组支持、懒递归、局限(基本类型/兼容性) |
| 闭包 | 私有变量、内存泄漏、函数工厂 |
| 原型链 | __proto__、prototype、继承 |
| 事件循环 | 宏任务(setTimeout/raf)、微任务(Promise.then)、执行顺序 |
| 异步定义 | 不阻塞主线程、回调机制 |
| 同步API | localStorage、URL参数读取是同步 |
| rAF | 宏任务,渲染前执行 |
| 移动端适配 | rem、vw、百分比、媒体查询 |
| Flex布局 | 容器属性、项目属性、居中用法 |
| 算法 | 滑动窗口,O(n)时间 |
📌 最后一句:
快手这场一面,最大的特点是"追问到底"------从WebSocket心跳字段怎么设计,到撤回用HTTP还是WebSocket,再到虚拟列表什么时候有正向收益。面试官不满足于"我会用",而是要确认你"真的做过、思考过、踩过坑"。挂掉的原因,大概率是在某个细节上没答透,或者算法没写好。