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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:腾讯CSIG
🕐面试时间:近期
💻面试岗位:前端一面
📝面试体验:实习拷打,八股追问,手撕很难,反问没有,第二天挂了
❓面试问题:
八股拷打
- 流式输出的原理
- 为什么用SSE不用axios,他们在http上有什么区别
- 讲讲http1,1.1,2,3,他们是为了做什么优化才更新的
- 埋点的意义
- vue的计算属性的原理
- 有了解过浏览器安全的知识吗
- XSS是什么知道吗
- UDP是怎么保证通讯可靠
- http请求的流程知道吗
- 有用框架写过东西吗
- 现在工作中你写逻辑还是样式
- 流式输出前端怎么样从后台拿到数据
- 有没有什么做的不好的地方
- JS事件循环了解过吗?优点缺点?
- 为什么你说AI对你埋点的帮助不大
- 有用过Vux这种的,什么场景下需要他
- 你说到面包屑,面包屑只是弄个数组存起来然后拿出来用吗?他的难点在哪
- 虚拟列表的原理,有没有别的方案
- 你说的insectionObserver是什么,他怎么实现虚拟列表
- 你觉得最考验你的部分是什么
- JS闭包了解过吗?他是为了解决什么问题存在的?
手撕
- 高性能Top K频繁子串查询系统
💡 木木有话说(刷前先看)
腾讯CSIG这场一面,对于实习来说应该难度不小了,特别是手撕环节,人在紧张的时候如果没有对应训练的话,现想现实现,挂人的概率比较大(其实如果实在写不出来,也可以把思路告诉面试官!!!一定记住)。这份面经提醒我们:大厂面试,基础不牢,地动山摇。
📝 腾讯CSIG前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 硬核八股型 + 深度追问型 + 实战手撕型 |
| 难度评级 | ⭐⭐⭐⭐(四星半,基础深度+算法难度高) |
| 考察重心 | 网络协议(HTTP/SSE)、流式原理、浏览器安全、Vue原理、事件循环、高频算法 |
| 特殊之处 | 手撕"Top K频繁子串"难度很高,反问环节都没有 |
🔍 逐题深度解析
一、流式输出的原理
回答思路:流式输出是指服务端分块发送数据,客户端逐步接收和渲染。
原理:
- 服务端设置响应头
Content-Type: text/event-stream或Transfer-Encoding: chunked - 客户端使用
EventSource或fetch+ReadableStream接收 - 数据以块(chunk)形式传输,每收到一块就立即处理,不等全部数据
- 适用于大文件下载、AI对话、实时日志
javascript
// 客户端流式接收
const response = await fetch('/api/stream')
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
console.log('收到数据块:', chunk)
}
二、为什么用SSE不用axios?他们在HTTP上有什么区别?
回答思路:axios是基于Promise的HTTP客户端,默认等待完整响应;SSE是服务端推送协议。
核心区别:
- axios:请求-响应模式,一次请求返回完整数据,不支持流式
- SSE:建立持久连接,服务端可多次推送数据,客户端逐块接收
HTTP层面区别:
- axios使用标准HTTP请求,响应完成后连接关闭(或Keep-Alive复用)
- SSE使用
text/event-stream类型,连接保持打开,服务端持续写入数据
为什么AI场景用SSE:AI生成需要逐字输出,SSE支持流式,axios只能等全部生成完再返回。
三、讲讲http1,1.1,2,3,他们是为了做什么优化才更新的
回答思路:每个版本的更新都是为了解决前一个版本的性能瓶颈。
| 版本 | 核心特性 | 解决的问题 | 遗留问题 |
|---|---|---|---|
| HTTP/1.0 | 短连接,每次请求建立TCP | 基础通信 | 连接开销大 |
| HTTP/1.1 | 长连接(Keep-Alive)、管道化、host头 | 减少连接开销 | 队头阻塞 |
| HTTP/2 | 多路复用、头部压缩、服务器推送、二进制分帧 | 解决队头阻塞、减少头部冗余 | TCP队头阻塞 |
| HTTP/3 | 基于UDP(QUIC)、0-RTT连接、独立流 | 解决TCP队头阻塞、减少握手延迟 | 部署较新 |
关键演进:1.0→1.1解决连接复用;1.1→2解决并发请求;2→3解决TCP队头阻塞。
四、埋点的意义
回答思路:埋点是数据采集的基础,用于产品分析、性能监控、用户行为追踪。
意义:
- 产品决策:分析用户行为(点击、停留、转化),指导产品迭代
- 性能监控:采集页面加载时间、接口响应时间、错误率
- 用户画像:收集用户偏好、使用习惯
- A/B测试:对比不同方案的效果
- 异常告警:发现JS错误、接口异常
javascript
// 埋点示例
track('button_click', {
button_name: 'submit',
page: 'checkout',
user_id: '123'
})
五、Vue计算属性的原理
回答思路:计算属性是基于响应式依赖缓存的getter函数。
原理:
- 计算属性本质是一个
computedwatcher - 首次访问时执行getter,收集依赖(响应式数据)
- 依赖变化时,标记dirty,但不立即重新计算
- 再次访问计算属性时,若dirty则重新计算并缓存结果,否则返回缓存值
特点:
- 缓存:依赖不变时不重新计算,性能好
- 懒计算:只有被使用时才计算
- 响应式:依赖变化时自动更新
javascript
// 简化版实现
function computed(getter) {
let value
let dirty = true
const effect = () => {
dirty = true
}
// 收集依赖
track(getter, effect)
return {
get value() {
if (dirty) {
value = getter()
dirty = false
}
return value
}
}
}
六、浏览器安全知识
回答思路:常见Web安全攻击及防御。
主要攻击类型:
- XSS(跨站脚本攻击):注入恶意脚本
- CSRF(跨站请求伪造):伪造用户请求
- 点击劫持:透明iframe诱导点击
- SQL注入:输入恶意SQL语句
防御措施:
- 输入校验、输出转义
- CSP(内容安全策略)
- CSRF Token、SameSite Cookie
- X-Frame-Options
七、XSS是什么
回答思路:XSS(Cross-Site Scripting)是攻击者注入恶意脚本到网页中。
类型:
- 反射型:恶意脚本通过URL参数传入,服务端反射回页面
- 存储型:恶意脚本存储到数据库,访问页面时执行
- DOM型:通过修改DOM动态执行
防御:
- 对用户输入进行转义(
<→<) - 使用CSP限制脚本来源
- 设置
HttpOnlyCookie,防止JS读取
八、UDP是怎么保证通讯可靠
回答思路:UDP本身不保证可靠,可靠传输需要上层实现(如QUIC、RUDP)。
UDP特点:无连接、不可靠、无拥塞控制、无顺序保证。
上层实现可靠性的方法:
- ACK确认:接收方确认收到
- 超时重传:发送方超时未收到ACK则重传
- 序列号:解决乱序和重复
- 流量控制:动态调整发送速率
应用层协议:QUIC(HTTP/3底层)、WebRTC、KCP。
九、HTTP请求流程
回答思路:从URL输入到页面渲染的完整链路。
- DNS解析:域名 → IP地址
- TCP连接:三次握手
- TLS握手(HTTPS):协商加密密钥
- 发送请求:构建HTTP请求报文
- 服务端处理:接收请求,处理逻辑
- 返回响应:返回HTTP响应报文
- 浏览器解析:解析HTML、CSS、JS
- 渲染页面:构建DOM树、渲染树、布局、绘制
- TCP关闭:四次挥手
十、流式输出前端如何从后台拿到数据
回答思路:两种主流方式。
方式一:EventSource(SSE)
javascript
const source = new EventSource('/api/stream')
source.onmessage = (e) => {
console.log(JSON.parse(e.data))
}
方式二:Fetch + ReadableStream
javascript
const response = await fetch('/api/stream')
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
// 处理chunk
}
十一、JS事件循环的优点和缺点
回答思路:事件循环是JS异步模型的实现机制。
优点:
- 非阻塞:异步操作不阻塞主线程,UI保持响应
- 简单:单线程模型,无需处理锁、竞态条件
- 适合I/O密集型:网络请求、文件读写等
缺点:
- CPU密集型任务会阻塞:大量计算会导致页面卡死
- 任务优先级不精细:微任务可能长时间占用主线程
- 无法利用多核:需要Web Worker辅助
十二、Vuex(Vux)的使用场景
回答思路:Vuex是Vue的状态管理库,用于管理全局共享状态。
适用场景:
- 多个组件共享同一状态(如用户信息、购物车)
- 跨页面/跨组件的数据通信
- 需要持久化的状态(配合localStorage)
- 复杂的状态变更逻辑(actions + mutations)
不适用场景:简单的父子组件通信、独立组件内部状态。
十三、面包屑的难点
回答思路:用户质疑"面包屑只是数组存储吗?",说明面试官期望更深的理解。
难点:
- 动态路由:路由参数变化时,面包屑需要响应更新
- 嵌套路由:多级路由层级如何正确映射到面包屑
- 权限控制:某些路由用户无权限访问,面包屑应跳过
- 国际化:面包屑文本需要支持多语言
- 性能:高频路由切换时,面包屑计算不能拖慢页面
javascript
// 动态生成面包屑
const breadcrumbs = computed(() => {
const matched = route.matched
return matched
.filter(record => record.meta?.breadcrumb !== false)
.map(record => ({
name: t(record.meta?.title),
path: record.path
}))
})
十四、虚拟列表的原理及其他方案
回答思路:虚拟列表只渲染可视区域的DOM节点。
原理:
- 计算可视区域能容纳的节点数(
visibleCount = containerHeight / itemHeight) - 监听滚动,计算
startIndex = Math.floor(scrollTop / itemHeight) - 只渲染
startIndex到startIndex + visibleCount的节点 - 用占位撑高滚动条
其他方案:
- 分页加载:适合滚动触底加载
- 时间分片 :分批渲染节点(
requestIdleCallback) - Canvas渲染:极高性能要求场景
十五、IntersectionObserver如何实现虚拟列表
回答思路:IntersectionObserver监听元素是否进入可视区。
实现思路:
- 创建哨兵元素放在列表开头和结尾
- 使用IntersectionObserver监听哨兵
- 当顶部哨兵进入可视区,加载上一页数据
- 当底部哨兵进入可视区,加载下一页数据
javascript
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (entry.target.id === 'bottom-sentinel') {
loadMore()
}
}
})
})
observer.observe(bottomSentinel)
与滚动监听的比较:IntersectionObserver性能更好,不占用主线程。
十六、JS闭包及解决的问题
回答思路:参考之前面经。
闭包定义:函数可以访问其外部作用域的变量,即使外部函数已执行完毕。
解决的问题:
- 封装私有变量:模块模式,避免全局污染
- 保存状态:防抖、节流中保存timer
- 回调函数:事件处理中保存引用
- 函数工厂:根据参数生成特定功能的函数
javascript
// 封装私有变量
function createCounter() {
let count = 0
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
}
}
十七、手撕:高性能Top K频繁子串查询系统
题目:给定一个字符串,找出出现频率最高的K个子串(长度可指定或可变)。
解题思路:
- 滑动窗口枚举所有子串
- 使用哈希表统计频率
- 使用最小堆维护Top K
javascript
function topKFrequentSubstrings(s, k, minLen = 1, maxLen = 10) {
const freq = new Map()
// 枚举所有子串
for (let len = minLen; len <= maxLen; len++) {
for (let i = 0; i <= s.length - len; i++) {
const sub = s.substring(i, i + len)
freq.set(sub, (freq.get(sub) || 0) + 1)
}
}
// 使用最小堆维护Top K
const heap = []
for (const [sub, count] of freq) {
if (heap.length < k) {
heap.push({ sub, count })
heap.sort((a, b) => a.count - b.count)
} else if (count > heap[0].count) {
heap[0] = { sub, count }
heap.sort((a, b) => a.count - b.count)
}
}
return heap.sort((a, b) => b.count - a.count)
}
优化方向:
- 使用后缀数组 + 最长公共前缀(LCP)优化
- 使用字典树(Trie)统计
- 使用滚动哈希(Rabin-Karp)避免重复计算
复杂度:暴力枚举O(n²),优化后可达O(n log n)。
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 流式输出 | 分块传输、ReadableStream、SSE |
| SSE vs axios | SSE长连接流式、axios请求-响应 |
| HTTP版本演进 | 1.1长连接、2多路复用、3基于UDP |
| 埋点 | 行为分析、性能监控、异常告警 |
| 计算属性 | 缓存、懒计算、依赖收集 |
| XSS | 反射型/存储型/DOM型,转义+CSP防御 |
| UDP可靠性 | 上层实现(ACK、重传、序列号) |
| 事件循环 | 优点:非阻塞;缺点:CPU任务阻塞 |
| 虚拟列表 | 可视区渲染、IntersectionObserver、分页 |
| 闭包 | 私有变量、状态保存、函数工厂 |
| Top K子串 | 滑动窗口+哈希+最小堆,后缀数组优化 |
📌 最后一句:
腾讯CSIG这场一面,是一份"硬核基础+高难度算法"的面经。从流式输出原理到HTTP版本演进,从UDP可靠性到Top K子串查询,面试官几乎问遍了网络、安全、框架、算法等核心领域。用户反馈"手撕也没整会",第二天就挂了。这提醒我们:大厂面试,基础八股要深挖到原理层面,手撕算法要练到中高难度。只有准备到"超标",才能从容应对。