在学习设计模式的过程中,单例模式(Singleton Pattern) 是一个必不可少的成员。它常常被应用于前端项目的全局管理中,例如日志工具、配置中心、缓存服务,甚至我们日常使用的 window
、document
等浏览器 API 本身就是单例模式的自然体现。
本文将从概念入手,带你从 0-1 手写一个单例,再到框架中的实践,最后用 React 做一个 Demo 来展示它的优势,并结合一个 全局请求管理器 的实际案例。
一、什么是单例模式?
-
定义:单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。
-
类比:就像一个系统里的"控制中心",全局只能有一个,所有模块都共享它。
-
常见应用场景:
- 全局配置管理(Config)
- 全局状态管理(Redux/Vuex store)
- 工具类(Logger、缓存 Cache、请求管理器 RequestManager)
- 浏览器内置对象(
window
、document
、localStorage
)
二、普通写法 vs 单例写法
2.1 普通类(会创建多个实例)
javascript
class NormalLogger {
constructor() {
this.logs = [];
}
log(message) {
this.logs.push(message);
console.log("Log:", message);
}
}
const logger1 = new NormalLogger();
const logger2 = new NormalLogger();
console.log(logger1 === logger2); // false
👉 结果:logger1
和 logger2
是不同的实例。
2.2 单例写法
ini
class SingletonLogger {
constructor() {
if (!SingletonLogger.instance) {
this.logs = [];
SingletonLogger.instance = this;
}
return SingletonLogger.instance;
}
log(message) {
this.logs.push(message);
console.log("Log:", message);
}
}
const logger1 = new SingletonLogger();
const logger2 = new SingletonLogger();
console.log(logger1 === logger2); // true
👉 结果:无论创建多少次,拿到的都是同一个实例。
三、框架与 API 中的单例模式
3.1 Vue / React 状态管理
- Vuex、Pinia、Redux 中的 Store 都是单例。
- 不论在多少组件里调用
useStore()
,返回的都是同一个状态对象。
3.2 Vue 应用实例
在 Vue3 中,通常一个项目只会有一个 createApp(App)
实例,所有组件挂载在它之下,这就是一种单例思想。
3.3 浏览器中的天然单例
浏览器本身提供了很多天然的单例对象,例如:
window
全局唯一,所有脚本共享:
javascript
console.log(window === window); // true
window.appName = "MyApp";
console.log(window.appName); // "MyApp"
document
对应当前页面的唯一文档:
ini
const title = document.title;
document.title = "新标题";
console.log(document.title); // "新标题"
localStorage
页面中共享同一个存储:
javascript
localStorage.setItem("token", "abc123");
console.log(localStorage.getItem("token")); // "abc123"
👉 这些对象天生就是单例,保证了全局的一致性和可访问性。
四、React 中的实践 Demo
下面我们用 日志记录器 Logger 来模拟单例模式在实际 React 项目中的应用。
4.1 不使用单例(错误写法)
javascript
// Logger.js
export class Logger {
constructor() {
this.logs = [];
}
log(message) {
this.logs.push(message);
console.log("Log:", message);
}
getLogs() {
return this.logs;
}
}
// App.js
import React, { useState } from "react";
import { Logger } from "./Logger";
function App() {
const [logs, setLogs] = useState([]);
const logger = new Logger(); // ❌ 每次渲染都会生成新的实例
const handleClick = () => {
logger.log("按钮被点击");
setLogs([...logger.getLogs()]);
};
return (
<div>
<button onClick={handleClick}>点我</button>
<p>日志: {logs.join(", ")}</p>
</div>
);
}
export default App;
👉 问题:点击按钮时日志不会被正确累积,因为每次都在使用新实例。
4.2 使用单例(正确写法)
javascript
// SingletonLogger.js
class SingletonLogger {
constructor() {
if (!SingletonLogger.instance) {
this.logs = [];
SingletonLogger.instance = this;
}
return SingletonLogger.instance;
}
log(message) {
this.logs.push(message);
console.log("Log:", message);
}
getLogs() {
return this.logs;
}
}
export const logger = new SingletonLogger();
// App.js
import React, { useState } from "react";
import { logger } from "./SingletonLogger";
function App() {
const [logs, setLogs] = useState([]);
const handleClick = () => {
logger.log("按钮被点击");
setLogs([...logger.getLogs()]);
};
return (
<div>
<button onClick={handleClick}>点我</button>
<p>日志: {logs.join(", ")}</p>
</div>
);
}
export default App;
👉 优势:点击多次按钮后,日志会被正确累积。
五、实用案例:全局请求管理器 RequestManager
在实际开发中,常常会遇到 同一个请求被多次触发 的问题(例如多个组件同时请求用户信息)。
如果没有统一管理,很可能会发出多次重复请求,浪费带宽和资源。
此时,我们就可以用 单例模式 实现一个全局请求管理器,确保相同请求只发一次。
5.1 实现 RequestManager
kotlin
// RequestManager.js
class RequestManager {
constructor() {
if (!RequestManager.instance) {
this.cache = new Map(); // 缓存请求结果
this.pending = new Map(); // 正在进行的请求
RequestManager.instance = this;
}
return RequestManager.instance;
}
async fetchData(url) {
// 如果有缓存,直接返回
if (this.cache.has(url)) {
return this.cache.get(url);
}
// 如果有正在进行的请求,复用 Promise
if (this.pending.has(url)) {
return this.pending.get(url);
}
// 否则发起新请求
const promise = fetch(url).then(async (res) => {
const data = await res.json();
this.cache.set(url, data); // 缓存结果
this.pending.delete(url); // 移除 pending
return data;
});
this.pending.set(url, promise);
return promise;
}
}
export const requestManager = new RequestManager();
5.2 在 React 组件中使用
javascript
// App.js
import React, { useEffect, useState } from "react";
import { requestManager } from "./RequestManager";
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
requestManager.fetchData("https://jsonplaceholder.typicode.com/users/1")
.then(setUser);
}, []);
return (
<div>
<h1>用户信息</h1>
{user ? <p>{user.name}</p> : <p>加载中...</p>}
</div>
);
}
export default App;
👉 这样,即使多个组件同时请求用户数据,RequestManager
也会确保只发一次请求,其余请求复用结果。
六、单例模式的优缺点
优点
- 确保全局唯一实例,减少资源开销
- 全局访问点,管理方便
- 在全局状态、配置、工具类、请求缓存等场景非常实用
缺点
- 全局实例可能导致模块间高耦合
- 状态难以隔离,测试时可能需要额外清理
- 滥用会破坏代码的可维护性
七、总结
单例模式在前端开发中使用非常广泛,从框架(Vuex、Redux)到浏览器 API(window
、document
、localStorage
),都能看到它的影子。
通过手写和 React Demo 的对比,我们直观地理解了为什么需要保证全局唯一实例:它能让不同模块间共享同一份状态,避免重复创建和逻辑混乱。
进一步结合 全局请求管理器 RequestManager 的案例,我们看到单例模式不仅是理论上的概念,更是日常开发中解决实际问题的利器。
当你在项目中需要 统一全局资源、配置或状态 时,单例模式就是一种简单而有效的解决方案。