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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:快手
🕐面试时间:近期
💻面试岗位:前端一面
❓面试问题:
- 介绍React的hooks(useMemo、useCallback、useEffect、useLayoutEffect)
- 如何用React hooks模拟生命周期?比如组件初始化时的初始化工作怎么做?
- 多人协作开发时,代码管理的流程和冲突解决方式是怎样的?
- 做过的项目中如何做移动端适配?
- 有没有了解过iOS/安卓端的适配差异?
- 项目中做了哪些性能优化?优化的思路是什么?
- 无限层级文件夹管理的懒加载是纯前端渲染控制,还是结合后端接口?树结构的确定时机是怎样的?
- 项目的用户认证是如何实现的?
- 实时文本编辑的同步策略是什么?断网时的内容如何处理?
- WebSocket断联后的重连策略是怎样的?
- 了解过SSE吗?如果用SSE替换项目中的连接机制,该如何实现?
- 项目的权限设计是怎样的(发布者/接单员的权限隔离)?
- 复杂表单的状态管理与校验如何实现?新增动态表单输入项的流程是怎样的?
- 有效的括号(hot100原题 简单)
来源:牛客网 大学路滑冰黄果
💡 木木有话说(刷前先看)
快手这场一面,也是典型的AI+传统前端面,这些题目大部分我们之前面经已经遇到过了。AI相关的题型比较固定,我们引导文的面经已经可以覆盖80%内容了
📝 快手前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 项目实战型 + 场景追问型 + 工程落地型 |
| 难度评级 | ⭐⭐⭐(三到四星,项目细节深,场景真实) |
| 考察重心 | React Hooks、移动端适配、性能优化、实时同步、WebSocket、权限设计、表单处理 |
| 特殊之处 | 问题全部围绕真实项目展开,考察"做过什么"而非"知道什么" |
🔍 逐题深度解析
一、介绍React Hooks
回答思路:分别说明每个Hook的用途、使用场景、注意事项。
| Hook | 用途 | 使用场景 | 注意事项 |
|---|---|---|---|
| useState | 声明状态变量 | 组件内需要变化的数据 | 状态更新是异步的,合并更新 |
| useEffect | 处理副作用 | 数据获取、订阅、DOM操作 | 依赖数组控制执行时机,清理函数防止内存泄漏 |
| useLayoutEffect | 同步副作用 | 需要在DOM更新后、浏览器绘制前执行的操作(如测量DOM尺寸) | 会阻塞渲染,谨慎使用 |
| useMemo | 缓存计算结果 | 昂贵的计算、保持对象引用稳定 | 依赖不变时不重新计算 |
| useCallback | 缓存函数引用 | 传递给子组件的回调、useEffect依赖 | 配合React.memo使用优化性能 |
| useRef | 存储可变值 | DOM引用、存储不触发渲染的变量 | .current修改不会触发重渲染 |
javascript
// useMemo:缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(a, b)
}, [a, b])
// useCallback:缓存函数引用
const handleClick = useCallback(() => {
doSomething(a)
}, [a])
// useEffect:处理副作用
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => clearInterval(timer) // 清理
}, [])
// useLayoutEffect:同步执行
useLayoutEffect(() => {
const height = divRef.current.offsetHeight // 在绘制前获取高度
setHeight(height)
}, [])
二、用React Hooks模拟生命周期
回答思路:函数组件没有生命周期概念,但可以用Hooks实现类似效果。
javascript
// componentDidMount(组件挂载时执行一次)
useEffect(() => {
console.log('组件挂载')
fetchInitialData()
// 初始化工作:请求数据、订阅事件、设置定时器
}, []) // 空依赖数组
// componentDidUpdate(依赖变化时执行)
useEffect(() => {
console.log('count变化了', count)
// 依赖count变化时的操作
}, [count])
// componentWillUnmount(组件卸载时清理)
useEffect(() => {
const subscription = subscribe()
return () => {
subscription.unsubscribe() // 清理工作
}
}, [])
// 自定义Hook:封装初始化逻辑
function useMount(callback) {
useEffect(() => {
callback()
}, [])
}
三、多人协作:代码管理流程与冲突解决
回答思路:基于Git的工作流。
推荐流程:
- 功能分支 :从
main/develop切出功能分支(feature/xxx) - 本地开发 :commit时使用规范(
feat:/fix:/docs:) - 拉取最新 :push前先
git pull --rebase,保持线性历史 - 代码评审:提MR/PR,至少一人Review通过后合并
- 冲突解决 :出现冲突时,手动编辑文件 →
git add→git rebase --continue
冲突解决步骤:
bash
git fetch origin
git rebase origin/main
# 冲突提示,手动编辑冲突文件(删除<<<<<<< ======= >>>>>>>标记)
git add .
git rebase --continue
git push origin feature/xxx --force-with-lease
预防策略:
- 小批量、高频次提交
- 避免同时修改同一文件的同一区域
- 使用
.gitattribute标记锁定的文件(如package-lock.json)
四、移动端适配方案
回答思路:从视口、单位、布局、交互四个方面说明。
核心方案:
- 视口设置 :
<meta name="viewport" content="width=device-width, initial-scale=1"> - REM适配:设置根字体大小,所有尺寸用rem,配合postcss-pxtorem自动转换
- Flex/Grid布局:弹性布局适应不同屏幕
- 1px边框问题 :
transform: scale(0.5)或viewport+initial-scale=0.5 - 适配库:lib-flexible(已过时)、amfe-flexible + postcss-pxtorem
javascript
// 动态设置根字体(750px设计稿)
function setRootFontSize() {
const width = document.documentElement.clientWidth
const fontSize = (width / 750) * 100
document.documentElement.style.fontSize = `${fontSize}px`
}
window.addEventListener('resize', setRootFontSize)
五、iOS/安卓端适配差异
回答思路:从视觉、交互、性能三个维度说明。
| 差异点 | iOS | 安卓 |
|---|---|---|
| 滚动回弹 | 有弹性效果 | 无统一行为 |
| 日期选择器 | 滚轮式 | 不同厂商样式各异 |
| 键盘弹起 | 页面滚动到输入框 | 可能遮挡输入框 |
| 点击延迟 | 无(300ms已消除) | 部分浏览器仍有 |
| 字体渲染 | 系统字体统一 | 各厂商字体不同 |
| 圆角/阴影 | 支持好 | 低版本需兼容 |
解决方案:
- 使用
-webkit-overflow-scrolling: touch统一滚动 - 使用
inputmode属性控制键盘类型 - 监听
resize事件处理键盘遮挡 - 使用
postcss自动添加浏览器前缀
六、项目性能优化
回答思路:参考之前面经,从加载、渲染、运行时多维度说明。
优化方向:
- 代码分割:路由懒加载、动态import
- 图片优化:WebP格式、懒加载、响应式图片
- 缓存策略:强缓存、协商缓存、CDN
- 虚拟滚动:长列表优化(react-window)
- 防抖节流:高频事件优化
- 避免重渲染:React.memo、useMemo、useCallback
- 首屏优化:关键CSS内联、骨架屏、SSR
七、无限层级文件夹懒加载
回答思路:结合前端渲染和后端接口。
实现方式:
- 展开时请求:用户点击展开文件夹时,请求该文件夹的子节点数据
- 后端返回 :
{ id, name, type, childrenCount, hasChildren } - 前端状态:维护树形结构数据,已加载的节点缓存children
- 确定时机:树结构由后端返回的父子关系确定,前端负责渲染和交互
javascript
// 懒加载逻辑
async function loadChildren(nodeId) {
if (cachedChildren[nodeId]) return cachedChildren[nodeId]
const children = await fetch(`/api/folder/${nodeId}/children`)
cachedChildren[nodeId] = children
return children
}
// 树节点点击处理
function onNodeExpand(node) {
if (!node.children && node.hasChildren) {
const children = await loadChildren(node.id)
node.children = children
}
}
八、用户认证实现
回答思路:参考之前面经的双token方案。
流程:
- 登录:账号密码 → 后端验证 → 返回access_token + refresh_token
- 存储:access_token在内存,refresh_token在httpOnly cookie
- 请求:
Authorization: Bearer <access_token> - 过期:401 → 调用刷新接口 → 重试原请求
- 登出:清除本地token,跳转登录页
九、实时文本编辑的同步策略与断网处理
回答思路:协同编辑的核心是OT(操作转换)或CRDT算法。
同步策略:
- WebSocket:实时推送编辑操作
- 操作转换:每个编辑操作(insert/delete)转成op,服务端负责合并冲突
- 版本控制:每次编辑带版本号,服务端检测冲突
断网处理:
- 本地持久化:断网期间的编辑存到IndexedDB
- 乐观更新:先更新UI,再同步服务端
- 冲突处理:恢复网络后,上传本地op,服务端合并冲突
javascript
// 离线编辑队列
const offlineQueue = []
function applyEdit(edit) {
// 立即更新UI
updateEditor(edit)
if (navigator.onLine) {
sendEdit(edit)
} else {
offlineQueue.push(edit)
saveToIndexedDB(edit)
}
}
window.addEventListener('online', () => {
while (offlineQueue.length) {
sendEdit(offlineQueue.shift())
}
})
十、WebSocket断联重连策略
回答思路:参考之前面经的重连机制。
策略:
- 监听关闭事件 :
socket.onclose触发重连 - 指数退避:重连间隔1s→2s→4s→...最大30s
- 心跳保活:定时发送ping,超时未pong则主动重连
- 状态恢复:重连后重新订阅房间/会话
javascript
class WebSocketManager {
constructor(url) {
this.url = url
this.retryCount = 0
this.maxRetries = 10
this.reconnect()
}
reconnect() {
this.socket = new WebSocket(this.url)
this.socket.onclose = () => {
const delay = Math.min(1000 * Math.pow(2, this.retryCount), 30000)
setTimeout(() => {
this.retryCount++
this.reconnect()
}, delay)
}
this.socket.onopen = () => {
this.retryCount = 0
}
}
}
十一、SSE替换WebSocket的实现
回答思路:如果业务是单向推送(服务端→客户端),SSE是更简单的选择。
实现方案:
javascript
// SSE客户端
function connectSSE() {
const source = new EventSource('/api/events')
source.onmessage = (event) => {
const data = JSON.parse(event.data)
handleMessage(data)
}
source.onerror = () => {
source.close()
setTimeout(() => connectSSE(), 3000) // 重连
}
return source
}
// 如果需要双向(客户端发送消息),仍需配合HTTP POST
async function sendMessage(content) {
await fetch('/api/message', {
method: 'POST',
body: JSON.stringify({ content })
})
}
适用场景:实时通知、AI对话、股票行情、日志推送。
十二、权限设计(发布者/接单员隔离)
回答思路:RBAC(基于角色的访问控制)。
设计:
- 角色:发布者、接单员
- 权限:发布者可创建任务、查看自己发布的任务;接单员可查看任务列表、接单
- 前端:根据角色渲染不同UI,路由守卫拦截未授权访问
- 后端:每个接口校验角色权限
javascript
// 前端权限控制
const permissions = {
publisher: ['create_task', 'view_my_tasks'],
receiver: ['view_tasks', 'accept_task']
}
function hasPermission(userRole, action) {
return permissions[userRole]?.includes(action)
}
// 路由守卫
router.beforeEach((to, from, next) => {
const requiredRole = to.meta.role
if (requiredRole && userRole !== requiredRole) {
next('/unauthorized')
} else {
next()
}
})
十三、复杂表单状态管理与校验
回答思路:表单状态管理 + 动态表单项。
状态管理方案:
- 小型表单 :
useState+ 手动校验 - 中型表单 :
useReducer+ 校验库(如Zod、Yup) - 大型表单:Formily、React Hook Form
动态表单项流程:
javascript
function DynamicForm() {
const [fields, setFields] = useState([{ id: 1, value: '' }])
// 添加表单项
const addField = () => {
setFields([...fields, { id: Date.now(), value: '' }])
}
// 删除表单项
const removeField = (id) => {
setFields(fields.filter(f => f.id !== id))
}
// 校验
const validate = () => {
const errors = fields.map(f => {
if (!f.value) return '不能为空'
return null
})
return errors.every(e => e === null)
}
return (
<form>
{fields.map(field => (
<div key={field.id}>
<input value={field.value} onChange={...} />
<button onClick={() => removeField(field.id)}>删除</button>
</div>
))}
<button onClick={addField}>添加</button>
</form>
)
}
十四、有效的括号
题目 :判断括号字符串是否合法({}、[]、())。
javascript
function isValid(s) {
const stack = []
const pairs = {
'(': ')',
'[': ']',
'{': '}'
}
for (const char of s) {
if (pairs[char]) {
stack.push(char)
} else {
const last = stack.pop()
if (pairs[last] !== char) return false
}
}
return stack.length === 0
}
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| React Hooks | useEffect/useLayoutEffect执行时机、useMemo/useCallback缓存、useRef不触发渲染 |
| 模拟生命周期 | useEffect空依赖(挂载)、返回清理(卸载)、依赖数组(更新) |
| Git协作 | 功能分支、rebase保持线性、冲突手动解决 |
| 移动端适配 | viewport、REM、Flex/Grid、1px边框 |
| 端差异 | iOS弹性滚动、安卓键盘遮挡、字体渲染 |
| 性能优化 | 代码分割、图片优化、虚拟滚动、缓存 |
| 懒加载 | 展开时请求、缓存已加载节点 |
| 用户认证 | 双token、401刷新、无感体验 |
| 实时编辑 | OT/CRDT、乐观更新、离线队列、冲突合并 |
| WebSocket重连 | 指数退避、心跳保活、状态恢复 |
| SSE | 单向推送、EventSource、配合POST发送 |
| 权限设计 | RBAC、角色→权限映射、路由守卫 |
| 动态表单 | 状态数组、增删表单项、实时校验 |
| 有效括号 | 栈结构、括号匹配 |
📌 最后一句:
快手这场一面,最大的特点是"接地气"。每个问题都来自真实业务:无限层级文件夹、实时协同编辑、WebSocket断线重连、动态表单校验......面试官想听的不是标准答案,而是你在真实项目中是怎么做的、遇到了什么问题、怎么解决的。