面试题库与参考回答
目录
- JavaScript 基础与底层原理
- TypeScript 深度
- CSS 与布局
- 浏览器核心原理
- Vue 深度
- React 深度
- 前端工程化
- 性能优化
- 安全
- 数据结构和算法
- 系统设计
- 开放性问题和反向提问
一、JavaScript 基础与底层原理
1.1 执行上下文与作用域链
Q: 什么是执行上下文?JavaScript 代码执行时经历了什么?
A: 执行上下文是 JavaScript 引擎执行代码时的运行环境,主要分为三种:
全局执行上下文:代码首次运行时的最外层环境,创建全局对象 window(浏览器)或 global(Node.js),并设置 this 指向全局对象。
函数执行上下文:每次调用函数时创建,每个函数都有自己独立的上下文。
Eval 执行上下文:不推荐使用。
执行过程:
- 创建阶段:创建变量对象(VO/AO),建立作用域链,确定 this 指向
- 执行阶段:逐行执行代码,对变量赋值,执行函数调用
作用域链:由当前执行上下文的变量对象 + 外层嵌套函数的变量对象组成,用于解析变量时的查找顺序(就近原则)。
var a = 1
function outer() {
var b = 2
function inner() {
var c = 3
console.log(a, b, c) // 1, 2, 3 — 作用域链向上追溯
}
inner()
}
outer()
Q: let、const、var 的区别是什么?
A:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 声明提升,初始化不提升(undefined) | 声明提升,但存在暂时性死区 | 同 let |
| 重复声明 | 允许 | 不允许 | 不允许 |
| 全局属性 | 会成为 window 属性 | 不会 | 不会 |
| 初始值 | 可无 | 可无 | 必须有 |
暂时性死区(TDZ):
console.log(a) // Cannot access 'a' before initialization
let a = 1
let 在创建阶段到执行阶段之间存在 TDZ,此期间访问变量会报错。
为什么不用 var:
- 函数作用域导致变量泄露(如循环闭包问题)
- 可重复声明造成不可预期覆盖
- 没有块级作用域,if/for 块内的变量污染外部
1.2 闭包与内存
Q: 什么是闭包?有什么应用场景?有什么潜在问题?
A: 闭包是指函数能够记住并访问其词法作用域(外部函数的变量),即使该函数在其词法作用域之外执行。
function makeCounter() {
let count = 0 // 私有变量
return {
increment: () => ++count,
getCount: () => count
}
}
const counter = makeCounter()
counter.increment() // 1
counter.increment() // 2
counter.getCount() // 2 — count 无法被外部直接访问
应用场景:
- 模块化:封装私有变量,暴露公共 API
- 函数工厂:创建带预设参数的函数
- 防抖/节流:保存定时器状态
- 缓存:保存计算结果(记忆化)
- 循环中的异步:在 setTimeout 中保存正确的循环变量
潜在问题——内存泄漏:
// 问题:大型对象被闭包持有无法释放
function heavy() {
const bigData = new Array(1000000)
return () => bigData[0] // bigData 无法被 GC
}
解决方案:
- 手动置 null:bigData = null
- 避免在不需要时创建闭包
- Vue/React 中及时清理定时器、事件监听
1.3 原型与继承
Q: JavaScript 的原型链是如何工作的?如何实现继承?
A: JavaScript 通过原型链实现对象间的继承关系。
核心概念:
- 每个对象都有一个 [[Prototype]](浏览器中通过 proto 访问)
- 函数有一个 prototype 属性,指向一个包含 constructor 的对象
- 当访问对象的属性时,如果对象本身没有,会沿原型链向上查找
原型链:Object.prototype 是链的顶端,null 是原型链的终点。
function Animal(name) {
this.name = name
}
Animal.prototype.speak = function() {
console.log(this.name + ' makes a noise.')
}
function Dog(name, breed) {
Animal.call(this, name) // 借用构造函数
this.breed = breed
}
// 原型链继承
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
const dog = new Dog('Rex', 'Shepherd')
dog.speak() // Rex makes a noise.
dog instanceof Animal // true
ES6 Class 继承:
class Animal {
constructor(name) { this.name = name }
speak() { console.log(this.name + ' speaks') }
}
class Cat extends Animal {
constructor(name, color) {
super(name)
this.color = color
}
}
1.4 this 指向
Q: this 的指向有哪些规则?如何改变 this?
A: JavaScript 中 this 的指向取决于调用方式:
规则一:默认绑定(独立函数调用)
function foo() { console.log(this) }
foo() // 严格模式下 undefined,非严格模式下 window
规则二:隐式绑定(对象方法调用)
const obj = { name: 'A', foo() { console.log(this.name) } }
obj.foo() // 'A' — this 指向 obj
规则三:显式绑定(call/apply/bind)
function foo() { console.log(this.name) }
const obj = { name: 'B' }
foo.call(obj) // 'B'
foo.apply(obj) // 'B'
const fn = foo.bind(obj)
fn() // 'B'
规则四:new 绑定
function Person(name) { this.name = name }
const p = new Person('C')
p.name // 'C'
规则五:箭头函数
- 箭头函数没有自己的 this,它继承外层第一个普通函数的 this
- 箭头函数无法用 call/apply/bind 改变 this
改变 this 的优先级: new > 显式绑定(bind/call/apply)> 隐式绑定 > 默认绑定
1.5 事件循环与异步
Q: 解释 JavaScript 的事件循环机制。什么是宏任务和微任务?
A: JavaScript 是单线程的,通过事件循环实现异步操作。
核心概念:
- 调用栈(Call Stack):执行同步代码
- 任务队列(Task Queue):宏任务队列
- 微任务队列(Microtask Queue):Promise 回调、MutationObserver、queueMicrotask
执行顺序:
- 执行同步代码(调用栈清空)
- 执行所有微任务(清空微任务队列)
- 执行一个宏任务(从任务队列取一个)
- 重复步骤 2-3
console.log('1') // 同步
setTimeout(() => console.log('2'), 0) // 宏任务
Promise.resolve().then(() => console.log('3')) // 微任务
console.log('4') // 同步
// 输出顺序: 1 → 4 → 3 → 2
常见宏任务:setTimeout、setInterval、I/O、UI 渲染、setImmediate(Node)
常见微任务:Promise.then/.catch/.finally、MutationObserver、queueMicrotask
async/await 原理:
- async 函数返回一个 Promise
- await 后的代码变为微任务
async function test() {
console.log('1')
await Promise.resolve()
console.log('2') // 微任务,在下一个宏任务前执行
}
test()
console.log('3') // 同步
// 输出: 1 → 3 → 2
1.6 Promise 与 Generator
Q: Promise 有哪些状态?then/catch/finally 的返回值是什么?
A: Promise 有三种状态:
- pending:初始状态
- fulfilled:操作成功完成
- rejected:操作失败
状态一旦改变就不可逆。
then/catch/finally 返回值:
Promise.resolve(1)
.then(x => x + 1) // 返回 2(值穿透)
.then(x => { throw new Error('err') }) // 抛出错误,进入 rejected
.catch(err => 0) // 捕获错误,返回 0,进入 fulfilled
.finally(() => console.log('done')) // 不影响返回值
.then(x => console.log(x)) // 打印 0
Promise 静态方法:
Promise.all([p1, p2, p3]) // 所有成功才成功,一个失败则整体失败
Promise.allSettled([p1, p2]) // 等所有结果(成功/失败),总是成功
Promise.race([p1, p2]) // 返回最快 resolved/rejected 的结果
Promise.any([p1, p2]) // 返回最快 resolved 的,一个失败可接受
1.7 深拷贝与浅拷贝
Q: 如何实现深拷贝?有什么优缺点?
A:
浅拷贝:只复制对象的第一层属性,引用类型仍共享。
const copy1 = { ...obj }
const copy2 = Object.assign({}, obj)
深拷贝:
// 方式一:JSON 序列化(简单但有局限)
const deep1 = JSON.parse(JSON.stringify(obj))
// 局限:undefined、函数、Symbol、循环引用、Date、RegExp 无法正确拷贝
// 方式二:递归实现
function deepClone(obj, weakMap = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj
if (weakMap.has(obj)) return weakMap.get(obj) // 处理循环引用
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
const clone = Array.isArray(obj) ? [] : {}
weakMap.set(obj, clone)
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], weakMap)
}
return clone
}
// 方式三:structuredClone(原生 API,2022+)
const deep2 = structuredClone(obj)
1.8 模块化
Q: ES Module 和 CommonJS 有什么区别?
A:
| 特性 | ES Module | CommonJS |
|---|---|---|
| 语法 | import/export | require/module.exports |
| 加载方式 | 静态解析,编译时确定 | 动态执行,运行时确定 |
| 值拷贝 | 只读绑定(动态映射) | 值拷贝 |
| this | undefined | module.exports |
| 循环引用 | 可以(但值为 undefined) | 可能卡住 |
| 异步/同步 | 异步(浏览器原生支持) | 同步 |
// ES Module
export const name = 'A'
export function foo() {}
import { name, foo } from './module'
import * as m from './module'
// CommonJS
const { name } = require('./module')
module.exports = { name }
二、TypeScript 深度
2.1 类型基础
Q: TypeScript 和 JavaScript 的核心区别是什么?为什么要用 TypeScript?
A: TypeScript 是 JavaScript 的超集,主要增加了静态类型检查。
核心区别:
- TS 在编译阶段进行类型检查,JS 在运行时才报错
- TS 需要编译成 JS 才能在浏览器/Node.js 运行
- TS 支持接口、泛型、枚举、元组等类型语法
为什么要用 TypeScript:
- 类型安全:编译期发现类型错误,减少运行时 bug
- 代码提示:IDE 智能提示更准确,开发效率高
- 可维护性:大型项目中,类型即文档,新人容易上手
- 重构友好:修改类型后,编译器告诉你所有调用点
- 生态友好:主流框架(Vue3、React)都支持 TypeScript
2.2 泛型
Q: 什么是泛型?有哪些常见的泛型工具类型?
A: 泛型允许创建可复用的组件,同时保持类型安全。
// 泛型函数
function identity<T>(arg: T): T {
return arg
}
identity<string>('hello')
identity(123) // 类型推断
// 泛型约束
function getLength<T extends { length: number }>(arg: T): number {
return arg.length
}
// 泛型接口
interface KeyValuePair<K, V> {
key: K
value: V
}
const pair: KeyValuePair<string, number> = { key: 'age', value: 25 }
内置工具类型:
Partial<T> // 所有属性变为可选
Required<T> // 所有属性变为必填
Readonly<T> // 所有属性变为只读
Pick<T, K> // 从 T 中选取部分属性
Omit<T, K> // 从 T 中排除部分属性
Record<K, V> // 创建键值对类型
Extract<T, U> // 取交集
Exclude<T, U> // 取差集
NonNullable<T> // 排除 null 和 undefined
ReturnType<F> // 获取函数返回值类型
Parameters<F> // 获取函数参数类型
2.3 装饰器
Q: 什么是装饰器?在 Vue3 和 Angular 中如何使用?
A: 装饰器是一种特殊类型的声明,用于修改类或类的成员的行为。
类装饰器:
function sealed(constructor: Function) {
Object.seal(constructor)
Object.seal(constructor.prototype)
}
@sealed
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
}
方法装饰器:
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function(...args: any[]) {
console.log(`Calling ${key} with`, args)
return original.apply(this, args)
}
return descriptor
}
class Calculator {
@log
add(a: number, b: number) {
return a + b
}
}
Vue3 组合式 API vs 装饰器:Vue3 推荐 Composition API,而 Angular 仍广泛使用装饰器(@Component、@Inject 等)。
三、CSS 与布局
3.1 盒模型与布局
Q: CSS 盒模型是什么?content-box 和 border-box 的区别?
A: CSS 盒模型描述了元素在页面上占据空间的方式。
标准盒模型(W3C):
- width = content 的宽度
- 总宽度 = width + padding + border + margin
IE 盒模型(border-box):
- width = content + padding + border 的总宽度
- 总宽度 = width + margin
/* 现代浏览器默认 */
box-sizing: content-box;
/* 实际项目推荐 */
box-sizing: border-box; /* padding/border 包含在 width 内 */
Q: Flex 布局和 Grid 布局各自适用什么场景?
A:
Flexbox(一维布局):
- 适合单一方向排列(行或列)
- 项目之间的空间分配、对齐
- 典型场景:导航栏、卡片列表、表单项
/* 常见用法:垂直居中 */
.container {
display: flex;
justify-content: center; /* 主轴居中 */
align-items: center; /* 交叉轴居中 */
}
Grid(二维布局):
- 同时控制行和列
- 适合页面整体布局、仪表盘、相册
.container {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
gap: 20px;
}
选择原则:简单一维排列用 Flex,复杂二维布局用 Grid,可以组合使用。
3.2 居中方案
Q: 如何让一个元素完美居中?列举所有方法。
A:
/* 方法一:Flexbox */
.container {
display: flex;
justify-content: center;
align-items: center;
}
/* 方法二:Grid */
.container {
display: grid;
place-items: center; /* 简写,等同于 justify-items + align-items */
}
/* 方法三:绝对定位 + transform */
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 方法四:绝对定位 + margin auto */
.container { position: relative }
.box {
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
margin: auto;
}
/* 方法五:绝对定位 + calc */
.box {
position: absolute;
top: calc(50% - 高度/2);
left: calc(50% - 宽度/2);
}
/* 方法六:行内元素垂直居中 */
.container {
height: 200px;
line-height: 200px;
text-align: center;
}
/* 方法七:table-cell */
.container {
display: table-cell;
vertical-align: middle;
text-align: center;
}
3.3 BFC 与清除浮动
Q: 什么是 BFC?有哪些创建 BFC 的方式?
A: BFC(Block Formatting Context)是块级格式化上下文,决定了块级盒子的布局规则。
创建 BFC 的方式:
- 根元素 html
- float 不为 none
- position 为 absolute 或 fixed
- display 为 inline-block、table-cell、flex、grid
- overflow 不为 visible(hidden、auto、scroll)
- contain 属性为 layout、content、strict
BFC 的特点:
- 同一个 BFC 内,垂直方向的外边距会合并
- BFC 内的浮动不会影响外部布局
- BFC 可以包含浮动元素(清除浮动)
BFC 应用场景:
/* 清除浮动 */
.parent {
overflow: hidden; /* 创建 BFC 包含浮动 */
}
/* 防止 margin 合并 */
.parent {
overflow: hidden;
}
.child {
margin-top: 20px;
}
3.4 重排与重绘
Q: 什么是重排(reflow)和重绘(repaint)?如何优化?
A: 浏览器渲染流程:HTML → DOM → CSSOM → Render Tree → Layout → Paint → Composite
重排(Reflow):几何属性改变时(宽高、位置、字体大小),浏览器重新计算布局。 重绘(Repaint):外观改变但不影响布局(颜色、背景、visibility),重新绘制像素。
触发重排的操作:
- 添加/删除 DOM 元素
- 元素位置、尺寸改变
- 内容变化(文本或图片大小)
- 浏览器窗口大小改变
优化策略:
- 批量修改 DOM:使用 documentFragment 或将 DOM 离线(display:none)
- 使用 transform/opacity:仅触发 composite,不触发 layout/paint
- 避免频繁读取布局属性:读取后缓存
- CSS 动画使用 transform:GPU 加速
- will-change:提前告知浏览器元素将变化
// 批量修改
const fragment = document.createDocumentFragment()
items.forEach(item => {
const li = document.createElement('li')
li.textContent = item
fragment.appendChild(li)
})
list.appendChild(fragment)
// 使用 transform 动画
element.style.transform = 'translateX(100px)' // 仅重绘
element.style.left = '100px' // 触发重排
四、浏览器核心原理
4.1 浏览器渲染机制
Q: 描述浏览器的渲染过程。从 HTML 到页面显示经历了什么?
A:
完整渲染流水线:
- HTML 解析:将 HTML 文本解析成 DOM 树
- CSS 解析:将 CSS 解析成 CSSOM 树
- DOM + CSSOM → Render Tree:只包含可见节点
- Layout(布局):计算每个节点的几何信息(位置、大小)
- Paint(绘制):将节点绘制成多个图层的绘制记录
- Composite(合成):将图层合成为最终画面,上传 GPU 显示
关键优化点:
- CSS 放 head 中,JS 放 body 末尾(defer)
- 使用 requestAnimationFrame 安排动画
- 避免同步布局(读写分离)
4.2 浏览器缓存
Q: 浏览器缓存策略有哪些?协商缓存和强缓存的区别?
A:
强缓存:不发送请求,直接使用本地缓存。
Cache-Control: max-age=3600 // 相对时间(HTTP/1.1)
Expires: Wed, 21 Oct 2026 07:28:00 GMT // 绝对时间(HTTP/1.0)
协商缓存:发送请求,由服务器决定是否使用缓存。
Last-Modified / If-Modified-Since // 文件修改时间
ETag / If-None-Match // 文件内容 hash
缓存优先级:
- Service Worker(最高)
- Memory Cache(内存)
- Disk Cache(磁盘)
- Push Cache(HTTP/2 的 push)
用户行为对缓存的影响:
- 强制刷新(Ctrl+Shift+R):跳过所有缓存
- 地址栏回车:先检查过期,未过期用缓存
- F5 刷新:跳过强缓存,走协商缓存
4.3 跨域
Q: 什么是跨域?有哪些解决方案?
A: 浏览器同源策略(Same-Origin Policy)限制了不同源之间的资源访问。
同源定义:协议 + 域名 + 端口 三者相同。
解决方案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| CORS | 服务端设置 Access-Control-Allow-Origin | API 跨域(最推荐) |
| JSONP | 利用 script 标签不受同源限制(仅 GET) | 老项目兼容 |
| 代理服务器 | Nginx/Node 转发请求到同源 | 前端开发环境 |
| postMessage | 窗口间消息通信 | iframe 通信 |
| WebSocket | 协议本身不受同源限制 | 实时通信 |
| document.domain | 共享子域名 iframe | 同主域不同子域 |
CORS 预检请求:
- 非简单请求(PUT/DELETE/Custom Header)会先发 OPTIONS 预检
- 需服务器配置 Allow-Methods、Allow-Headers、Allow-Origin
4.4 垃圾回收
Q: 浏览器垃圾回收机制是什么?什么是内存泄漏?
A: JavaScript 使用自动垃圾回收(GC),主要算法:
引用计数(已废弃):对象被引用次数归零时回收。无法处理循环引用。
标记清除(现代浏览器主流):
- 从根对象(window/global)出发,标记可达对象
- 清除未被标记的对象
V8 引擎的 GC 优化:
- 分代回收:新生代(短生命周期对象,用 Scavenge)、老生代(长生命周期,用 Mark-Sweep + Mark-Compact)
- 增量标记:将 GC 过程分片,避免长停顿
- 并发/并行回收:利用多核并行处理
常见内存泄漏场景:
// 1. 全局变量泄漏
function leak() { bigData = new Array(1000000) }
// 2. 定时器未清理
setInterval(() => { /* ... */ }, 1000)
clearInterval(id) // 组件卸载时必须清理
// 3. 事件监听未移除
element.addEventListener('click', handler)
element.removeEventListener('click', handler)
// 4. 闭包持有大对象
function heavy() {
const data = new Array(1000000)
return () => data[0]
}
// 5. Vue/React 中:
// - 未清理的订阅/事件监听
// - 组件中创建的定时器
// - 全局 store 的冗余数据
五、Vue 深度
5.1 响应式原理
Q: Vue2 和 Vue3 的响应式原理有什么区别?
A:
Vue2:Object.defineProperty
// Vue2 的响应式实现
function defineReactive(obj, key, val) {
const dep = new Dep() // 依赖收集
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.depend() // 收集依赖
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
dep.notify() // 通知更新
}
})
}
Vue2 的问题:
- 无法检测数组下标变化(通过重写数组方法解决)
- 需要 $set(Vue.set)处理新增属性
- 深度递归开销大
Vue3:Proxy + Reflect
// Vue3 的响应式实现
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return typeof res === 'object' ? reactive(res) : res // 深度代理
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 通知更新
return result
}
})
}
Vue3 的优势:
- Proxy 可以检测数组下标、新增属性、删除属性
- 无需递归,性能更好
- 支持 Map、Set、WeakMap、WeakSet
Q: 什么是依赖收集?Vue 如何知道哪个组件需要更新?
A:
响应式系统的核心:
- Track(追踪):在 get 时收集当前活跃的 Watcher
- Trigger(触发):在 set 时通知所有依赖的 Watcher 更新
// 依赖管理器
class Dep {
constructor() { this.subs = [] }
addSub(watcher) { this.subs.push(watcher) }
notify() { this.subs.forEach(w => w.update()) }
}
// 渲染 watcher
class Watcher {
constructor(vm, expOrFn, callback) {
this.vm = vm
this.getter = expOrFn
this.callback = callback
this.get() // 执行 getter,将自身设为全局 activeWatcher
}
get() {
pushTarget(this) // 设置 activeWatcher
this.getter.call(this.vm, this.vm)
popTarget() // 恢复上一个 activeWatcher
}
}
流程:
- 组件渲染时创建 Watcher,执行 render()
- render() 读取响应式数据,触发 get()
- get() 中 activeWatcher 订阅 Dep
- 数据变化时,Dep 通知所有 Watcher,触发 re-render
5.2 虚拟 DOM
Q: 什么是虚拟 DOM?Vue 为什么使用它?它的性能一定比直接操作 DOM 快吗?
A:
虚拟 DOM(VNode):用 JavaScript 对象描述真实 DOM 结构。
// 真实 DOM
<div class="container">
<h1>Title</h1>
<p>Content</p>
</div>
// 虚拟 DOM
{
tag: 'div',
props: { class: 'container' },
children: [
{ tag: 'h1', children: 'Title' },
{ tag: 'p', children: 'Content' }
]
}
Vue 为什么使用 VNode:
- 跨平台:同一套 VNode 可以渲染到 DOM、SSR、Native(Weex)
- 减少重排重绘:通过 Diff 算法找出最小更新范围
- 更好的开发体验:组件化、模板语法
性能真相:
- 少量操作(1-2个 DOM 变更):直接操作 DOM 更快(VNode 有额外开销)
- 大量操作:VNode + Diff 算法远快于直接 DOM
- VNode 不是银弹,滥用仍会性能问题
Diff 算法:
- 同级比较(只看同一层级,不跨级)
- key 的作用:复用 DOM 节点,减少重排
- 策略:先比较标签、属性、最后比较子节点
5.3 computed vs watch
Q: computed 和 watch 有什么区别?
A:
| 特性 | computed | watch |
|---|---|---|
| 缓存 | 基于响应式依赖,缓存结果 | 不缓存,每次都执行 |
| 适用 | 派生状态(基于已有数据计算) | 副作用(执行异步操作、调用 API) |
| 返回值 | 必须有返回值 | 不必须有 |
| 同步/异步 | 同步 | 支持异步 |
// computed:计算属性,依赖响应式数据,自动缓存
const fullName = computed(() => firstName.value + ' ' + lastName.value)
// watch:监听数据变化,执行副作用
watch(userName, (newVal, oldVal) => {
console.log('name changed:', newVal)
})
// watch 深度监听
watch(() => state.obj, (newVal) => { /* ... */ }, { deep: true })
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(count.value) // 自动追踪 count
})
5.4 Vue3 Composition API
Q: Composition API 相比 Options API 有什么优势?
A:
逻辑复用:
- Options API 用 mixins:来源不清晰,命名冲突
- Composition API 用 composables(hooks):来源清晰,逻辑可组合
// composable: useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const handleResize = () => {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', handleResize))
onUnmounted(() => window.removeEventListener('resize', handleResize))
return { width, height }
}
// 使用:逻辑按功能分组,来源清晰
import { useWindowSize } from './useWindowSize'
import { useWebSocket } from './useWebSocket'
export default {
setup() {
const { width, height } = useWindowSize()
const { messages, send } = useWebSocket()
// ...
}
}
其他优势:
- 更好的 TypeScript 支持
- 代码量减少(无需 data/methods/computed 分开写)
- 更好的 Tree-shaking
- 更好的逻辑内聚(相关逻辑集中在一起,而非分散在多个选项中)
5.5 Vue Router
Q: Vue Router 的原理是什么?有哪几种路由模式?
A:
Hash 模式:
- 使用 URL 的 hash(#xxx)作为路由
- 监听 hashchange 事件
- 优点:兼容性好,不需要服务器配置
- 缺点:URL 不好看,有#
History 模式:
- 使用 HTML5 History API(pushState/replaceState)
- 监听 popstate 事件(浏览器前进/后退)
- 优点:URL 好看
- 缺点:需要服务器配置(所有路径都返回 index.html)
Abstract 模式:
- 在非浏览器环境(如 Node.js、SSR)使用
导航守卫:
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
5.6 Vue3 进阶
Q: nextTick 的原理是什么?为什么需要它?
A:
为什么需要 nextTick: Vue 的 DOM 更新是异步的。修改响应式数据后,DOM 不会立即更新,而是在下一个事件循环中批量更新。
// 不用 nextTick:获取的是旧值
this.message = 'updated'
console.log(this.$refs.el.innerText) // 旧值
// 使用 nextTick:DOM 已更新
this.message = 'updated'
this.$nextTick(() => {
console.log(this.$refs.el.innerText) // 新值
})
原理:Vue 2 用 Promise + MutationObserver + setTimeout(降级);Vue 3 用 Promise + Scheduler。
执行顺序:
- 同步代码执行
- 微任务(Promise.then, nextTick callback)
- 下一个宏任务(setTimeout)
六、React 深度
6.1 Fiber 架构
Q: React Fiber 是什么?解决了什么问题?
A:
Fiber 的核心思想:将渲染工作拆分成小单元,可以中断和恢复。
旧架构(Stack Reconciler):
- 递归更新,一旦开始不能中断
- 大型更新会造成主线程阻塞,用户交互卡顿
Fiber 架构:
- 每个 VNode 对应一个 Fiber 节点
- 每个 Fiber 节点有优先级(动画高、低优先级低)
- 可中断、可恢复、可复用已完成的工作
- 双缓冲:一次只更新一个"工作树",完成后才切换
Fiber 的 return:将工作单元的结果返回给父节点,父节点可以收集子节点的完成结果。
时间切片:每帧预留 5ms 给浏览器执行交互,保证不卡顿。
6.2 Hooks
Q: useEffect 和 useLayoutEffect 有什么区别?
A:
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 时机 | DOM 更新后,浏览器渲染前(异步) | DOM 更新后,同步执行 |
| 是否阻塞渲染 | 否 | 是 |
| SSR | 在服务端执行 | 不执行(SSR 时为空函数) |
| 适用场景 | 异步操作、订阅、数据获取 | 同步 DOM 操作、测量布局 |
// useEffect:异步,不阻塞渲染
useEffect(() => {
document.title = count
}, [count])
// useLayoutEffect:同步,DOM 操作前执行
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect()
// 同步测量/修改 DOM,避免闪烁
}, [])
useRef vs useState:
- useState:触发组件重新渲染,异步
- useRef:修改不触发重渲染,.current 同步更新
Q: 自定义 Hook 有什么应用场景?
A:
// 1. 数据获取
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return { data, loading, error }
}
// 2. 媒体查询
function useMediaQuery(query) {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
)
useEffect(() => {
const mql = window.matchMedia(query)
const handler = (e) => setMatches(e.matches)
mql.addEventListener('change', handler)
return () => mql.removeEventListener('change', handler)
}, [query])
return matches
}
// 3. 防抖
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const id = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(id)
}, [value, delay])
return debouncedValue
}
6.3 Redux vs Zustand
Q: Redux 的工作原理?和 Zustand 有什么区别?
A:
Redux 三大原则:
- 单一数据源(store)
- State 只读(通过 action 触发更新)
- 纯函数修改(reducer 必须是纯函数)
Redux 流程:
用户交互 → dispatch(action) → reducer(newState) → subscribe(通知) → UI 更新
Redux vs Zustand:
| 特性 | Redux | Zustand |
|---|---|---|
| 代码量 | 多(action、reducer、type、selector) | 少(一组 store) |
| 中间件 | 支持(redux-thunk、saga) | 通过 middleware 扩展 |
| DevTools | 强大的调试工具 | 基础工具 |
| Boilerplate | 多 | 少 |
| 适用 | 大型复杂应用 | 中小型应用 |
Zustand 示例:
import { create } from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set(state => ({ count: state.count + 1 })),
reset: () => set({ count: 0 })
}))
function Counter() {
const { count, increment } = useStore()
return <button onClick={increment}>{count}</button>
}
6.4 React 渲染优化
Q: React 如何避免不必要的重新渲染?
A:
1. React.memo(函数组件):
const MyComponent = React.memo(function MyComponent({ name }) {
return <div>{name}</div>
})
// 进阶:自定义比较函数
const MyComponent = React.memo(
function MyComponent({ name, age }) {
return <div>{name} - {age}</div>
},
(prevProps, nextProps) => {
return prevProps.name === nextProps.name // 只比较 name
}
)
2. useMemo 和 useCallback:
// 缓存计算结果
const expensiveValue = useMemo(() => computeExpensive(a, b), [a, b])
// 缓存回调函数(防止子组件不必要的重渲染)
const handleClick = useCallback(() => {
doSomething(count)
}, [count])
3. 合理使用 key:
- 列表中用稳定、唯一的 key
- 避免用数组索引作为 key(删除项目时索引变化,导致错误复用)
4. 状态提升的边界:
- 将频繁变化的状态放在子组件,避免父组件重渲染影响子组件
七、前端工程化
7.1 构建工具
Q: Webpack 和 Vite 的核心区别是什么?为什么 Vite 更快?
A:
Webpack 原理:
- 启动时扫描所有依赖,构建完整的依赖图
- 将所有模块打包成少量 bundle
- 开发时需要全量构建,修改后重新构建
- HMR(热模块替换)需要重新构建相关模块
Vite 原理:
- 利用浏览器的 ES Module 原生支持(ESM)
- 开发时仅编译当前需要的文件,不打包
- 生产时用 Rollup 打包
Vite 为什么快:
- No Bundle:直接提供 ESM,浏览器自己处理模块解析
- 按需编译:只编译当前访问的页面
- 预构建依赖:node_modules 中的 CommonJS 依赖预构建为 ESM
- HMR:模块级别热更新,无需重新加载整个应用
Vite 配置示例:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
three: ['three'],
vendor: ['vue', 'vue-router']
}
}
}
},
optimizeDeps: {
include: ['three', 'vue'] // 预构建依赖
}
})
7.2 CI/CD
Q: 前端 CI/CD 流程是怎样的?GitHub Actions 如何配置?
A:
典型 CI/CD 流程:
代码提交 → 静态检查(Lint) → 单元测试 → 构建 → E2E 测试 → 部署
GitHub Actions 示例:
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test:unit
- run: npm run test:e2e
deploy:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
- name: Deploy to Server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_KEY }}
script: |
cd /var/www/app
git pull
npm ci
npm run build
pm2 restart app
7.3 TypeScript 配置
Q: tsconfig.json 核心配置有哪些?如何优化编译速度和类型检查?
A:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler", // Vite/Webpack 场景用 bundler
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve", // Vue/React 保留 JSX
// 严格模式
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
// 路径别名
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
// 输出
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// 性能优化
"skipLibCheck": true, // 跳过 .d.ts 检查
"incremental": true, // 增量编译
"isolatedModules": true, // 每个文件独立转译
// 模块解析
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
八、性能优化
8.1 Core Web Vitals
Q: 什么是 Core Web Vitals?LCP、LCP、CLS 的优化方法?
A:
Core Web Vitals 三大指标:
LCP(Largest Contentful Paint):最大内容绘制时间
- 目标:< 2.5 秒
- 优化方法:
- 图片优化(WebP/AVIF、懒加载、预加载)
- 优化服务器响应时间
- 移除阻塞渲染的资源
<!-- 预加载关键图片 -->
<link rel="preload" href="hero.webp" as="image">
FID/INP(交互到下一绘制):输入响应时间
- 目标:< 100ms
- 优化方法:
- 减少主线程阻塞(代码分割、懒加载)
- 优化事件处理器
- Web Worker 处理耗时计算
CLS(Cumulative Layout Shift):累计布局偏移
- 目标:< 0.1
- 优化方法:
- 给图片和视频设置宽高
- 避免动态注入内容(广告、弹窗)
- 使用 transform 做动画
/* 预留图片空间,防止 CLS */
img {
width: 100%;
height: auto;
aspect-ratio: 16/9; /* 固定比例 */
}
8.2 首屏优化
Q: 如何优化首屏加载速度?
A:
核心策略:
1. 代码层面:
- 代码分割(Code Splitting):按路由/组件拆分
- 懒加载(Lazy Loading):dynamic import
- Tree Shaking:移除未使用的代码
// 路由懒加载
const Home = () => import('./Home.vue')
// 组件懒加载
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'))
2. 资源层面:
- 压缩图片(WebP、AVIF)
- 图片懒加载
- CSS/JS 合并与压缩
- 使用 CDN
<!-- 图片懒加载 -->
<img loading="lazy" src="image.webp" alt="...">
<!-- 关键 CSS 内联 -->
<style>/* critical CSS */</style>
<!-- 非关键 CSS 异步加载 -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
3. 网络层面:
- DNS 预解析:
- 预连接:
- HTTP/2 多路复用
- 缓存策略优化
4. 渲染层面:
- SSR/SSG:首屏直出 HTML
- 服务端压缩(Gzip/Brotli)
8.3 Vue/React 性能优化
Q: Vue/React 应用中有哪些常用的性能优化手段?
A:
Vue 优化:
// 1. v-show vs v-if
// v-if:条件不渲染,适合不频繁切换
// v-show:渲染但隐藏,适合频繁切换
// 2. v-for 加 key,避免用 index
<item v-for="item in list" :key="item.id" /> // ✅
<item v-for="(item, index) in list" :key="index" /> // ❌
// 3. v-memo:缓存模板子树
<div v-memo="[selectedId]">
<ExpensiveComponent />
</div>
// 4. keep-alive:缓存组件状态
<keep-alive include="Home,About" :max="10">
<router-view />
</keep-alive>
// 5. shallowRef:只监听第一层
const state = shallowRef({ count: 0, deep: { nested: 1 } })
state.value.deep.nested = 2 // 不触发更新
// 6. defineAsyncComponent:异步组件
const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'))
React 优化:
// 1. React.lazy + Suspense
const Home = React.lazy(() => import('./Home'))
<Suspense fallback={<Loading />}>
<Home />
</Suspense>
// 2. useMemo / useCallback
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])
const memoizedFn = useCallback(() => doSomething(count), [count])
// 3. React.memo
const MemoizedComponent = React.memo(Component, (prev, next) => {
return prev.name === next.name // 自定义比较
})
// 4. 虚拟列表:大列表只渲染可见区域
import { FixedSizeList } from 'react-window'
九、安全
9.1 XSS 与 CSRF
Q: 什么是 XSS 攻击?有哪些类型?如何防范?
A:
XSS(跨站脚本攻击):攻击者在网页中注入恶意脚本。
三种类型:
- 存储型 XSS:恶意代码存储在服务器(数据库、评论)
- 反射型 XSS:恶意代码通过 URL 参数传递
- DOM 型 XSS:纯前端代码漏洞,恶意数据直接操作 DOM
<!-- 反射型 XSS 示例 -->
<!-- URL: https://example.com?q=<script>alert(1)</script> -->
<!-- 服务器将 q 参数直接渲染到页面 -->
防范措施:
1. 内容转义:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
2. CSP(内容安全策略):
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self' 'unsafe-inline'">
3. HttpOnly Cookie:
- 设置 Cookie 的 HttpOnly 属性,防止 JavaScript 读取
4. 输入验证 + 输出编码:
- 服务端验证输入格式
- 输出时根据上下文(HTML/JS/CSS/URL)做不同编码
Q: 什么是 CSRF?如何防护?
A:
CSRF(跨站请求伪造):利用用户已登录的身份,伪造请求执行操作。
攻击流程:
- 用户登录 A 网站,Cookie 被保存
- 用户访问 B 网站,B 网站中有自动提交的表单/脚本
- 浏览器自动携带 A 网站的 Cookie 发请求
- A 网站认为是用户本人操作,执行请求
防护措施:
1. CSRF Token:
// 服务端生成随机 token
// 响应 HTML 中包含 token
<input type="hidden" name="csrf_token" value="random_token">
// 提交时验证
app.post('/api/action', (req, res) => {
if (req.body.csrf_token !== session.csrf_token) {
return res.status(403).json({ error: 'CSRF' })
}
// 处理请求
})
2. SameSite Cookie:
Set-Cookie: session=abc123; SameSite=Strict
- SameSite=Strict:完全禁止跨站携带
- SameSite=Lax:允许导航跳转携带,禁止子请求携带
- SameSite=None:允许跨站(需 Secure)
3. 验证来源(Origin/Referer):
const origin = req.headers.origin
const referer = req.headers.referer
if (origin !== 'https://mysite.com') {
return res.status(403)
}
9.2 其他安全话题
Q: 什么是 CSP?如何配置?
A:
CSP(Content Security Policy):通过 HTTP 响应头或 meta 标签,告诉浏览器允许加载哪些资源。
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-random123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://cdn.example.com;
connect-src 'self' https://api.example.com;
font-src 'self' https://fonts.gstatic.com;
frame-ancestors 'none';">
常用指令:
- default-src:默认来源
- script-src:JavaScript 来源
- style-src:CSS 来源
- img-src:图片来源
- connect-src:AJAX/WebSocket 请求来源
- frame-ancestors:可被嵌入的来源(防止 clickjacking)
十、数据结构和算法
10.1 常见算法
Q: 手写一个防抖(debounce)和节流(throttle)函数。
A:
// 防抖:在事件触发 n 秒后执行,如果 n 秒内再次触发,重新计时
function debounce(fn, delay, immediate = false) {
let timer = null
return function(...args) {
const context = this
if (timer) clearTimeout(timer)
if (immediate && !timer) {
fn.apply(context, args)
}
timer = setTimeout(() => {
if (!immediate) {
fn.apply(context, args)
}
timer = null
}, delay)
}
}
// 使用
const debouncedSearch = debounce((query) => {
console.log('Searching:', query)
}, 300)
input.addEventListener('input', (e) => debouncedSearch(e.target.value))
// 节流:n 秒内只执行一次,稀释执行频率
function throttle(fn, delay) {
let last = 0
return function(...args) {
const context = this
const now = Date.now()
if (now - last >= delay) {
last = now
fn.apply(context, args)
}
}
}
// 使用
const throttledScroll = throttle(() => {
console.log('Scrolling:', window.scrollY)
}, 100)
window.addEventListener('scroll', throttledScroll)
Q: 实现一个简易的 Promise。
A:
class MyPromise {
constructor(executor) {
this.state = 'pending'
this.value = undefined
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) => {
if (this.state !== 'pending') return
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(fn => fn(value))
}
const reject = (reason) => {
if (this.state !== 'pending') return
this.state = 'rejected'
this.value = reason
this.onRejectedCallbacks.forEach(fn => fn(reason))
}
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handle = (fn, fallback) => {
try {
if (typeof fn === 'function') {
const result = fn(this.value)
resolve(result)
} else {
resolve(this.value)
}
} catch (err) {
reject(err)
}
}
if (this.state === 'fulfilled') {
handle(onFulfilled)
} else if (this.state === 'rejected') {
handle(onRejected)
} else {
this.onFulfilledCallbacks.push(() => handle(onFulfilled))
this.onRejectedCallbacks.push(() => handle(onRejected))
}
})
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(fn) {
return this.then(
value => { fn(); throw value },
reason => { fn(); throw reason }
)
}
static resolve(value) {
return new MyPromise(resolve => resolve(value))
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason))
}
static all(promises) {
return new MyPromise((resolve, reject) => {
const results = []
let count = 0
promises.forEach((p, i) => {
p.then(value => {
results[i] = value
count++
if (count === promises.length) resolve(results)
}, reject)
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(p => p.then(resolve, reject))
})
}
}
Q: 实现深拷贝(考虑循环引用和特殊类型)。
A:
function deepClone(target, map = new WeakMap()) {
// 处理原始类型
if (target === null || typeof target !== 'object') {
return target
}
// 处理循环引用
if (map.has(target)) {
return map.get(target)
}
// 处理 Date
if (target instanceof Date) {
return new Date(target.getTime())
}
// 处理 RegExp
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags)
}
// 处理 Map
if (target instanceof Map) {
const cloneMap = new Map()
map.set(target, cloneMap)
target.forEach((v, k) => {
cloneMap.set(deepClone(k, map), deepClone(v, map))
})
return cloneMap
}
// 处理 Set
if (target instanceof Set) {
const cloneSet = new Set()
map.set(target, cloneSet)
target.forEach(v => {
cloneSet.add(deepClone(v, map))
})
return cloneSet
}
// 处理数组和普通对象
const cloneTarget = Array.isArray(target) ? [] : {}
map.set(target, cloneTarget)
Object.keys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], map)
})
return cloneTarget
}
Q: 实现一个函数,将扁平数据结构转成树形结构。
A:
// 输入
const list = [
{ id: 1, name: 'A', parentId: null },
{ id: 2, name: 'B', parentId: 1 },
{ id: 3, name: 'C', parentId: 1 },
{ id: 4, name: 'D', parentId: 2 },
{ id: 5, name: 'E', parentId: 2 },
]
// 输出
function buildTree(list) {
const map = new Map()
const result = []
// 第一遍:建立 id → 节点的映射
list.forEach(item => {
map.set(item.id, { ...item, children: [] })
})
// 第二遍:建立父子关系
list.forEach(item => {
const node = map.get(item.id)
if (item.parentId === null) {
result.push(node)
} else {
const parent = map.get(item.parentId)
if (parent) {
parent.children.push(node)
}
}
})
return result
}
// 变体:仅用一次遍历(边插入边构建)
function buildTreeOncPass(list) {
const map = new Map()
const result = []
list.forEach(item => {
const node = { ...item, children: [] }
map.set(item.id, node)
if (item.parentId === null) {
result.push(node)
} else {
const parent = map.get(item.parentId)
if (parent) {
parent.children.push(node)
}
}
})
return result
}
10.2 常见数据结构
Q: Map 和 Object 的区别?WeakMap 有什么用?
A:
| 特性 | Map | Object |
|---|---|---|
| 键的类型 | 任意值 | 字符串或 Symbol |
| 键的顺序 | 插入顺序(多数实现) | 无序(除数字键) |
| 迭代 | 可迭代(for...of) | 不可直接迭代 |
| 大小 | Map.size | Object.keys().length |
| 性能 | 大量键值对时更优 | 小规模时足够 |
| 原型链 | 无(除非手动设置) | 有(可能有键名冲突) |
WeakMap:
- 键必须是对象(不能是原始值)
- 键是弱引用,不阻止 GC
- 不可遍历(没有 size、keys 等)
WeakMap 应用场景:
// 1. 私有属性
const privateData = new WeakMap()
class User {
constructor(name) {
privateData.set(this, { name, createdAt: Date.now() })
}
getName() {
return privateData.get(this).name
}
}
// 2. DOM 节点关联数据(不阻止 DOM 被 GC)
const cache = new WeakMap()
cache.set(domNode, computedStyles)
// 当 domNode 被移除,cache 中的关联数据也被回收
Q: 什么是发布-订阅模式?和观察者模式有什么区别?
A:
发布-订阅模式:
class EventEmitter {
constructor() {
this.events = {}
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(listener)
return () => this.off(event, listener) // 返回取消订阅函数
}
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach(listener => listener(...args))
}
}
off(event, listener) {
this.events[event] = this.events[event].filter(l => l !== listener)
}
once(event, listener) {
const wrapper = (...args) => {
listener(...args)
this.off(event, wrapper)
}
this.on(event, wrapper)
}
}
// 使用
const emitter = new EventEmitter()
emitter.on('message', (msg) => console.log('Handler 1:', msg))
const unsubscribe = emitter.on('message', (msg) => console.log('Handler 2:', msg))
emitter.emit('message', 'Hello') // 两个 handler 都执行
unsubscribe() // 取消订阅
emitter.emit('message', 'World') // 只有 Handler 1 执行
发布-订阅 vs 观察者模式:
| 特性 | 发布-订阅 | 观察者 |
|---|---|---|
| 中间层 | 有 EventEmitter 中心 | 无中间层 |
| 耦合度 | 发布者和订阅者解耦 | 目标和观察者耦合 |
| 管理 | 统一管理事件订阅 | 观察者自行注册 |
| 典型应用 | Vue 事件总线、Node EventEmitter | Vue 响应式、DOM 事件 |
十一、系统设计
11.1 设计一个实时协作白板
Q: 如何设计一个支持多人实时协作的白板应用?
A:
核心架构:
┌──────────────────────────────────────────────────┐
│ 前端应用 │
│ Canvas/Three.js + WebSocket + Operation Log │
└────────────────────────┬─────────────────────────┘
│ WebSocket
┌────────────────────────┴─────────────────────────┐
│ WebSocket Gateway (Node.js) │
│ - 广播消息到所有客户端 │
│ - 处理房间逻辑 │
└────────────────────────┬─────────────────────────┘
│
┌────────────────────────┴─────────────────────────┐
│ 消息队列 │
│ Redis Pub/Sub 或 Kafka │
└──────────────────────────────────────────────────┘
核心技术:
- WebSocket 长连接:实时双向通信
- OT(Operational Transform)算法:解决并发冲突
- 用户 A 和 B 同时修改同一个图形
- 服务器通过 OT 转换,保证最终一致性
- Canvas 增量更新:只传输操作,而非全量画面
- Canvas 重播:新用户加入时,回放操作历史
数据结构设计:
// 操作日志
{
id: 'op-001',
type: 'MOVE_SHAPE',
shapeId: 'shape-001',
from: { x: 100, y: 100 },
to: { x: 200, y: 200 },
timestamp: 1699999999999,
userId: 'user-001'
}
降级策略:
- WebSocket 断开时切换到 HTTP 长轮询
- 离线时记录本地操作,上线后同步
11.2 设计一个前端监控SDK
Q: 如何设计一个前端监控 SDK(性能、错误、行为)?
A:
监控维度:
- 性能指标:FP、FCP、LCP、FID/INP、CLS、TTFB
- 错误监控:JS Error、Promise Rejection、资源加载失败
- 行为数据:PV/UV、点击热力图、页面停留时长
- 自定义事件:接口耗时、用户操作路径
SDK 架构:
class MonitorSDK {
constructor(options) {
this.appId = options.appId
this.endpoint = options.endpoint || '/api/report'
this.queue = []
this.maxQueueSize = options.maxQueueSize || 100
this.flushInterval = options.flushInterval || 5000
this.initPerformance()
this.initError()
this.initBehavior()
this.startFlush()
}
// 性能监控
initPerformance() {
new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
this.report('performance', {
name: entry.name,
value: entry.value,
rating: entry.rating
})
})
}).observe({ entryTypes: ['paint', 'navigation', 'longtask'] })
}
// 错误监控
initError() {
window.addEventListener('error', (e) => {
this.report('error', {
type: 'js',
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno
})
})
window.addEventListener('unhandledrejection', (e) => {
this.report('error', {
type: 'promise',
message: e.reason?.message || e.reason
})
})
}
// 行为数据
initBehavior() {
document.addEventListener('click', (e) => {
this.report('behavior', {
type: 'click',
x: e.clientX,
y: e.clientY,
target: e.target.tagName
})
})
}
// 数据上报
report(type, data) {
this.queue.push({
appId: this.appId,
type,
data,
timestamp: Date.now(),
url: location.href,
userId: this.getUserId()
})
if (this.queue.length >= this.maxQueueSize) {
this.flush()
}
}
async flush() {
if (this.queue.length === 0) return
const data = [...this.queue]
this.queue = []
await fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify(data),
keepalive: true // 页面关闭时也发送
})
}
startFlush() {
setInterval(() => this.flush(), this.flushInterval)
}
}
// 使用
const monitor = new MonitorSDK({
appId: 'my-app',
endpoint: '/api/monitor'
})
十二、开放性问题和反向提问
反向提问建议
Q: 你有什么问题想问我们的?
A:
技术相关:
- 前端团队目前的技术栈是什么?有计划引入新技术吗?
- 团队如何做代码审查和技术债务管理?
- 遇到技术难题时,团队如何做决策?
业务相关:
- 公司目前的核心业务方向是什么?
- 前端在产品中的定位是怎样的?
- 有哪些技术挑战是前端团队正在面对的?
团队相关:
- 前端团队有多少人?组织架构是怎样的?
- 新人入职后的成长路径是什么?
- 团队的技术分享氛围如何?
发展相关:
- 公司对技术专家的培养路径是怎样的?
- 有没有技术晋升通道?