设计模式、Web Vitals 与性能优化
前端面试准备中关于设计模式、Web Vitals 与性能优化的知识点整理。
设计模式
前端面试必备设计模式
1. 观察者模式 ⭐⭐⭐
一句话:一个对象变了,自动通知所有关注者。
Vue 的响应式原理就是它:数据变化 → 自动更新视图。
// 手写简易版 —— 面试高频题
class Dep {
constructor() { this.subs = []; }
addSub(watcher) { this.subs.push(watcher); }
notify() { this.subs.forEach(w => w.update()); }
}
class Watcher {
constructor(cb) { this.cb = cb; }
update() { this.cb(); }
}面试关键词:Vue 响应式、EventEmitter、DOM 事件
2. 单例模式 ⭐⭐⭐
答案为true
一句话:全局只能有一个实例。 后续无法再更改
典型场景:Vuex/Redux 的 Store、全局弹窗、登录框。
// 面试手写
class Singleton {
static instance = null;
static getInstance() {
if (!Singleton.instance) Singleton.instance = new Singleton();
return Singleton.instance;
}
}3. 代理模式 ⭐⭐⭐
类似于经纪人之于明星,谈戏要先经过经纪人看是否接戏
一句话:提供一个替身控制对原对象的访问。
Vue 3 的核心:用 Proxy 实现响应式,拦截 get/set。
const obj = new Proxy(data, {
get(target, key) { /* 收集依赖 */ return target[key]; },
set(target, key, val) { target[key] = val; /* 触发更新 */ return true; }
});面试关键词:Vue 3 Proxy、图片懒加载、缓存代理
vue3数据驱动视图更新简单实现:
注意要修改的是proxy的属性
4. 工厂模式 ⭐⭐
一句话:传入参数,返回不同类型的对象,隐藏创建细节。
典型场景:根据 type 创建不同组件、React.createElement 本质就是工厂。
function createComponent(type) {
const map = { header: Header, footer: Footer, sidebar: Sidebar };
return new map[type]();
}加static就可以不用实例化而直接调用
抽象工厂模式(Abstract Factory)
其实就是抽取了一个父类,然后不同的参数得到不同的子类实例化,父类又可以增加共同的属性和方法,子类进行继承,子类有自己的属性和方法
一句话理解
普通工厂:一个工厂生产一种产品
抽象工厂:一个工厂生产一族相关产品
核心思想
提供一个接口,用于创建相互关联的一组对象,而不需要指定具体类。
最通俗的例子
想象你去买车:
| 普通工厂 | 抽象工厂 | |
|---|---|---|
| 问题 | 只能造一个零件 | 一辆车需要发动机+轮胎+座椅,它们必须配套 |
| 解决 | 造发动机的工厂 | 宝马工厂 → 宝马发动机 + 宝马轮胎 + 宝马座椅 |
| 造轮胎的工厂 | 奔驰工厂 → 奔驰发动机 + 奔驰轮胎 + 奔驰座椅 |
抽象工厂保证了:同一族的产品是配套的,不会出现"宝马发动机配奔驰轮胎"的混乱。
代码示例
// 抽象工厂:定义接口
class UIFactory {
createButton() {}
createInput() {}
createModal() {}
}
// 具体工厂1:暗色主题
class DarkThemeFactory extends UIFactory {
createButton() { return { color: '#fff', bg: '#333' }; }
createInput() { return { border: '1px solid #555', bg: '#222' }; }
createModal() { return { bg: '#1a1a1a', shadow: 'none' }; }
}
// 具体工厂2:亮色主题
class LightThemeFactory extends UIFactory {
createButton() { return { color: '#000', bg: '#fff' }; }
createInput() { return { border: '1px solid #ccc', bg: '#fafafa' }; }
createModal() { return { bg: '#ffffff', shadow: '0 2px 8px rgba(0,0,0,.15)' }; }
}
// 使用:切换主题只需换工厂,所有组件自动配套
function renderUI(factory) {
const button = factory.createButton();
const input = factory.createInput();
const modal = factory.createModal();
// ...渲染页面
}
renderUI(new DarkThemeFactory()); // 暗色主题
renderUI(new LightThemeFactory()); // 亮色主题和普通工厂的区别(面试必答)
| 工厂方法 | 抽象工厂 | |
|---|---|---|
| 生产什么 | 单个产品 | 一族相关产品 |
| 工厂数量 | 每个产品一个工厂 | 一个工厂造多个配套产品 |
| 扩展方式 | 新增产品 → 新增工厂 | 新增产品族 → 新增工厂 |
| 耦合度 | 较低 | 保证产品族内部一致 |
前端实际应用
| 场景 | 说明 |
|---|---|
| 主题切换 | 暗色/亮色主题,按钮、输入框、弹窗等必须风格统一 |
| 跨平台UI | 一套代码适配 Web / Mobile / Desktop,每个平台有不同的组件工厂 |
| 多数据库适配 | 后端接口抽象,一个工厂出 MySQL/MongoDB/Redis 的连接+查询+缓存 |
| CSS-in-JS 主题 | styled-components 的 ThemeProvider 本质就是抽象工厂 |
面试答题模板
"抽象工厂模式提供一个接口,用于创建一族相关或相互依赖的对象。
比如前端的主题系统——暗色工厂同时生产暗色按钮、暗色输入框、暗色弹窗,保证风格一致。切换主题时只需切换工厂,所有组件自动配套,符合开闭原则。"
5. 策略模式 ⭐⭐
一句话:把不同的算法封装起来,可以互相替换。
典型场景:表单验证(最常考)。
const rules = {
required: v => v !== '' || '不能为空',
email: v => /\S+@\S+\.\S+/.test(v) || '邮箱格式不对',
minLength: (v, n) => v.length >= n || `至少${n}位`,
};
function validate(value, ruleList) {
return ruleList.map(r => rules[r.type](value, r.param)).filter(Boolean);
}速记口诀
「观代单工厂,策略不难记」
- 观察者 → Vue 响应式、事件
- 代理 → Vue 3 Proxy
- 单例 → 全局 Store
- 工厂 → 动态创建组件
- 策略 → 表单验证
面试怎么答?
被问到设计模式时,不要背定义,直接说:
"以观察者模式为例,Vue 的响应式系统就是典型应用——当数据变化时,所有依赖这个数据的组件会自动更新,底层通过 Dep 收集依赖、Watcher 触发更新来实现。"
先说模式 → 再说场景 → 最后说框架中的应用,这个三段式回答最加分。
好的,下面为你总结发布订阅模式和装饰器模式:
6.发布订阅模式(Publish-Subscribe Pattern)
核心思想
发布者(Publisher)和订阅者(Subscriber)之间通过一个事件中心(Event Channel)进行解耦。发布者只负责发送事件,订阅者只负责监听和处理事件,两者互不直接引用。
角色
| 角色 | 职责 |
|---|---|
| Publisher(发布者) | 发布事件/消息 |
| Subscriber(订阅者) | 注册感兴趣的事件,收到通知后执行回调 |
| EventChannel(事件中心) | 管理订阅关系,负责事件分发 |
简单实现
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, ...args) {
const callbacks = this.events[event] || [];
callbacks.forEach(cb => cb(...args));
}
off(event, callback) {
this.events[event] = (this.events[event] || []).filter(cb => cb !== callback);
}
}使用场景
GUI 事件系统(按钮点击、键盘事件)
消息队列(RabbitMQ、Kafka)
Vue 的事件总线(EventBus)
React 的状态管理(Redux 的 dispatch/subscribe)
WebSocket 消息推送
优点与缺点
✅ 松耦合:发布者和订阅者互不知道对方
✅ 可扩展性强:新增订阅者无需修改发布者
❌ 事件过多时难以追踪:调试和排查问题较困难
❌ 可能存在内存泄漏:忘记取消订阅时
7.装饰器模式(Decorator Pattern)
核心思想
在不修改原始对象的情况下,通过将对象放入装饰器对象中来动态扩展其功能。装饰器提供了比继承更灵活的功能扩展方式。
角色
| 角色 | 职责 |
|---|---|
| Component(组件接口) | 定义对象的统一接口 |
| ConcreteComponent(具体组件) | 核心功能实现 |
| Decorator(装饰器基类) | 持有组件引用,实现组件接口 |
| ConcreteDecorator(具体装饰器) | 扩展具体功能 |
简单实现
// 组件接口
class Coffee {
cost() { return 10; }
description() { return '基础咖啡'; }
}
// 装饰器:加牛奶
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 5; }
description() { return this.coffee.description() + ' + 牛奶'; }
}
// 装饰器:加糖
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 3; }
description() { return this.coffee.description() + ' + 糖'; }
}
// 使用:自由组合
const myCoffee = new SugarDecorator(new MilkDecorator(new Coffee()));
console.log(myCoffee.cost()); // 18
console.log(myCoffee.description()); // '基础咖啡 + 牛奶 + 糖'使用场景
Java I/O 流:
new BufferedReader(new InputStreamReader(new FileInputStream(file)))Python 装饰器语法:
@decorator(语法糖形式)TypeScript / ES7 装饰器:
@log、@memoize等中间件:Koa / Express 的洋葱模型
AOP(面向切面编程):日志、权限、缓存等横切关注点
优点与缺点
✅ 开闭原则:无需修改原有代码即可扩展功能
✅ 灵活组合:可自由叠加多个装饰器
✅ 单一职责:每个装饰器只关注自己的功能
❌ 会产生很多小对象:系统复杂度增加
❌ 多层装饰时排错困难
核心性能指标(Web Vitals)
现代前端关注的核心性能指标主要包括 Core Web Vitals(CWV):
三大核心指标(2024+)
| 指标 | 全称 | 衡量什么 | 好的阈值 | 说明 |
|---|---|---|---|---|
| LCP | Largest Contentful Paint | 最大内容绘制时间 | ≤ 2.5s | 衡量加载性能,用户看到主要内容的时间 |
| INP | Interaction to Next Paint | 交互到下次绘制的延迟 | ≤ 200ms | 衡量交互响应性,取代了 FID(2024年3月起) |
| CLS | Cumulative Layout Shift | 累积布局偏移 | ≤ 0.1 | 衡量视觉稳定性,页面元素是否乱跳 |
性能指标不仅关乎用户体验也关乎seo,google会把性能得分靠前的往前放。
LCP
通常一个页面的最大元素是主视觉图
LCP如果2.5s还没渲染,认为需要提示,4s还没渲染,认为有问题
设置优先级
INP
点一次按钮,没有立即响应,会有一点点卡顿或者延迟
FID只是第一次,已经被INP取代了
尽量不要做大量计算的工作,如果一定要做需要通过worker这种方式,避免阻塞主线程
CLS
影响力分数乘以距离分数
影响力可以理解为一个元素越大,影响力就越大
距离分数是一个元素被挤得偏移的越远,距离得分就越大
传统指标(仍有参考价值)
| 指标 | 说明 |
|---|---|
| FCP (First Contentful Paint) | 第一次有内容绘制,衡量首次加载速度 |
| TTFB (Time to First Byte) | 服务器响应时间 |
| FID (First Input Delay) | 首次输入延迟(已被 INP 取代) |
| TTI (Time to Interactive) | 页面完全可交互时间 |
| TBT (Total Blocking Time) | 总阻塞时间,FCP 到 TTI 之间长任务的累计时间 |
如何理解这些指标
想象你开了一家餐厅:
LCP = 顾客坐下后,第一道主菜端上来的时间
INP = 顾客喊服务员后,服务员响应的速度
CLS = 菜端上来后,桌子不会突然滑走/餐具不会乱飞
FCP = 顾客坐下后,先上一碟花生米(有东西可看)的时间
TTFB = 顾客点了菜后,厨房开始做的时间
监测工具
Lighthouse(Chrome DevTools 内置)
Web Vitals 库(
npm install web-vitals)PageSpeed Insights(Google)
Chrome User Experience Report (CrUX)
性能优化
白屏时间相对来说一般更长,其中资源加载时间是最长的
异步加载:
分析哪些适合做,体积比较大,但又不是一开始就需要的功能,比如图片压缩功能引入一个比较大的库,这种就可以异步加载
更新为体积更小的新版本:
第三方库一般不会用到所有功能,treeshaking可以只打包用到的那些方法,但要看库支不支持,一般新版本就会支持,按需引入一般就是支持tree-shaking,减小代码体积,加快首屏渲染,但能不用三方库就不用
比如xlsx这个库
去除大的base64体积:
打包的时候图标会转base64,大的图片不要转base64
加骨架屏:
vue和react都是打包之后出一个小的html文件,解析html再去请求js,那么可以先给一个骨架屏提高用户体验,因为业务问题无法再从技术上减少了
4.请求粒度
删除之后只需要请求列表,类目此时就是无效的请求
慎用keepalive,不更新带来的困扰比缓存带来的优化更多
网络层:
DNS 预解析、HTTP/2、资源压缩(Gzip/Brotli)
CDN 加速、缓存策略(强缓存/协商缓存)
按需加载、Tree Shaking
渲染层:
骨架屏(首屏感知速度)
图片懒加载、WebP 格式
CSS 动画代替 JS 动画(触发 GPU 合成层)
减少重排重绘(合并 DOM 操作、
transform代替top/left)
运行时:
虚拟列表(长列表只渲染可视区域)
防抖节流
Web Worker(复杂计算异步化)
减少主线程阻塞
像送快递一样
主要从三个方向考虑:
传输更快,加载更快,体积更小
升级带宽,加钱
CDN内容分发网络,延迟低,也可以同时支持更多用户访问,但是双刃剑,按流量计费,很贵
浏览器缓存到用户本地,
CDN缓存解决地理距离问题,浏览器缓存解决重复访问问题
在web服务器进行开启,或CND一键开启HTTP2.0配置
兼容性和稳定性有待验证
优化文件体积
图片,音视频 jpg,gpg -》 webp
代码压缩,去除空格注释换行 打包工具会自动压缩优化tree shaking 或者用min压缩版本
网站传输时用gzip压缩
浏览器发请求表示接受gzip压缩,服务器发生压缩后的文件,浏览器解压处理
web服务器添加gzip配置
网站加载优化
延迟加载--懒加载,适合长页面和图片较多的网站
按需加载--分模块
分词加载-先快后好
先让用户看到东西,减少等待焦虑感
缩略图 -低清小图,高清大图
渐进式加载--模糊浏览,逐渐清晰
预加载
在用户访问之前就把资源准备好,让体验更流畅
比如文章列表,可以预加载几篇文章,把握好度
请求合并
浏览器对同一域名有并发请求数的限制,可能会产生请求排队和阻塞,导致数据加载耗时过长
小图标---雪碧图 用background-position决定显示哪个图标
性能分析工具
lighthouse
一键分析,进行性能评估并给出建议