news 2026/3/2 4:27:13

什么是单例模式?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
什么是单例模式?

例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。在 TypeScript 中,单例模式特别有用,因为它结合了 JavaScript 的灵活性和 TypeScript 的类型安全。

为什么需要单例模式?

想象一下这些场景:

数据库连接池管理

应用程序配置管理器

日志记录器

缓存管理器

在这些情况下,我们需要确保整个应用程序中只有一个实例来处理这些全局资源,避免资源浪费和不一致的状态。

基础单例实现

让我们从最简单的实现开始:

class Singleton {

private static instance: Singleton;

private constructor() {

// 私有构造函数防止外部实例化

}

public static getInstance(): Singleton {

if (!Singleton.instance) {

Singleton.instance = new Singleton();

}

return Singleton.instance;

}

public someBusinessLogic() {

// 业务逻辑

}

}

线程安全的单例实现

在 JavaScript/TypeScript 中,由于是单线程环境,我们不需要担心传统的线程安全问题。但考虑到异步操作,我们可以使用更安全的实现:

class ThreadSafeSingleton {

private static instance: ThreadSafeSingleton;

private constructor() {

// 初始化代码

}

public static getInstance(): ThreadSafeSingleton {

if (!ThreadSafeSingleton.instance) {

ThreadSafeSingleton.instance = new ThreadSafeSingleton();

}

return ThreadSafeSingleton.instance;

}

// 使用 Promise 确保异步安全

public static async getInstanceAsync(): Promise<ThreadSafeSingleton> {

if (!ThreadSafeSingleton.instance) {

ThreadSafeSingleton.instance = new ThreadSafeSingleton();

// 模拟异步初始化

await new Promise(resolve => setTimeout(resolve, 0));

}

return ThreadSafeSingleton.instance;

}

}

使用模块模式的单例实现

TypeScript 的模块系统天然支持单例模式:

// Logger.ts

class Logger {

private logs: string[] = [];

log(message: string) {

this.logs.push(`${new Date().toISOString()}: ${message}`);

console.log(message);

}

getLogs(): string[] {

return [...this.logs];

}

}

// 直接导出实例

export const logger = new Logger();

带参数的单例模式

有时我们需要在单例初始化时传递参数:

class ConfigManager {

private static instance: ConfigManager;

private config: Record<string, any>;

private constructor(initialConfig?: Record<string, any>) {

this.config = initialConfig || {};

}

public static initialize(initialConfig?: Record<string, any>): ConfigManager {

if (!ConfigManager.instance) {

ConfigManager.instance = new ConfigManager(initialConfig);

}

return ConfigManager.instance;

}

public static getInstance(): ConfigManager {

if (!ConfigManager.instance) {

throw new Error('ConfigManager not initialized. Call initialize() first.');

}

return ConfigManager.instance;

}

public set(key: string, value: any): void {

this.config[key] = value;

}

public get(key: string): any {

return this.config[key];

}

}

单例模式的优缺点

优点:

严格控制实例数量:确保全局唯一实例

全局访问点:方便在任何地方访问

延迟初始化:只有在需要时才创建实例

缺点:

违反单一职责原则:类需要管理自己的生命周期

隐藏的依赖关系:单例的使用可能不明显

测试困难:难以模拟和测试

全局状态:可能导致代码耦合

测试单例模式

测试单例类时需要特别注意:

describe('Singleton', () => {

beforeEach(() => {

// 重置单例实例用于测试

(Singleton as any).instance = undefined;

});

it('should return the same instance', () => {

const instance1 = Singleton.getInstance();

const instance2 = Singleton.getInstance();

expect(instance1).toBe(instance2);

});

});

实际应用示例:数据库连接池

让我们看一个实际的数据库连接池单例实现:

interface DatabaseConfig {

host: string;

port: number;

username: string;

password: string;

database: string;

}

class DatabaseConnectionPool {

private static instance: DatabaseConnectionPool;

private connections: any[] = [];

private config: DatabaseConfig;

private constructor(config: DatabaseConfig) {

this.config = config;

this.initializePool();

}

public static getInstance(config?: DatabaseConfig): DatabaseConnectionPool {

if (!DatabaseConnectionPool.instance) {

if (!config) {

throw new Error('Configuration required for first initialization');

}

DatabaseConnectionPool.instance = new DatabaseConnectionPool(config);

}

return DatabaseConnectionPool.instance;

}

private initializePool(): void {

// 初始化连接池

for (let i = 0; i < 10; i++) {

this.connections.push(this.createConnection());

}

}

private createConnection(): any {

// 创建数据库连接的逻辑

return {

query: (sql: string) => console.log(`Executing: ${sql}`),

close: () => console.log('Connection closed')

};

}

public getConnection(): any {

return this.connections.pop() || this.createConnection();

}

public releaseConnection(connection: any): void {

this.connections.push(connection);

}

}

实际应用示例:Streams to River

Streams to River 由字节跳动开源, 是一款英语学习应用。该产品的初衷是通过将日常所见的英语单词、句子和相关的上下文进行记录、提取和管理, 结合 艾宾浩斯遗忘曲线,进行周期性的学习和记忆。

在开发过程中,深度采用了 TRAE 进行代码的开发和调试、注释和单测的编写,通过 coze workflow 快速集成了图像转文字、实时聊天、语音识别、单词划线等大模型能力。

在该项目代码中就存在大量的单例模式代码。

1. AuthService 的实现

class AuthService {

private static instance: AuthService;

private serverConfig: ServerConfig;

private constructor() {

this.serverConfig = ServerConfig.getInstance();

}

public static getInstance(): AuthService {

if (!AuthService.instance) {

AuthService.instance = new AuthService();

}

return AuthService.instance;

}

async login(loginData: LoginRequest): Promise<AuthResponse> {

try {

const response = await Taro.request({

url: this.serverConfig.getFullUrl('/api/login'),

method: 'POST',

data: loginData,

header: {

'Content-Type': 'application/json'

}

});

if (response.statusCode === 200) {

const authData = response.data as AuthResponse;

await this.setToken(authData.token);

return authData;

} else {

throw new Error(response.data || '登录失败');

}

} catch (error) {

console.error('Login error:', error);

throw error;

}

}

async register(registerData: RegisterRequest): Promise<AuthResponse> {

try {

const response = await Taro.request({

url: this.serverConfig.getFullUrl('/api/register'),

method: 'POST',

data: registerData,

header: {

'Content-Type': 'application/json'

}

});

if (response.statusCode === 200) {

const authData = response.data as AuthResponse;

await this.setToken(authData.token);

return authData;

} else {

throw new Error(response.data || '注册失败');

}

} catch (error) {

console.error('Register error:', error);

throw error;

}

}

async getUserInfo(): Promise<User> {

try {

const token = await this.getToken();

if (!token) {

throw new Error('未找到token');

}

const response = await Taro.request({

url: this.serverConfig.getFullUrl('/api/user'),

method: 'GET',

header: {

'Authorization': `Bearer ${token}`,

'Content-Type': 'application/json'

}

});

if (response.statusCode === 200) {

return response.data as User;

} else {

throw new Error('获取用户信息失败');

}

} catch (error) {

console.error('Get user info error:', error);

throw error;

}

}

async setToken(token: string): Promise<void> {

try {

await Taro.setStorageSync('jwt_token', token);

} catch (error) {

console.error('Set token error:', error);

throw error;

}

}

async getToken(): Promise<string | null> {

try {

return Taro.getStorageSync('jwt_token') || null;

} catch (error) {

console.error('Get token error:', error);

return null;

}

}

async clearToken(): Promise<void> {

try {

await Taro.removeStorageSync('jwt_token');

} catch (error) {

console.error('Clear token error:', error);

}

}

async isLoggedIn(): Promise<boolean> {

const token = await this.getToken();

return !!token;

}

async logout(): Promise<void> {

await this.clearToken();

}

}

2. AudioManager 的实现

class AudioManager {

private static instance: AudioManager;

private currentAudio: HTMLAudioElement | null = null;

private currentWordId: number | null = null;

private playingCallbacks: Map<number, (isPlaying: boolean) => void> = new Map();

static getInstance(): AudioManager {

if (!AudioManager.instance) {

AudioManager.instance = new AudioManager();

}

return AudioManager.instance;

}

// Register playback status callback

registerCallback(wordId: number, callback: (isPlaying: boolean) => void) {

this.playingCallbacks.set(wordId, callback);

}

// Unregister callback

unregisterCallback(wordId: number) {

this.playingCallbacks.delete(wordId);

}

// Play audio

async playAudio(wordId: number, audioUrl: string): Promise<void> {

try {

// Stop currently playing audio

this.stopCurrentAudio();

// Create new audio instance

const audio = new Audio(audioUrl);

this.currentAudio = audio;

this.currentWordId = wordId;

// Set audio properties

audio.preload = 'auto';

audio.volume = 1.0;

// Notify playback start

this.notifyPlayingState(wordId, true);

// Listen to audio events

audio.addEventListener('ended', () => {

this.handleAudioEnd();

});

audio.addEventListener('error', (e) => {

console.error('Audio playback error:', e);

this.handleAudioEnd();

});

// Play audio

await audio.play();

} catch (error) {

console.error('Failed to play audio:', error);

this.handleAudioEnd();

}

}

// Stop current audio

private stopCurrentAudio() {

if (this.currentAudio) {

this.currentAudio.pause();

this.currentAudio.currentTime = 0;

this.currentAudio = null;

}

if (this.currentWordId !== null) {

this.notifyPlayingState(this.currentWordId, false);

this.currentWordId = null;

}

}

// Handle audio end

private handleAudioEnd() {

if (this.currentWordId !== null) {

this.notifyPlayingState(this.currentWordId, false);

}

this.currentAudio = null;

this.currentWordId = null;

}

// Notify playback state change

private notifyPlayingState(wordId: number, isPlaying: boolean) {

const callback = this.playingCallbacks.get(wordId);

if (callback) {

callback(isPlaying);

}

}

// Check if currently playing

isPlaying(wordId: number): boolean {

return this.currentWordId === wordId && this.currentAudio !== null;

}

}

3. ServerConfig 的实现

class ServerConfig {

private static instance: ServerConfig;

private config: ServerConfigInterface;

private constructor() {

this.config = this.loadConfig();

}

public static getInstance(): ServerConfig {

if (!ServerConfig.instance) {

ServerConfig.instance = new ServerConfig();

}

return ServerConfig.instance;

}

private loadConfig(): ServerConfigInterface {

const serverDomain = location.origin;

const url = new URL(serverDomain);

return {

domain: url.hostname,

port: url.port ? parseInt(url.port) : (url.protocol === 'https:' ? 443 : 80),

protocol: url.protocol.replace(':', ''),

};

}

public getDomain(): string {

return this.config.domain;

}

public getPort(): number {

return this.config.port || 80;

}

public getProtocol(): string {

return this.config.protocol || 'http';

}

public getBaseUrl(): string {

const port = this.getPort();

const protocol = this.getProtocol();

const domain = this.getDomain();

if ((protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)) {

return `${protocol}://${domain}`;

}

return `${protocol}://${domain}:${port}`;

}

public getFullUrl(path: string = ''): string {

const baseUrl = this.getBaseUrl();

const cleanPath = path.startsWith('/') ? path : `/${path}`;

return `${baseUrl}${cleanPath}`;

}

}

实际应用示例:Cherry Studio

🍒 Cherry Studio 是一款支持多个大语言模型(LLM)服务商的桌面客户端,兼容 Windows、Mac 和 Linux 系统。。

该项目前端是比较复杂的, 采用良好的设计十分必要。它的代码中也存在大量的单例模式设计。

1. StoreSyncService 的实现

import { IpcChannel } from '@shared/IpcChannel'

import type { StoreSyncAction } from '@types'

import { BrowserWindow, ipcMain } from 'electron'

/**

* StoreSyncService class manages Redux store synchronization between multiple windows in the main process

* It uses singleton pattern to ensure only one sync service instance exists in the application

*

* Main features:

* 1. Manages window subscriptions for store sync

* 2. Handles IPC communication for store sync between windows

* 3. Broadcasts Redux actions from one window to all other windows

* 4. Adds metadata to synced actions to prevent infinite sync loops

*/

export class StoreSyncService {

private static instance: StoreSyncService

private windowIds: number[] = []

private isIpcHandlerRegistered = false

private constructor() {

return

}

/**

* Get the singleton instance of StoreSyncService

*/

public static getInstance(): StoreSyncService {

if (!StoreSyncService.instance) {

StoreSyncService.instance = new StoreSyncService()

}

return StoreSyncService.instance

}

/**

* Subscribe a window to store sync

* @param windowId ID of the window to subscribe

*/

public subscribe(windowId: number): void {

if (!this.windowIds.includes(windowId)) {

this.windowIds.push(windowId)

}

}

/**

* Unsubscribe a window from store sync

* @param windowId ID of the window to unsubscribe

*/

public unsubscribe(windowId: number): void {

this.windowIds = this.windowIds.filter((id) => id !== windowId)

}

/**

* Sync an action to all renderer windows

* @param type Action type, like 'settings/setTray'

* @param payload Action payload

*

* NOTICE: DO NOT use directly in ConfigManager, may cause infinite sync loop

*/

public syncToRenderer(type: string, payload: any): void {

const action: StoreSyncAction = {

type,

payload

}

//-1 means the action is from the main process, will be broadcast to all windows

this.broadcastToOtherWindows(-1, action)

}

/**

* Register IPC handlers for store sync communication

* Handles window subscription, unsubscription and action broadcasting

*/

public registerIpcHandler(): void {

if (this.isIpcHandlerRegistered) return

ipcMain.handle(IpcChannel.StoreSync_Subscribe, (event) => {

const windowId = BrowserWindow.fromWebContents(event.sender)?.id

if (windowId) {

this.subscribe(windowId)

}

})

ipcMain.handle(IpcChannel.StoreSync_Unsubscribe, (event) => {

const windowId = BrowserWindow.fromWebContents(event.sender)?.id

if (windowId) {

this.unsubscribe(windowId)

}

})

ipcMain.handle(IpcChannel.StoreSync_OnUpdate, (event, action: StoreSyncAction) => {

const sourceWindowId = BrowserWindow.fromWebContents(event.sender)?.id

if (!sourceWindowId) return

// Broadcast the action to all other windows

this.broadcastToOtherWindows(sourceWindowId, action)

})

this.isIpcHandlerRegistered = true

}

/**

* Broadcast a Redux action to all other windows except the source

* @param sourceWindowId ID of the window that originated the action

* @param action Redux action to broadcast

*/

private broadcastToOtherWindows(sourceWindowId: number, action: StoreSyncAction): void {

// Add metadata to indicate this action came from sync

const syncAction = {

...action,

meta: {

...action.meta,

fromSync: true,

source: `windowId:${sourceWindowId}`

}

}

// Send to all windows except the source

this.windowIds.forEach((windowId) => {

if (windowId !== sourceWindowId) {

const targetWindow = BrowserWindow.fromId(windowId)

if (targetWindow && !targetWindow.isDestroyed()) {

targetWindow.webContents.send(IpcChannel.StoreSync_BroadcastSync, syncAction)

} else {

this.unsubscribe(windowId)

}

}

})

}

}

// Export singleton instance

export default StoreSyncService.getInstance()

2. NotificationQueue 的实现

import type { Notification } from '@renderer/types/notification'

import PQueue from 'p-queue'

type NotificationListener = (notification: Notification) => Promise<void> | void

export class NotificationQueue {

private static instance: NotificationQueue

private queue = new PQueue({ concurrency: 1 })

private listeners: NotificationListener[] = []

// oxlint-disable-next-line @typescript-eslint/no-empty-function

private constructor() {}

public static getInstance(): NotificationQueue {

if (!NotificationQueue.instance) {

NotificationQueue.instance = new NotificationQueue()

}

return NotificationQueue.instance

}

public subscribe(listener: NotificationListener) {

this.listeners.push(listener)

}

public unsubscribe(listener: NotificationListener) {

this.listeners = this.listeners.filter((l) => l !== listener)

}

public async add(notification: Notification): Promise<void> {

await this.queue.add(() => Promise.all(this.listeners.map((listener) => listener(notification))))

}

/**

* 清空通知队列

*/

public clear(): void {

this.queue.clear()

}

/**

* 获取队列中等待的任务数量

*/

public get pending(): number {

return this.queue.pending

}

/**

* 获取队列的大小(包括正在进行和等待的任务)

*/

public get size(): number {

return this.queue.size

}

}

3. AgentService 的实现

import path from 'node:path'

import { loggerService } from '@logger'

import { pluginService } from '@main/services/agents/plugins/PluginService'

import { getDataPath } from '@main/utils'

import type {

AgentEntity,

CreateAgentRequest,

CreateAgentResponse,

GetAgentResponse,

ListOptions,

UpdateAgentRequest,

UpdateAgentResponse

} from '@types'

import { AgentBaseSchema } from '@types'

import { asc, count, desc, eq } from 'drizzle-orm'

import { BaseService } from '../BaseService'

import { type AgentRow, agentsTable, type InsertAgentRow } from '../database/schema'

import type { AgentModelField } from '../errors'

const logger = loggerService.withContext('AgentService')

export class AgentService extends BaseService {

private static instance: AgentService | null = null

private readonly modelFields: AgentModelField[] = ['model', 'plan_model', 'small_model']

static getInstance(): AgentService {

if (!AgentService.instance) {

AgentService.instance = new AgentService()

}

return AgentService.instance

}

async initialize(): Promise<void> {

await BaseService.initialize()

}

// Agent Methods

async createAgent(req: CreateAgentRequest): Promise<CreateAgentResponse> {

this.ensureInitialized()

const id = `agent_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`

const now = new Date().toISOString()

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/27 9:26:33

千万注意!实验室装修这5个关键点不容忽视

千万注意&#xff01;实验室装修这5个关键点不容忽视前言实验室装修是一项复杂而细致的工作&#xff0c;不仅需要考虑功能性和安全性&#xff0c;还要兼顾未来的扩展性和合规性。作为专业的实验室装修公司&#xff0c;北京大度空间科技有限公司深知其中的每一个细节都至关重要。…

作者头像 李华
网站建设 2026/2/27 3:43:09

关于指纹浏览器

指尖的隐身衣&#xff1a;指纹浏览器如何重塑网络身份边界在这个数字身份几乎等同于现实身份的时代&#xff0c;每一次点击、每一次浏览都在定义着“你是谁”。而一种被称为“指纹浏览器”的工具&#xff0c;正在这个边界上悄然掀起一场静默革命——它既是隐私的盾牌&#xff0…

作者头像 李华
网站建设 2026/3/1 10:54:12

ModelScope 模型一键上线?FunModel 让你 5 分钟从零到生产

一、前言&#xff1a;AI 浪潮下的模型诉求在当今这个全民 AI 的时代&#xff0c;快速入门 AI&#xff0c;赶上时代的浪潮&#xff0c;成为了大家当下最热切的追求和期盼。于是&#xff0c;模型作为 AI 场景的载体&#xff0c;随着 AI 技术的不断发展&#xff0c;在持续的&#…

作者头像 李华
网站建设 2026/2/24 12:31:32

云服务器与传统服务器

随着信息技术的飞速发展&#xff0c;企业对计算资源的需求日益增长。在这一背景下&#xff0c;服务器作为支撑各类应用和业务运行的核心基础设施&#xff0c;其形态和部署方式也经历了深刻的变革。传统的物理服务器逐渐被灵活高效的云服务器所补充甚至替代。本文将系统介绍云服…

作者头像 李华
网站建设 2026/2/28 22:13:18

Step-Audio 2:颠覆性多模态音频AI如何重新定义人机交互?

Step-Audio 2&#xff1a;颠覆性多模态音频AI如何重新定义人机交互&#xff1f; 【免费下载链接】Step-Audio-2-mini-Think 项目地址: https://ai.gitcode.com/StepFun/Step-Audio-2-mini-Think 还在为传统语音助手"答非所问"而烦恼&#xff1f;&#x1f914…

作者头像 李华
网站建设 2026/3/2 0:16:41

3步掌握OpenUSD在Blender中的高效应用方法

3步掌握OpenUSD在Blender中的高效应用方法 【免费下载链接】OpenUSD Universal Scene Description 项目地址: https://gitcode.com/GitHub_Trending/ope/OpenUSD 你是否曾经遇到过这样的困扰&#xff1a;在Blender中精心制作的模型和材质&#xff0c;想要导入到其他3D软…

作者头像 李华