拼多多前端春招准备
围绕拼多多春招前端笔试里出现的 CSS 基线、浏览器时间切片、requestAnimationFrame 与 Vue 响应式手写题做系统整理。
[!info] related notes
- 前端高频入口: 前端八股文 MOC
- CSS 主线: CSS MOC, CSS 基线与 vertical-align
- 浏览器与性能: 浏览器时间切片与协作式调度, Interaction to Next Paint
- Vue 主线: Vue MOC, 手写 Vue reactive 和 watchEffect 的最小实现
拼多多前端春招准备
围绕拼多多春招前端笔试里出现的 CSS 基线、浏览器时间切片、requestAnimationFrame 与 Vue 响应式手写题做系统整理。
这篇怎么用
- 先看每道题的“直接答法”,确保笔试或口述时能快速落点。
- 再看“背后的知识链”,把零散题目挂到已有知识地图上。
- 对需要手写的题,优先掌握最小可用实现,再去理解工程级边界。
这组题在考什么
表面上是 4 个题,底层其实在考 4 条主线:
- 浏览器动画调度:为什么高帧动画更适合
requestAnimationFrame - CSS 行内排版:为什么字体、按钮、图标会出现基线错位
- 主线程调度:怎么把长任务拆成时间切片,避免阻塞交互和渲染
- Vue 原理:
reactive和watchEffect背后的依赖收集与触发更新
1. 选择题:为什么高帧动画优先用 requestAnimationFrame
直接答法
因为 requestAnimationFrame 会在下一次浏览器绘制前执行回调,和屏幕刷新节奏对齐,更适合做平滑动画;相比 setTimeout,它更不容易掉帧和时间漂移。
更完整的理解
这题不是在问“哪个 API 更快”,而是在问你知不知道动画属于浏览器渲染链路的一部分。
回答时最好带上这几个点:
requestAnimationFrame回调时机在下一次 paint 之前- 浏览器可以根据刷新节奏统一调度动画
- 后台标签页会自动降频或暂停,避免无意义消耗
- 动画代码和渲染时机更贴近,视觉更稳定
和 setTimeout 的区别
| 对比项 | requestAnimationFrame | setTimeout |
|---|---|---|
| 目标场景 | 动画、视觉更新 | 通用延迟执行 |
| 时机 | 下一次绘制前 | 定时器到时后排队 |
| 是否容易漂移 | 相对更稳 | 容易受主线程繁忙影响 |
| 后台标签页 | 通常自动降频 | 也会被限制,但不是为动画设计 |
容易补充的一句
如果是高帧动画,我会优先选
requestAnimationFrame;如果是一般任务分片或延迟执行,则要看是不是更适合MessageChannel、setTimeout或 Worker。
2. 问答题:特殊字体导致基线不同,怎么解决
直接答法
这类问题本质上是行内排版的基线规则和字体度量差异导致的。排查时我会先统一 font-size、line-height,再检查按钮/图标/文字的显示类型和默认样式;如果是按钮或图标混排,通常会改成 inline-flex、统一行高,必要时显式设置 vertical-align。
背后的知识点
这题真正考的是:
- 你知不知道“基线”不是盒子中心
- 你知不知道字体、按钮、图片、
inline-block的对齐规则不同 - 你会不会从行内格式化上下文去解释问题
相关知识主笔记:
实战排查顺序
- 看字体是否一致
- 看
line-height是否一致 - 看按钮/输入框有没有默认
padding、border - 看元素是不是
inline-block或替换元素 - 试着改成
inline-flex,观察问题是否立刻消失
更稳的修法
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
.text {
line-height: 1;
}
如果本质是多元素整体布局问题,优先用 Flex/Grid,不要继续拿 vertical-align 当万能方案。
3. 手写题:实现时间切片
题目描述:
当有大量同步函数时,每次最多执行
15ms,然后让浏览器有机会去更新动画或渲染;如果某个函数报错,返回错误 Promise。
这题在考什么
本质是浏览器主线程调度。
如果你把几十个同步函数一次性全跑完,主线程在这段时间里不能处理:
- 用户点击
- 动画刷新
- 样式布局绘制
所以要把任务拆成多个切片,每片执行到预算上限就主动让出执行权。
答题时一定要提的关键词
- 长任务
- 主线程阻塞
- 帧预算
performance.now()Promise- fail-fast 错误处理
一个可用实现
function timeSlice(functionArray, option = {}) {
const {
budget = 15,
args = [],
yieldStrategy = 'messageChannel',
} = option;
if (!Array.isArray(functionArray)) {
return Promise.reject(new TypeError('functionArray must be an array'));
}
let index = 0;
const results = new Array(functionArray.length);
const channel = typeof MessageChannel !== 'undefined'
? new MessageChannel()
: null;
return new Promise((resolve, reject) => {
const scheduleNext = () => {
if (yieldStrategy === 'raf' && typeof requestAnimationFrame === 'function') {
requestAnimationFrame(runSlice);
return;
}
if (yieldStrategy === 'timeout' || !channel) {
setTimeout(runSlice, 0);
return;
}
channel.port2.postMessage(undefined);
};
const runSlice = () => {
const start = performance.now();
try {
while (
index < functionArray.length &&
performance.now() - start < budget
) {
const task = functionArray[index];
if (typeof task !== 'function') {
throw new TypeError(`task at index ${index} is not a function`);
}
results[index] = task(...args);
index += 1;
}
if (index >= functionArray.length) {
channel && (channel.port1.onmessage = null);
resolve(results);
return;
}
scheduleNext();
} catch (error) {
channel && (channel.port1.onmessage = null);
reject(error);
}
};
if (channel) {
channel.port1.onmessage = runSlice;
}
scheduleNext();
});
}
为什么这版实现是对的
- 顺序执行,符合“函数数组”的语义
- 每轮通过
performance.now()控制预算 - 超预算就调度下一轮,不继续霸占主线程
- 一旦抛错立即
reject - 最终返回所有函数的结果数组
这题背后的系统知识
建议连着看:
4. 手写题:用纯 JS 实现 reactive 和 watchEffect
直接思路
这题最小实现只需要三步:
- 用
Proxy拦截对象get/set get时收集依赖set时触发依赖重新执行
watchEffect 的关键点是:
- 执行回调前,把当前副作用函数挂到全局上下文里
- 回调里读到哪些响应式属性,就自动把自己订阅到那些属性上
手写题最小骨架
const bucket = new WeakMap();
let activeEffect = null;
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
const dep = depsMap?.get(key);
dep?.forEach((effectFn) => effectFn());
}
function watchEffect(fn) {
const effectFn = () => {
activeEffect = effectFn;
fn();
activeEffect = null;
};
effectFn();
}
function 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 result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
},
});
}
面试时最好主动补一句
上面这版只是最小原理版。真实 Vue 还会处理依赖清理、嵌套 effect、调度器、批量更新、数组和
Map/Set等边界。
继续往下补的主线
5. 这组题应该怎么继续扩知识库
如果后面继续补这一组题,优先按下面的结构走:
- 动画与调度:
requestAnimationFrame、长任务、时间切片、INP - CSS 排版:基线、
line-height、vertical-align、Flex 内部对齐 - Vue 原理:依赖收集、触发更新、
ref/reactive/computed/watch的边界 - 笔试表达:每个题都保留“30 秒答法 + 2 分钟展开 + 可手写版本”
6. 速背版
- 高帧动画优先
requestAnimationFrame,因为它和浏览器绘制节奏对齐 - 基线问题本质是行内排版和字体度量差异,不是简单的“没居中”
- 时间切片是协作式调度:跑一会儿就主动让出主线程
reactive负责拦截访问,watchEffect负责自动依赖收集