前端八股文面经大全:小红书前端一二面OC(下)·(2026-03-17)·面经深度解析

前言

大家好,我是木斯佳。

相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。

这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。专栏快速链接

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。

面经原文内容(二面)

📍面试公司:小红书

🕐面试时间:近期,用户上传于2026-03-17

💻面试岗位:前端(二面)

⏱️面试时长:1小时

📝面试体验:二面学到了很多

❓面试问题(二面):

(一)框架(深入拷打)

  1. Vue2与Vue3的响应式原理差异?
  2. 为什么Vue3还要进行重写?
  3. Vue3的依赖收集
  4. ref 和reactive之间的差异,追问底层
  5. 为什么 ref 需要 .value,而 reactive 不需要?
  6. 如何处理 reactive 复杂对象重新赋值?(业务场景,封装函数)
  7. Vue3 中 Proxy 的核心缺陷?(面试官通过这一点把以上整个思路串起来了)
  8. Vue数据通信,兄弟组件通信的前提是?(公共父组件)

(二)CSS

  1. 口述三栏布局如何实现(提到BFC)

  2. 追问BFC的场景,浮动,margin重叠

(三)工程化

  1. git场景实操,两个人代码在不同分支操作现在要在测试环境测试如何操作

  2. 追问,冲突解决,Merge过程想要终止git命令是什么?rebase呢?

  3. 口述当前面试的赛码网如何拆分组件?

  4. 赛码网对于用户信息这种组件可能都能使用的数据如何处理?

  5. 赛码网代码编辑器的不同tab栏思路,不同的tab都是一个组件吗?

(四)手撕

  1. 事件循环代码输出题

  2. 手撕(未透露题目)

来源:牛客网 今天的算法题刷了吗

💡 木木有话说(刷前先看)

这个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层面,达到了设计思想层面

相关推荐
陈天伟教授2 小时前
人工智能应用- 预测新冠病毒传染性:04. 中国:强力措施遏制疫情
前端·人工智能·安全·xss·csrf
zayzy2 小时前
前端八股总结
开发语言·前端·javascript
今天减肥吗2 小时前
前端面试题
开发语言·前端·javascript
Rabbit_QL2 小时前
【前端UI行话】前端 UI 术语速查表
前端·ui·状态模式
小码哥_常3 小时前
一文带你吃透Android BLE蓝牙开发全流程
前端
敲代码的嘎仔3 小时前
Java后端面试——SSM框架面试题
java·面试·职场和发展·mybatis·ssm·springboot·八股
小码哥_常3 小时前
从“新老交锋”看Retrofit与Ktor
前端
小J听不清3 小时前
CSS 外边距(margin)全解析:取值规则 + 实战用法
前端·javascript·css·html·css3
还是大剑师兰特4 小时前
Stats.js 插件详解及示例(完全攻略)
前端·大剑师·stats