单例模式在前端开发中的应用与实践

在学习设计模式的过程中,单例模式(Singleton Pattern) 是一个必不可少的成员。它常常被应用于前端项目的全局管理中,例如日志工具、配置中心、缓存服务,甚至我们日常使用的 windowdocument 等浏览器 API 本身就是单例模式的自然体现。

本文将从概念入手,带你从 0-1 手写一个单例,再到框架中的实践,最后用 React 做一个 Demo 来展示它的优势,并结合一个 全局请求管理器 的实际案例。


一、什么是单例模式?

  • 定义:单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。

  • 类比:就像一个系统里的"控制中心",全局只能有一个,所有模块都共享它。

  • 常见应用场景

    • 全局配置管理(Config)
    • 全局状态管理(Redux/Vuex store)
    • 工具类(Logger、缓存 Cache、请求管理器 RequestManager)
    • 浏览器内置对象(windowdocumentlocalStorage

二、普通写法 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

👉 结果:logger1logger2 是不同的实例。


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(windowdocumentlocalStorage),都能看到它的影子。

通过手写和 React Demo 的对比,我们直观地理解了为什么需要保证全局唯一实例:它能让不同模块间共享同一份状态,避免重复创建和逻辑混乱。

进一步结合 全局请求管理器 RequestManager 的案例,我们看到单例模式不仅是理论上的概念,更是日常开发中解决实际问题的利器。

当你在项目中需要 统一全局资源、配置或状态 时,单例模式就是一种简单而有效的解决方案。

相关推荐
LoveXming6 小时前
Chapter1—设计模式基础
c++·qt·设计模式·设计规范
翻滚丷大头鱼13 小时前
Java设计模式之结构型—代理模式
java·设计模式·代理模式
东北南西13 小时前
设计模式-代理模式
设计模式·typescript·代理模式
mask哥13 小时前
DP-观察者模式代码详解
java·观察者模式·微服务·设计模式·springboot·设计原则
柯南二号16 小时前
【Android】【设计模式】抽象工厂模式改造弹窗组件必知必会
android·设计模式·抽象工厂模式
Pure03191 天前
Spring MVC BOOT 中体现的设计模式
spring·设计模式·mvc
ytadpole1 天前
揭秘设计模式:优雅地为复杂对象结构增添新功能-访问者模式
java·设计模式
我的征途是星辰大海。1 天前
java-设计模式-5-创建型模式-建造
java·设计模式
wallflower20201 天前
深入理解前端设计模式:发布-订阅模式(Pub/Sub)
设计模式