前言
大家好,我是木斯佳。
在这个春节假期,当大家都在谈论返乡、团圆与休息时,作为一名技术人,我的思考却不由自主地转向了行业的「冬」与「春」。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
正值春节,也是复盘与规划的好时机。结合CSDN这次「春节代码贺新年」活动所提倡的"用技术视角记录春节、复盘成长",我决定在这个假期持续更新专栏,帮助年后参加春招的同学。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。
我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。
温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
在这个假期,让我们一起充电,为下一个技术春天做好准备。

面经原文内容
📍面试公司:小红书
🕐面试时间: 2 月 3 日
💻面试岗位:前端实习
❓面试问题:
自我介绍一下
- 你提到的项目:XX项目开发过程中遇到了什么困难。我的困难是双 token 的处理,然后讨论了一些双 token 的东西
- 公司实习项目干了什么,然后讨论了一些问题,我是怎么处理的,面试官追问细节
- vue3 响应式原理
- vue 父子组件通信
- 平时怎么写代码的
- 手写三栏布局,多种情况,聊了一些 grid 和 flex还有追问如果是平均分要怎么写
flex:1 是什么 - 代码输出题,宏任务和微任务
- 手写 promise.all,遇到一些问题,我用foreach同步代码遍历没有判断结果的数量对不对就直接resolve了
- 反问,问了一些业务和我的一些不足点
🙌面试感想:
直接面了 45 分钟,面试官人很好,反而问的八股很少,都是结合在手撕和项目上面,然后也是体验到要你写代码的面试了,我去面试体验 100 分,面试反馈直接写能不能给面试官加薪了,不管结果怎么样,确实是一个很棒的体验!!!然后面试官推荐我去看一些 js 的书,也可以巩固一下之类的,我觉得很对
来源: 牛客网 小海c
📝 小红书前端一面·面经深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 部门定位 | 小红书 - 社区/电商业务前端 |
| 面试风格 | 项目驱动型 + 场景追问型 + 手写实战型 |
| 难度评级 | ⭐⭐⭐(三星,偏重实战与原理) |
| 考察重心 | 双Token机制、Vue响应式、布局实现、Promise手写 |
| 面试体验 | ⭐⭐⭐⭐⭐(候选人评价极高) |
💡 面经关键点:
小红书面试非常重视项目实战能力 和代码实现质量
八股问得少,但问到的都是核心原理,且会结合手写验证
🔐 双Token机制·项目难点深挖
问题:项目开发中遇到的困难(双Token处理)
✅ 完整答案结构:
1. 什么是双Token机制?
| Token类型 | 有效期 | 存储位置 | 用途 |
|---|---|---|---|
| Access Token | 短(15分钟-2小时) | 内存/ localStorage | 请求认证 |
| Refresh Token | 长(7-30天) | httpOnly Cookie / 安全存储 | 刷新Access Token |
2. 为什么需要双Token?
javascript
// 单Token的问题
- Token被盗用后,攻击者可以在有效期内任意访问
- Token无法在服务端主动失效(除非黑名单)
- 频繁登录体验差
// 双Token的优势
- 短时效Access Token降低被盗风险
- Refresh Token可以安全存储在httpOnly Cookie中
- 可以在服务端主动失效Refresh Token(如修改密码)
3. 前端实现细节
javascript
// axios拦截器实现自动刷新
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// 如果是401且不是刷新token请求
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 正在刷新中,将请求加入队列
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return axios(originalRequest);
})
.catch(err => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/auth/refresh', {
refreshToken
});
const { accessToken, refreshToken: newRefreshToken } = response.data;
localStorage.setItem('accessToken', accessToken);
if (newRefreshToken) {
localStorage.setItem('refreshToken', newRefreshToken);
}
processQueue(null, accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
// 刷新失败,跳转登录
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
4. 面试官追问细节
追问1:刷新Token时,多个并发请求怎么处理?
使用队列机制,第一个请求触发刷新,其他请求排队等待新Token
追问2:Refresh Token被盗怎么办?
javascript
// 解决方案:
1. 存储在httpOnly Cookie中(防止XSS)
2. 绑定设备指纹(UserAgent + IP)
3. 定期轮换Refresh Token
4. 登出时服务端加入黑名单
追问3:Token存储在localStorage vs Cookie?
javascript
// localStorage
✅ 优点:简单、跨标签页共享
❌ 缺点:XSS风险
// Cookie(httpOnly)
✅ 优点:防止XSS
❌ 缺点:CSRF风险(可通过SameSite缓解)
// 最佳实践:
Access Token → 内存(变量) + 必要时存localStorage
Refresh Token → httpOnly Cookie + Secure + SameSite
⚛️ Vue3响应式原理·深度解析
问题:Vue3响应式原理
✅ 完整答案:
1. 核心机制:Proxy
javascript
// Vue2使用Object.defineProperty
- 无法检测新增/删除属性
- 无法直接监听数组变化(需要hack)
- 需要递归遍历所有属性
// Vue3使用Proxy
const reactive = (target) => {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key);
return Reflect.get(target, key, receiver);
},
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;
}
});
};
2. 依赖收集(track)
javascript
// 数据结构
targetMap = WeakMap({
target: Map({
key: Set([effect1, effect2])
})
})
let activeEffect;
class ReactiveEffect {
constructor(fn) {
this.fn = fn;
this.deps = [];
}
run() {
activeEffect = this;
const result = this.fn(); // 执行时会触发get
activeEffect = null;
return result;
}
}
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
3. 触发更新(trigger)
javascript
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (effects) {
effects.forEach(effect => {
if (effect !== activeEffect) {
effect.run(); // 重新执行
}
});
}
}
4. ref vs reactive
javascript
// reactive:处理对象
const state = reactive({ count: 0 });
state.count++; // 自动追踪
// ref:处理基本类型
const count = ref(0);
count.value++; // 需要通过.value访问
// ref的原理
function ref(value) {
return new RefImpl(value);
}
class RefImpl {
constructor(value) {
this._value = value;
this.__v_isRef = true;
}
get value() {
track(this, 'value');
return this._value;
}
set value(newVal) {
this._value = newVal;
trigger(this, 'value');
}
}
5. 面试官追问:computed原理
javascript
function computed(getter) {
let value;
let dirty = true;
const effect = new ReactiveEffect(getter, () => {
dirty = true; // 依赖变化时标记为脏
});
return {
get value() {
if (dirty) {
value = effect.run();
dirty = false;
}
return value;
}
};
}
🔄 Vue父子组件通信·全场景总结
问题:Vue父子组件通信方式
✅ 完整总结:
1. Props / Emit(最常用)
vue
<!-- 父组件 -->
<template>
<Child
:message="parentMsg"
@update="handleUpdate"
/>
</template>
<!-- 子组件 -->
<script setup>
const props = defineProps({
message: String
});
const emit = defineEmits(['update']);
const handleClick = () => {
emit('update', 'new value');
};
</script>
2. v-model(双向绑定)
vue
<!-- 父组件 -->
<Child v-model:title="pageTitle" />
<!-- 子组件 -->
<script setup>
const props = defineProps(['title']);
const emit = defineEmits(['update:title']);
const updateTitle = () => {
emit('update:title', 'new title');
};
</script>
3. ref / expose(直接调用)
vue
<!-- 父组件 -->
<template>
<Child ref="childRef" />
</template>
<script setup>
const childRef = ref(null);
const callChildMethod = () => {
childRef.value.someMethod();
};
</script>
<!-- 子组件 -->
<script setup>
const someMethod = () => {
console.log('child method');
};
// 暴露给父组件
defineExpose({ someMethod });
</script>
4. provide / inject(跨层级)
javascript
// 祖先组件
import { provide } from 'vue';
provide('theme', 'dark');
// 后代组件
import { inject } from 'vue';
const theme = inject('theme', 'light'); // 第二个参数为默认值
5. 事件总线(mitt)
javascript
// event-bus.js
import mitt from 'mitt';
export const emitter = mitt();
// 组件A
import { emitter } from './event-bus';
emitter.emit('some-event', data);
// 组件B
import { emitter } from './event-bus';
emitter.on('some-event', (data) => {
console.log(data);
});
6. Vuex / Pinia(全局状态)
javascript
// store.js
import { defineStore } from 'pinia';
export const useMainStore = defineStore('main', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
}
}
});
// 任何组件
const store = useMainStore();
store.increment();
🎨 三栏布局·手写实战
问题:手写三栏布局,多种情况
✅ 完整实现:
1. 经典圣杯布局(中间自适应,左右固定)
html
<!-- Flexbox实现 -->
<div class="flex-container">
<div class="left">左侧固定 200px</div>
<div class="center">中间自适应</div>
<div class="right">右侧固定 200px</div>
</div>
<style>
.flex-container {
display: flex;
height: 200px;
}
.left {
width: 200px;
background: #f0f0f0;
}
.center {
flex: 1; /* 自动占据剩余空间 */
background: #e0e0e0;
}
.right {
width: 200px;
background: #d0d0d0;
}
</style>
2. Grid实现
html
<div class="grid-container">
<div class="left">左侧固定</div>
<div class="center">中间自适应</div>
<div class="right">右侧固定</div>
</div>
<style>
.grid-container {
display: grid;
grid-template-columns: 200px 1fr 200px; /* 固定 自适应 固定 */
height: 200px;
}
.left { background: #f0f0f0; }
.center { background: #e0e0e0; }
.right { background: #d0d0d0; }
</style>
3. 双飞翼布局(中间优先加载)
html
<div class="container">
<div class="center">
<div class="center-content">中间内容(优先加载)</div>
</div>
<div class="left">左侧</div>
<div class="right">右侧</div>
</div>
<style>
.container {
display: flex;
}
.center {
order: 2;
flex: 1;
margin: 0 200px; /* 给左右留空间 */
}
.left {
order: 1;
width: 200px;
margin-left: -100%;
}
.right {
order: 3;
width: 200px;
margin-left: -200px;
}
</style>
4. 平均分三列
html
<!-- 三列平均分布 -->
<div class="equal-container">
<div class="item">1/3</div>
<div class="item">1/3</div>
<div class="item">1/3</div>
</div>
<style>
/* Flex实现 */
.equal-container {
display: flex;
}
.item {
flex: 1; /* 每个item占据相等空间 */
height: 200px;
}
/* Grid实现 */
.equal-container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 三个等分列 */
}
</style>
5. 面试官追问:flex:1 是什么?
css
/* flex: 1 是简写 */
flex: 1 1 0%;
/* 相当于:
flex-grow: 1; // 有剩余空间时放大
flex-shrink: 1; // 空间不足时缩小
flex-basis: 0%; // 初始大小为0
*/
/* 其他常见值 */
flex: 0 1 auto; /* 默认值 */
flex: 2; /* flex: 2 1 0% */
flex: auto; /* flex: 1 1 auto */
flex: none; /* flex: 0 0 auto */
🔄 事件循环·代码输出题
问题:宏任务和微任务执行顺序
✅ 完整解析:
1. 基础概念回顾
javascript
// 宏任务(MacroTask)
- setTimeout
- setInterval
- setImmediate (Node)
- I/O
- UI渲染
// 微任务(MicroTask)
- Promise.then/catch/finally
- MutationObserver
- queueMicrotask
- process.nextTick (Node)
2. 常见考题
javascript
console.log('1: 同步');
setTimeout(() => {
console.log('2: setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise');
}).then(() => {
console.log('4: Promise chain');
});
console.log('5: 同步');
// 输出顺序:
// 1: 同步
// 5: 同步
// 3: Promise
// 4: Promise chain (微任务产生的微任务)
// 2: setTimeout
// 关键点:微任务产生的微任务会在当前宏任务结束前执行,不是下一帧!
3. 进阶考题(async/await)
javascript
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 (await后面的相当于then)
// promise2
// setTimeout
4. 候选人复盘的关键点
"我以为微任务产生的微任务是丢到下一帧处理的,这个搞错了"
javascript
// ❌ 错误理解
微任务队列: [task1]
执行 task1 → 产生微任务 task1-1
task1-1 放到"下一帧"的微任务队列
// ✅ 正确理解
微任务队列: [task1]
执行 task1 → 产生微任务 task1-1
task1-1 放到**当前微任务队列末尾**
继续执行直到队列清空
🛠️ 手写Promise.all·常见错误
问题:手写Promise.all
✅ 完整实现:
1. 标准实现
javascript
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('参数必须是数组'));
}
const results = [];
let completed = 0;
// 处理空数组
if (promises.length === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
// 确保每个元素都是Promise
Promise.resolve(promise)
.then(value => {
results[index] = value;
completed++;
if (completed === promises.length) {
resolve(results);
}
})
.catch(reject); // 任何一个失败,直接reject
});
});
}
2. 候选人实现分析
javascript
// ❌ 不友好实现(用forEach同步遍历,直接resolve)
function promiseAllWrong(promises) {
return new Promise((resolve, reject) => {
const results = [];
promises.forEach((promise, index) => {
promise.then(value => {
results[index] = value;
// 这里直接resolve,没有判断是否所有都完成
resolve(results); // ❌ 第一个完成就resolve了
}).catch(reject);
});
});
}
// 问题:第一个Promise完成就直接resolve了
// 没有等待所有Promise完成
3. 完善版(考虑各种情况)
javascript
function promiseAllComplete(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('参数必须是数组'));
}
const results = new Array(promises.length);
let completed = 0;
let rejected = false;
if (promises.length === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
if (rejected) return;
results[index] = value;
completed++;
if (completed === promises.length) {
resolve(results);
}
})
.catch(error => {
if (!rejected) {
rejected = true;
reject(error);
}
});
});
});
}
4. 相关API对比
javascript
// Promise.allSettled - 等待所有完成,不关心成功/失败
Promise.allSettled = function(promises) {
return new Promise(resolve => {
const results = [];
let completed = 0;
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then(value => {
results[index] = { status: 'fulfilled', value };
})
.catch(reason => {
results[index] = { status: 'rejected', reason };
})
.finally(() => {
completed++;
if (completed === promises.length) {
resolve(results);
}
});
});
});
};
// Promise.race - 谁先完成就返回谁
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach(promise => {
Promise.resolve(promise)
.then(resolve)
.catch(reject);
});
});
};
🎁 附:小红书面试复习清单
| 知识点 | 掌握程度 | 重点方向 |
|---|---|---|
| 双Token机制 | ⭐⭐⭐⭐ | 实现、并发处理、安全 |
| Vue3响应式 | ⭐⭐⭐⭐ | Proxy、依赖收集、ref/reactive |
| 组件通信 | ⭐⭐⭐ | props/emit、provide/inject、v-model |
| 三栏布局 | ⭐⭐⭐ | flex、grid、圣杯/双飞翼 |
| flex:1 | ⭐⭐⭐ | 含义、计算规则 |
| 事件循环 | ⭐⭐⭐⭐ | 宏任务/微任务、async/await |
| Promise.all | ⭐⭐⭐⭐ | 手写实现、边界情况 |
| 错误复盘 | ⭐⭐⭐⭐ | 微任务连续处理、Promise计数 |
📌 最后一句:
小红书的面试,是一场技术实战 + 学习能力 的温暖对话。
他们不只想要一个能写代码的人,
他们想要一个能一起解决问题、一起成长的人 。
当面试官推荐你看书的时候,他不是在说你不行,而是在说:
"我看好你,希望你能变得更好。"