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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容(二面)
📍面试公司:小红书
🕐面试时间:近期,用户上传于2026-03-17
💻面试岗位:前端(二面)
⏱️面试时长:1小时
📝面试体验:二面学到了很多
❓面试问题(二面):
(一)框架(深入拷打)
- Vue2与Vue3的响应式原理差异?
- 为什么Vue3还要进行重写?
- Vue3的依赖收集
- ref 和reactive之间的差异,追问底层
- 为什么 ref 需要 .value,而 reactive 不需要?
- 如何处理 reactive 复杂对象重新赋值?(业务场景,封装函数)
- Vue3 中 Proxy 的核心缺陷?(面试官通过这一点把以上整个思路串起来了)
- Vue数据通信,兄弟组件通信的前提是?(公共父组件)
(二)CSS
-
口述三栏布局如何实现(提到BFC)
-
追问BFC的场景,浮动,margin重叠
(三)工程化
-
git场景实操,两个人代码在不同分支操作现在要在测试环境测试如何操作
-
追问,冲突解决,Merge过程想要终止git命令是什么?rebase呢?
-
口述当前面试的赛码网如何拆分组件?
-
赛码网对于用户信息这种组件可能都能使用的数据如何处理?
-
赛码网代码编辑器的不同tab栏思路,不同的tab都是一个组件吗?
(四)手撕
-
事件循环代码输出题
-
手撕(未透露题目)
💡 木木有话说(刷前先看)
这个up的面经还是比较值得参考的,拆分成上下文发出来。二面我觉得八股内容还是比较多的,比较有意思的是围绕Vue3 中 Proxy 的核心缺陷的一系列八股,面试官还是有一定引导性技巧在的。
📝 小红书前端二面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 原理深入型 + 场景追问型 + 实战推演型 |
| 难度评级 | ⭐⭐⭐⭐(四星,Vue原理层层剥开) |
| 考察重心 | Vue3响应式原理、Proxy特性、依赖收集、Git实操、组件设计 |
| 特殊之处 | 面试官通过"Proxy核心缺陷"把整个响应式原理串起来,让候选人学到很多 |
🔍 逐题深度解析
一、Vue2与Vue3响应式原理差异
问题:Vue2与Vue3的响应式原理差异?
javascript
// 1. Vue2响应式:Object.defineProperty
function defineReactive(obj, key, val) {
// 递归处理嵌套对象
observe(val)
Object.defineProperty(obj, key, {
get() {
// 依赖收集
if (Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 触发更新
dep.notify()
}
})
}
// Vue2的局限:
// - 无法监听新增/删除属性(需Vue.set/Vue.delete)
// - 无法直接监听数组变化(需重写数组方法)
// - 需要递归遍历所有属性,初始化性能开销大
// 2. Vue3响应式:Proxy
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key)
const value = Reflect.get(target, key, receiver)
// 懒递归:只有访问时才递归处理
if (typeof value === 'object' && value !== null) {
return reactive(value)
}
return value
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
const hadKey = Reflect.has(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
trigger(target, key)
}
return result
}
})
}
// 3. 核心差异总结
| 维度 | Vue2 | Vue3 |
|------|------|------|
| 实现方式 | Object.defineProperty | Proxy |
| 监听新增属性 | 不支持 | 支持 |
| 监听删除属性 | 不支持 | 支持 |
| 监听数组 | 需重写方法 | 原生支持 |
| 递归时机 | 初始化时 | 访问时(懒递归) |
| 性能 | 初始化慢,更新快 | 初始化快,更新快 |
| 兼容性 | IE9+ | 不支持IE |
二、为什么Vue3要重写
问题:为什么Vue3还要进行重写?
javascript
// 1. 解决Vue2的先天不足
// - 无法检测新增/删除属性
// - 数组索引和length变更无法监听
// - 初始化性能开销大
// 2. 利用Proxy的优势
// - 可以监听13种操作,不仅仅是get/set
// - 懒递归,只在访问时处理嵌套对象
// - 更好的性能(初始化快)
// 3. TypeScript支持
// - 源码用TS重写,类型推导更好
// - Composition API对TS友好
// 4. 更好的逻辑复用
// - Composition API替代mixins
// - 逻辑按功能组织,而不是按选项
// 5. 性能提升
// - 打包体积更小(tree-shaking友好)
// - 更新性能提升(静态提升、补丁标记)
// 6. 新特性支持
// - Teleport、Suspense、Fragment等
三、Vue3依赖收集
问题:Vue3的依赖收集
javascript
// 1. 依赖收集的数据结构
// targetMap: WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap()
// 2. 依赖收集过程
let activeEffect
function track(target, key) {
if (!activeEffect) return
// 获取target对应的依赖Map
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取key对应的依赖Set
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 收集当前活跃的effect
dep.add(activeEffect)
}
// 3. 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach(effect => effect())
}
// 4. 为什么用WeakMap?
// - 弱引用,不影响垃圾回收
// - 当target对象被销毁时,对应的依赖自动清除
// - 防止内存泄漏
// 5. 示例
const state = reactive({ count: 0 })
effect(() => {
console.log(state.count) // 触发track
})
state.count = 1 // 触发trigger
四、ref vs reactive
问题:ref 和reactive之间的差异,追问底层
javascript
// 1. reactive
// - 接收对象类型
// - 基于Proxy实现
// - 直接访问属性
const state = reactive({ count: 0 })
state.count++ // 直接修改
// 2. ref
// - 接收任意类型(包括基本类型)
// - 基于RefImpl类实现
// - 需要通过.value访问
const count = ref(0)
count.value++ // 通过.value
// 3. ref底层实现
function ref(value) {
return new RefImpl(value)
}
class RefImpl {
constructor(value) {
this._value = convert(value) // 如果是对象,用reactive处理
this.__v_isRef = true
}
get value() {
track(this, 'value') // 依赖收集
return this._value
}
set value(newVal) {
if (newVal !== this._value) {
this._value = convert(newVal)
trigger(this, 'value') // 触发更新
}
}
}
function convert(val) {
return isObject(val) ? reactive(val) : val
}
// 4. 为什么ref需要.value?
// - 基本类型无法用Proxy拦截
// - 需要用对象包裹,通过getter/setter实现响应式
// - .value是获取这个包裹对象的属性
五、为什么ref需要.value,reactive不需要?
问题:为什么 ref 需要 .value,而 reactive 不需要?
javascript
// 1. 设计层面的原因
// - reactive用于对象,可以用Proxy直接拦截属性访问
// - ref用于基本类型,必须用一个对象包裹才能实现响应式
// 2. 实现层面的原因
// reactive的Proxy:
const proxy = new Proxy({ count: 0 }, handlers)
proxy.count // 直接触发get
// ref的RefImpl:
const ref = {
_value: 0,
get value() { /* 依赖收集 */ },
set value(v) { /* 触发更新 */ }
}
ref.value // 必须通过访问器属性
// 3. 自动解包
// 在模板中使用时会自动解包
<template>
{{ count }} <!-- 不需要 .value -->
</template>
// 在reactive对象中也会自动解包
const state = reactive({ count: ref(0) })
state.count // 可以直接访问,不需要.value
// 4. 为什么这样设计?
// - 保持JS语法一致性
// - 明确区分响应式引用和普通变量
// - 在TS中类型推导更友好
六、处理reactive复杂对象重新赋值
问题:如何处理 reactive 复杂对象重新赋值?(业务场景,封装函数)
javascript
// 1. 问题场景
const state = reactive({
user: {
name: 'Tom',
age: 25,
address: {
city: '北京',
district: '朝阳'
}
}
})
// ❌ 直接重新赋值会丢失响应式
state.user = { name: 'Jerry', age: 30 } // 新对象不是响应式
// 2. 解决方案
// 2.1 逐个属性赋值
state.user.name = 'Jerry'
state.user.age = 30
// 2.2 使用Object.assign
Object.assign(state.user, { name: 'Jerry', age: 30 })
// 2.3 封装更新函数
function updateReactive(target, newValue) {
if (isReactive(target)) {
// 如果是响应式对象,使用Object.assign
Object.assign(target, newValue)
} else {
// 如果不是,直接替换
return newValue
}
}
// 2.4 深层次更新
function deepUpdate(original, updates) {
for (const key in updates) {
const originalVal = original[key]
const newVal = updates[key]
if (isReactive(originalVal) && typeof newVal === 'object') {
// 递归更新
deepUpdate(originalVal, newVal)
} else {
original[key] = newVal
}
}
}
// 3. 业务场景:表单重置
function resetForm(formData, initialData) {
// 不能直接 formData = initialData
// 需要逐个属性赋值
Object.keys(initialData).forEach(key => {
formData[key] = initialData[key]
})
}
// 4. 使用toRefs解构
const { user } = toRefs(state)
// 这样解构出来的属性是ref,可以重新赋值
user.value = { name: 'Jerry', age: 30 }
七、Proxy的核心缺陷
问题:Vue3 中 Proxy 的核心缺陷?(面试官通过这一点把以上整个思路串起来了)
javascript
// 1. Proxy的核心缺陷:无法处理基本类型
// Proxy只能拦截对象,不能拦截基本类型
// 这就是为什么要有ref
// 2. 无法完全代理所有操作
// 某些操作无法被拦截
// - 对象的内置方法(如Object.keys)
// - Symbol属性的一些特殊行为
// 3. 兼容性问题
// - 不支持IE
// - 部分老旧浏览器不支持
// 4. 性能开销
// - Proxy本身比Object.defineProperty慢
// - 但Vue3通过懒递归等优化,整体性能更好
// 5. 无法直接监听嵌套属性
// - 需要在get中递归创建Proxy
// - 这就是懒递归的实现
// 6. 面试官串起来的思路
// - 因为Proxy无法处理基本类型 → 需要ref
// - 因为Proxy无法一次性监听所有嵌套 → 需要懒递归
// - 因为Proxy有兼容性问题 → 需要降级方案
// - 因为Proxy性能有开销 → 需要优化策略
// 7. 实际影响
// - 使用ref处理基本类型
// - 使用shallowReactive处理不需要深层次响应的场景
// - 使用markRaw跳过不需要响应式的对象
八、Vue数据通信
问题:Vue数据通信,兄弟组件通信的前提是?(公共父组件)
javascript
// 1. 兄弟组件通信的前提
// 必须有公共的父组件
// 2. 通信方式
// 2.1 状态提升(最常用)
// 父组件
<template>
<div>
<A :count="count" @update="handleUpdate" />
<B :count="count" />
</div>
</template>
// 2.2 事件总线
const bus = mitt()
// 组件A
bus.emit('update', data)
// 组件B
bus.on('update', (data) => {})
// 2.3 全局状态管理
// Pinia / Vuex
const store = useStore()
store.count
// 2.4 provide/inject
// 祖先组件
provide('sharedState', sharedState)
// 后代组件
const sharedState = inject('sharedState')
// 3. 选择原则
// - 简单场景用状态提升
// - 中等场景用事件总线
// - 复杂场景用Pinia
九、三栏布局与BFC
问题:口述三栏布局如何实现(提到BFC)
css
/* 1. 三栏布局(左右固定,中间自适应) */
/* 1.1 Flex实现 */
.container {
display: flex;
}
.left { width: 200px; }
.right { width: 200px; }
.center { flex: 1; }
/* 1.2 Grid实现 */
.container {
display: grid;
grid-template-columns: 200px 1fr 200px;
}
/* 1.3 浮动实现(BFC) */
.left { float: left; width: 200px; }
.right { float: right; width: 200px; }
.center {
overflow: hidden; /* 触发BFC,避免被浮动覆盖 */
}
问题:追问BFC的场景,浮动,margin重叠
css
/* 2. BFC(块级格式化上下文) */
/* 2.1 触发BFC的条件 */
- overflow: hidden/auto/scroll
- display: flex/inline-block/flow-root
- position: absolute/fixed
- float: left/right
/* 2.2 BFC的应用场景 */
/* 清除浮动 */
.parent {
overflow: hidden; /* 触发BFC,包含浮动元素 */
}
.child { float: left; }
/* 防止margin重叠 */
.box1 { margin-bottom: 20px; }
.box2 { margin-top: 30px; }
/* 两个box的margin会重叠,取最大值30px */
/* 解决重叠 */
.box1 {
margin-bottom: 20px;
}
.box2 {
margin-top: 30px;
display: flow-root; /* 触发BFC,margin不重叠 */
}
/* 避免浮动覆盖 */
.left { float: left; }
.right {
overflow: hidden; /* 触发BFC,不会被浮动覆盖 */
}
十、Git场景实操
问题:两个人代码在不同分支操作现在要在测试环境测试如何操作
bash
# 1. 场景:A在feature/a分支,B在feature/b分支,需要在测试环境测试
# 2. 操作步骤
# 2.1 切换到测试分支
git checkout test
# 2.2 拉取最新代码
git pull origin test
# 2.3 合并feature/a
git merge feature/a
# 2.4 合并feature/b
git merge feature/b
# 2.5 如果有冲突,解决冲突
# 编辑冲突文件
git add .
git commit -m "merge feature/a and feature/b"
# 2.6 推送到远程
git push origin test
# 3. 冲突解决
# 3.1 查看冲突文件
git status
# 3.2 解决冲突后
git add .
git commit -m "resolve conflicts"
# 4. 终止操作
# 4.1 终止merge
git merge --abort
# 4.2 终止rebase
git rebase --abort
# 5. 如果已经push了
# 不要用rebase修改已推送的commit
# 用merge安全合并
十一、组件拆分与状态管理
问题:口述当前面试的赛码网如何拆分组件?
javascript
// 1. 赛码网页面结构分析
- 头部:用户信息、导航
- 侧边栏:题目列表
- 主内容:题目描述、代码编辑器
- 底部:提交按钮、状态
// 2. 组件拆分
components/
├── layout/
│ ├── Header.vue // 头部组件
│ ├── Sidebar.vue // 侧边栏
│ └── Main.vue // 主内容区域
├── problem/
│ ├── ProblemDesc.vue // 题目描述
│ ├── CodeEditor.vue // 代码编辑器
│ └── TestCase.vue // 测试用例
├── user/
│ └── UserInfo.vue // 用户信息
└── common/
├── Button.vue // 通用按钮
└── Modal.vue // 弹窗
// 3. 拆分原则
// - 单一职责:每个组件只做一件事
// - 可复用性:通用组件放在common
// - 业务隔离:不同模块分开
问题:赛码网对于用户信息这种组件可能都能使用的数据如何处理?
javascript
// 1. 用户数据的特点
// - 多个组件需要访问
// - 全局共享
// - 需要持久化
// 2. 解决方案
// 2.1 Pinia存储
const useUserStore = defineStore('user', {
state: () => ({
info: null,
token: localStorage.getItem('token')
}),
actions: {
async fetchUser() {
this.info = await api.getUserInfo()
},
login(credentials) {
// 登录逻辑
}
}
})
// 2.2 组件中使用
const userStore = useUserStore()
const { info } = storeToRefs(userStore)
// 2.3 持久化
persist: true // 自动持久化到localStorage
// 3. 避免直接修改
// 在组件中不能直接修改store
// 需要通过actions
问题:赛码网代码编辑器的不同tab栏思路,不同的tab都是一个组件吗?
javascript
// 1. Tab栏设计思路
// 1.1 动态组件
<template>
<div class="tabs">
<div class="tab-header">
<div
v-for="tab in tabs"
:key="tab.name"
@click="activeTab = tab.name"
:class="{ active: activeTab === tab.name }"
>
{{ tab.title }}
</div>
</div>
<div class="tab-content">
<component :is="activeComponent" :data="currentData" />
</div>
</div>
</template>
// 1.2 不同tab对应不同组件
const tabs = [
{ name: 'description', title: '题目描述', component: ProblemDesc },
{ name: 'code', title: '代码', component: CodeEditor },
{ name: 'result', title: '运行结果', component: TestResult }
]
// 2. 是否用一个组件?
// - 不同tab内容差异大:用不同组件
// - 内容结构相似:可以用同一个组件,通过props区分
// 3. 代码编辑器特殊处理
// - 编辑器本身就是一个独立组件
// - 不同语言的tab只是切换语言
const CodeEditor = {
props: ['language'],
watch: {
language() {
this.editor.setLanguage(this.language)
}
}
}
十二、事件循环输出题
问题:事件循环代码输出题
javascript
// 1. 经典输出题
console.log('1')
setTimeout(() => {
console.log('2')
}, 0)
Promise.resolve().then(() => {
console.log('3')
}).then(() => {
console.log('4')
})
console.log('5')
// 输出:1, 5, 3, 4, 2
// 2. async/await题
async function test() {
console.log('1')
await console.log('2')
console.log('3')
}
console.log('4')
test()
console.log('5')
// 输出:4, 1, 2, 5, 3
// 解析:await后面的代码相当于Promise.then
// 3. 混合题
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('script end')
// 输出:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
// 4. 关键点
// - 同步代码优先
// - 微任务(Promise.then)先于宏任务(setTimeout)
// - await后面的代码属于微任务
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| Vue2响应式 | defineProperty、无法监听新增/删除 |
| Vue3响应式 | Proxy、懒递归、支持13种操作 |
| 依赖收集 | targetMap、activeEffect、track/trigger |
| ref vs reactive | 基本类型用ref,对象用reactive,.value设计 |
| Proxy缺陷 | 无法处理基本类型,需要ref |
| 重新赋值 | Object.assign、深更新、toRefs |
| BFC | 触发条件、清除浮动、防止margin重叠 |
| Git操作 | merge、冲突解决、--abort |
| 组件拆分 | 单一职责、可复用、业务隔离 |
| 事件循环 | 同步>微任务>宏任务、await解析 |
📌 最后一句:
小红书这场二面,面试官通过"Proxy缺陷"把整个Vue3响应式设计串起来,让你不仅知道是什么,更知道为什么这么设计。能通过这样的面试,说明你对Vue的理解已经超越API层面,达到了设计思想层面。