前端设计模式系列——单例模式
持续整理一些前端用到的设计模式,欢迎大家关注专栏:
- 工厂模式
- 单例模式
- 代理模式
- 策略模式
- 建造者模式
- 观察者模式
- 适配器模式
- 装饰器模式
- 迭代器模式
- 中间件模式
- ……
单例模式
基本概念
单例模式是所有程序设计语言中都会用到的一种模式,单例模式的目标就是确保某个类只能出现一个实例,对该类所作的任何访问,都必须通过这个实例执行。例如考虑下面的情况:
- 共享状态信息,不想让同一个实体由好几个对象来表示,否则就会出现状态不一致的情况
- 优化资源占用,不想让用户创建多个相同的资源
一个典型的场景就是通过 Database 类设计一个数据库连接池,这样就可以复用这些连接,而不是每遇到一个请求,就去开一个新的连接。
class Database {
constructor(name, options = {}) {
}
}
于是我们希望全局只能出现一个 Database 类的实例,可以通过下面的静态方法来实现:
class Database {
constructor(name) {
this.name = name
}
static getInstance(name) {
if (!this.instance) {
this.instance = new Database(name)
}
return this.instance
}
}
var w1 = Database.getInstance('mysql')
var w2 = Database.getInstance('mysql')
console.log(w1 === w2) // true
但是这种方式是有缺点的,用户必须调用 getInstance 方法来创建,如果不想抛弃 new 的方式,可以再定义一个 SingleDatabase 对原类进行二次封装:
const SingletonDatabase = (() => {
let db
return function (name) {
if (db) return db
return (db = new Database(name))
}
})()
const db1 = new SingletonDatabase('mysql')
const db2 = new SingletonDatabase('mysql')
console.log(db1 === db2) // true
这样的话,用户通过 new 操作符创建的所有实例,其实都是同一份全局唯一的实例。我们不建议直接修改原来的类,让其默认成为单例,这样会对用户造成困惑。由于 SingletonDatabase 这个类名在语义上已经很清晰了,所以这么做是没问题的。
不过需要注意的是,单例不一定和 new 操作符有关系,即使是一个普通的 JavaScript 对象,如果能够保证其全局唯一,那么就是单例。
记住:单例模式的核心是确保只有一个实例,并提供全局访问。
典型示例
angular
在下面的代码中,通过 @Injectable() 装饰器在全局注册了单例的 user service:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UserService {
}
例如当前的登录用户只可能会有一个,用此方法可以确保全局唯一的登录用户实例。
gulp
在 gulp 中,也是通过 new Gulp()
创建了 Gulp 类的一个实例,然后导出去:
var util = require('util');
var Undertaker = require('undertaker');
var vfs = require('vinyl-fs');
var watch = require('glob-watcher');
function Gulp() {
Undertaker.call(this);
this.watch = this.watch.bind(this);
this.task = this.task.bind(this);
this.series = this.series.bind(this);
this.parallel = this.parallel.bind(this);
this.registry = this.registry.bind(this);
this.tree = this.tree.bind(this);
this.lastRun = this.lastRun.bind(this);
this.src = this.src.bind(this);
this.dest = this.dest.bind(this);
this.symlink = this.symlink.bind(this);
}
util.inherits(Gulp, Undertaker);
// 省略一些代码...
// Let people use this class from our instance
Gulp.prototype.Gulp = Gulp;
var inst = new Gulp();
module.exports = inst;
这样用户无论在哪个文件中引入 gulp 的时候,就会拿到这个唯一的实例:
var gulp = require('gulp'); // 得益于 CommonJS 的模块缓存机制,拿到的是同一个 Gulp 实例
当然,你肯定注意到了这句话:
// Let people use this class from our instance
Gulp.prototype.Gulp = Gulp;
注释说的很明白了,用户可以直接使用 gulp.Gulp 类自己创建一个实例,也就是说如果用户不想要单例,而是想创建多实例,这就是逃生通道。
redux
在 redux 项目中,我们会通过 createStore 函数创建一个全局的 store:
import { createStore } from 'redux'
function reducer(state, action) {
return state
}
const store = createStore(reducer)
export default store
这个 store 就是全局单例的,可以放在 Provider 组件的 store 属性里面供所有组件使用:
ReactDOM.render(
<Provider store={store}>
{//省略无关代码}
</Provider>,
document.getElementById('app')
)
想想看,如果调用两次 createStore 创建 store 对象,那它们之间的状态是完全隔离的,那么组件之间就无法通过 store 来进行通信,因为状态无法共享。本质上是因为 createStore 返回的 store 对象中引用到的状态是保存在这个函数内部的,是闭包变量:
export default function createStore(reducer, preloadedState?, enhancer?) {
let currentReducer = reducer
let currentState = preloadedState as S
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false
// 省略很多行代码...
const store = {
dispatch,
subscribe,
getState,
replaceReducer,
}
return store
}
history
同样在 React 项目中,如果使用了 react-router 路由模块,往往会通过 history 这个包创建全局唯一的 hashHistory 或者 browserHistory:
import { createHashHistory, createBrowserHistory } from "history";
const hashHistory = createHashHistory();
window.hashHistory = hashHistory; // 挂载 window 上面方便调试
export default hashHistory;
然后放到 Router 组件的 history 属性里面:
ReactDOM.render(
<Router history={hashHistory}>
<Switch>
<Route path="/" component={App} />
</Switch>
</Router>,
document.getElementById('app')
)
createHashHistory 的逻辑跟 redux 中的 createStore 几乎如出一辙,都是在函数内部保存一些状态变量,然后返回一个可以操纵这些闭包变量的对象:
export function createHashHistory(options: HashHistoryOptions = {}): HashHistory {
let { window = document.defaultView! } = options;
let globalHistory = window.history;
let action = Action.Pop;
let [index, location] = getIndexAndLocation();
let listeners = createEvents<Listener>();
let blockers = createEvents<Blocker>();
// 省略代码...
let history: HashHistory = {
get action() { return action; },
get location() { return location; },
createHref,
push,
replace,
go,
back() { go(-1); },
forward() { go(1); },
listen(listener) { return listeners.push(listener); },
};
return history;
}
转载自:https://juejin.cn/post/7184685905859313724