电商前端面试题
一、前端基础
1. 浏览器渲染流程?reflow 和 repaint 区别?
答案: HTML → DOM 树,CSS → CSSOM 树,合并成渲染树 → 布局(Layout)→ 绘制(Paint)→ 合成(Composite)。
// 重排:改布局属性(宽高、位置)→ 重新计算布局
el.style.width = '100px' // reflow
// 重绘:改外观属性(颜色、背景)→ 只重新画
el.style.color = 'red' // repaint
// 重排一定触发重绘,重绘不一定要重排2. 多端适配方案?
答案: 响应式(媒体查询)+ 动态尺寸(rem/vw)+ 布局弹性(flex/grid)。
/* 方案1:vw 单位 —— 随视口变化 */
.box { width: 50vw; }/* 方案2:rem —— 根字体基准 */
html { font-size: 13.3333vw; } /* 375px 设计稿 = 50px */
.box { width: 1rem; } /* 375px 屏 = 50px *//* 方案3:媒体查询断点 */
@media (max-width: 768px) { ... }小程序用 rpx(750 设计稿),uni-app 里可写 rpx 直接多端适配。
3. 闭包、原型链、事件循环?
答案: 闭包 = 函数记住定义时作用域;原型链 = 对象找属性一路沿 proto 向上;事件循环 = 同步先执行,微任务(Promise)在宏任务(setTimeout)前。
// 闭包
function outer() {
let n = 0
return () => ++n
}
// 原型链
arr -> Array.prototype -> Object.prototype -> null
// 事件循环
console.log(1) // 同步
Promise.resolve().then(() => console.log(2)) // 微任务
setTimeout(() => console.log(3)) // 宏任务
// 输出:1 2 34. 深拷贝实现及 JSON 方案的缺陷?
答案: JSON.parse(JSON.stringify) 丢失 undefined/函数/Symbol、Date 变字符串、循环引用直接报错。
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (map.has(obj)) return map.get(obj) // 循环引用
const copy = Array.isArray(obj) ? [] : {}
map.set(obj, copy)
for (const key of Object.keys(obj)) {
copy[key] = deepClone(obj[key], map)
}
return copy
}5. HTTP 缓存?
答案: 强缓存命中不发请求(Cache-Control/Expires),没命中走协商缓存(ETag/Last-Modified),服务器返回 304 则用缓存。
第一次请求 → 200 + 响应头 Cache-Control: max-age=3600 3600秒内 → 200 (from disk cache),不发请求 过期 → 带 If-None-Match 问服务器 → 304 用旧 / 200 用新 6. 跨域解决方案?
答案: CORS(服务端配头)、JSONP(script 标签)、代理(开发 Vite proxy / 生产 Nginx 反向代理)。
// 后端加响应头Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST
// 预检请求:非简单请求(自定义头/JSON)先发 OPTIONS 问服务器允许吗7. 防抖节流?
答案: 防抖 = 停止触发后才执行(搜索联想);节流 = 固定间隔执行(滚动加载)。
function debounce(fn, delay) {
let t
return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), delay) }}
function throttle(fn, wait) {
let last = 0
return (...a) => {
const now = Date.now()
if (now - last >= wait) { last = now; fn(...a) }} } 8. Promise.all 手写?
答案: 全部成功返回数组,一个失败就 reject。
function promiseAll(promises) {
return new Promise((resolve, reject) => {
const results = []
let count = 0
promises.forEach((p, i) => {p.then(val => { results[i] = val
if (++count === promises.length) resolve(results)}, reject) }) }) }
二、Vue3 专项
9. Vue3 响应式原理?比 Vue2 强在哪?
答案: Proxy 拦截对象读写,get 收集依赖,set 触发更新;Vue2 用 defineProperty 只能监听已有属性,新增/删除属性、数组索引都监听不到。
const state = reactive({ count: 0 })
// get → 收集依赖;set → 通知更新
effect(() => console.log(state.count))
state.count++ // 自动执行 effect// Vue2 痛点:state.newKey = 1 不响应(需 $set)10. Composition API vs Options API?
答案: 组合式按"功能"组织代码,避免同一个功能的逻辑散在 data/methods/lifecycle 三处,逻辑复用靠自定义 hook(useXxx)。
// Options:数据、方法、生命周期分开
export default { data() {}, methods: {}, mounted() {} }// Composition:一个功能的代码在一起
export default {setup() {
const count = ref(0)
const inc = () => count.value++onMounted(() => console.log('mounted'))
return { count, inc }} } 11. v-if 和 v-for 为什么不能一起用?
答案: v-for 优先级比 v-if 高,同时用会"先循环再判断",每项都白遍历一次;应该用计算属性过滤后再 v-for。
// ❌
<li v-for="item in list" v-if="item.visible">
// ✅
const visibleList = computed(() => list.filter(i => i.visible))
<li v-for="item in visibleList">12. computed vs watch?
答案: computed 由依赖自动推导、有缓存、模板里用;watch 监听变化做副作用(请求、跳转)。
const total = computed(() => items.value.reduce((s, i) => s + i.price, 0))watch(() => route.query.id, (val) => { fetchDetail(val) }) 13. nextTick 原理?
答案: Vue 更新是异步的(微任务批处理),DOM 更新完后的回调用 nextTick 包;底层优先 Promise。
const count = ref(0)
count.value = 1
console.log(document.querySelector('p').textContent) // 还是旧值
await nextTick()
console.log(document.querySelector('p').textContent) // 新值14. 组件通信方式?
答案: props 父传子、emit 子传父、v-model 双向、provide/inject 跨层级、Pinia 全局共享、ref 直接拿子组件实例。
// provide/injectprovide('user', user) // 父
const user = inject('user') // 任意深层子// Pinia
const store = useUserStore() // 任何组件15. 虚拟 DOM 和 diff?Vue3 优化了什么?
答案: diff 是比对新旧虚拟 DOM,只更新差异;Vue3 优化:静态节点提升(不重复创建)、patch flag 按需更新、双端 diff 改最长递增子序列。
// Vue3 patch flag:标记动态节点,只更新变的
<div>静态内容</div>
<span :class="cls">动态</span> // 只给这个打 flag三、React18 专项
16. 并发模式?
答案: 渲染可中断、高优任务插队、低优任务让路;startTransition 标记低优更新,useDeferredValue 延迟值更新。
const [isPending, startTransition] = useTransition()startTransition(() => setSearchResults(data)) // 低优,不卡输入 17. 渲染流程?Fiber/Scheduler/Reconciler/双缓冲?
答案: Scheduler 调度 → Render 阶段(Fiber 树上 diff,可中断)→ Commit 阶段(改 DOM,不可中断)→ 浏览器绘制。双缓冲 = 两棵 Fiber 树,切换指针上屏,用户永远看不到半成品。
setState → 批处理 → Scheduler → Render(重跑组件 → 新虚拟DOM → reconcile/diff → effectTag) → Commit(Mutation 改DOM → useLayoutEffect → 切指针) → 绘制 → useEffect 18. useEffect/useLayoutEffect?useMemo/useCallback?
答案: useEffect 绘制后异步跑;useLayoutEffect 绘制前同步跑(改布局防闪烁)。useMemo 缓存值防重复计算,useCallback 缓存函数防子组件重渲染。
useLayoutEffect(() => {
// 读布局并调整,用户看不到闪烁}, [])
useEffect(() => {
// 请求数据、日志,不阻塞绘制}, []) 19. 防止不必要渲染的手段?
答案: React.memo(函数组件浅比较 props)、PureComponent(类组件)、shouldComponentUpdate(手动控制)、useMemo/useCallback(稳住引用)。
const Child = React.memo(({ name }) => <div>{name}</div>)
// props 引用/值没变就不重渲染20. 合成事件是什么?
答案: React 把所有事件委托到根容器统一处理(17 前在 document,17+ 在 root),模拟冒泡;好处:跨浏览器统一行为、自动清理、性能好。
<button onClick={handle}> // 实际绑定在 root 上,事件对象是 SyntheticEvent21. 受控/非受控组件?
答案: 受控 = 值由 state 管(value + onChange),数据可靠但代码多;非受控 = DOM 自己管(defaultValue + ref 读值),代码少但难控制。电商表单(下单地址、优惠券)建议受控。
// 受控
<input value={form.name} onChange={e => setForm({...form, name: e.target.value})} />
// 非受控
<input defaultValue="Tom" ref={inputRef} />四、工程化与构建
22. Vite vs Webpack?
答案: Vite 开发用原生 ESM 按需编译,启动秒开;Webpack 开发要全量打包,项目大就慢。生产 Vite 也会用 Rollup 打包(浏览器对 ESM 请求太多)。
Vite 开发:浏览器直接请求模块,esbuild 预构建依赖,改哪个编译哪个 Webpack 开发:先全量打包成 bundle,再启动 dev server 23. 前端工程化包含什么?
答案: 模块化(ESM 拆代码)、组件化(复用 UI)、规范化(ESLint/Prettier/commit 规范)、自动化(CI/CD、自动部署、自动化测试)、性能化(构建优化、分包)。
24. 通用组件封装思路?
答案: props 驱动 + 插槽扩展 + 事件通知 + 样式可控(class 透传)。
// 商品卡片组件设计
<GoodsCard:data="goods" // 数据 @click="onCardClick" // 事件 >
<template #tag>限时购</template> // 插槽扩展
</GoodsCard>25. 小程序架构 + 多端方案?
答案: 小程序是双线程架构(逻辑层 JS 线程 + 渲染层 WebView),通信靠 setData;Taro/uni-app 把 Vue/React 代码编译成各端代码。
五、性能优化与兼容性
26. 首页首屏慢怎么排查?
答案: Performance 面板看瀑布图 → 分清网络/JS/渲染瓶颈 → 对症下药。
网络:CDN、HTTP2、gzip、图片压缩、懒加载 JS:分包、按需引入、Tree-shaking、减少主包 渲染:SSR/预渲染、骨架屏、减少重排 缓存:HTML no-cache、静态资源强缓存+hash 27. 大列表渲染?
答案: 虚拟滚动,只渲染可视区域;固定高度用 react-window / vue-virtual-scroller,原理 = 占位撑高 + translateY 偏移。
// 核心:只渲染 start~end
const start = Math.floor(scrollTop / itemHeight)
const end = start + visibleCount28. 浏览器兼容性?
答案: CSS 加前缀(autoprefixer)、降级写法;JS 用 Babel 转译 + polyfill(core-js);旧浏览器(如低版本安卓 WebView)不用可选链等新语法,或用 esbuild 降级。
user-select: none; /* 现代 / -webkit-user-select: none; / 旧 Safari */ 29. 埋点和性能监控?
答案: 手动埋点(业务事件)+ 自动埋点(PV/点击)+ Web Vitals 采集。
// 性能指标
new PerformanceObserver((list) => {
const lcp = list.getEntries().at(-1)sendBeacon('/log', { lcp: lcp.startTime }) }).observe({ type: 'largest-contentful-paint', buffered: true })
// 埋点
track('order_submit', { orderId, amount }) // 下单埋点,CPS 归属靠这个六、电商业务场景
30. 电商下单结算流程 + CPS 追踪 + 支付回调排查?
答案:
下单流程: 商品页 → 购物车 → 确认订单(地址/优惠券) → 提交订单(后端创建) → 拉起支付(微信/支付宝) → 支付成功回调 → 后端改订单状态 → 前端轮询/WebSocket 拿到已支付 → 跳订单详情
// CPS 追踪:推广链接带 pid,下单带上
// https://shop.com/p/123?pid=u_888
localStorage.setItem('cps_pid', params.pid) // 存到下单请求里
submitOrder({ goodsId: 123, pid: localStorage.getItem('cps_pid') })// 支付成功页面没跳转排查:
// 1. 看后端订单状态是否已变(轮询接口)
// 2. 看 WebSocket 是否收到推送(网络/重连)
// 3. 看支付回调是否成功(后端日志)
// 4. 前端兜底:定时轮询 + 手动刷新按钮