今晚揭开单例模式的面纱~

什么是单例模式?

保证一个类仅有一个实例,并提供一个全局访问点来获取该实例 ,这便是广为人知的单例模式

我们先暂时抛开单例模式的应用场景,单纯聚焦于它的实现过程。

此时,一个关键问题摆在我们面前:怎样才能确保一个类仅有一个实例呢?

this a question !

通常情况下,当我们定义了一个类(本质上是构造函数)后,通过使用new关键字调用构造函数,能够创建出任意数量的实例对象。以如下代码为例:

js 复制代码
class SingleDog {
    show() {
        console.log('我是一个单例对象');
    }
}
const s1 = new SingleDog();
const s2 = new SingleDog();
// false
console.log(s1 === s2);

在上述代码中,我们先通过new创建了s1,随后又创建了s2

显然,s1s2相互独立,各自占据一块内存空间,它们之间不存在任何关联。然而,单例模式的目标是无论尝试创建多少次,始终只返回首次创建的那个唯一实例。

how to get it ?

要达成这一目标,构造函数需要具备判断自身是否已创建过实例的能力 , 即在创建实例的时候 , 能够拦截最后在检验

我们可以将这段判断逻辑编写成一个静态方法(实际上,也可直接将其写入构造函数的函数体中,这里为了展现 static 的用法 , 我选择一下的方法)

js 复制代码
 static getInstance() {
        // 判断是否已经new过1个实例
        if (!SingleDog.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            SingleDog.instance = new SingleDog();
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return SingleDog.instance;
    }

完整的代码就是下面的代码:

js 复制代码
class SingleDog {
    show() {
        console.log('我是一个单例对象');
    }
    static getInstance() {
        // 判断是否已经new过1个实例
        if (!SingleDog.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            SingleDog.instance = new SingleDog();
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return SingleDog.instance;
    }
}
const s1 = SingleDog.getInstance();
const s2 = SingleDog.getInstance();
// true
console.log(s1 === s2);

除了上述实现方式,getInstance的逻辑还能借助闭包来实现:

js 复制代码
SingleDog.getInstance = (function() {
    // 定义自由变量instance,模拟私有变量
    let instance = null;
    return function() {
        // 判断自由变量是否为null
        if (!instance) {
            // 如果为null则new出唯一实例
            instance = new SingleDog();
        }
        return instance;
    };
})();

可以看到,在getInstance方法的判断与拦截机制下,无论调用多少次,SingleDog都只会返回同一个实例,此时s1s2均指向这个唯一实例。

到这里不得不提到 Vuex 中单例模式的应用了 ~

基于Flux架构状态管理工具,其中Redux和Vuex是应用最为广泛的代表。无论是

  • Redux
  • 还是Vuex

它们都实现了一个全局的Store,用于存储应用的所有状态。而这个Store的实现,正是单例模式的典型应用场景。

我们再次回顾单例模式的关键定义 : 保证一个类(Store)仅有一个实例 , 并且提供全局访问点 🤡 , 使用过状态管理工具的倔友 , 应该深有体会 ~

在此,我们以Vuex为例,深入探究单例模式在其中的具体应用。

Vuex中单例模式

vuex 的出现

Vuex采用单一状态树的设计理念,通过一个对象来囊括应用层级的全部状态。如此一来,它便作为"唯一数据源 (SSOT)"存在。这意味着每个应用仅包含一个store实例。

单一状态树的优势在于,能够让我们快速定位到任意特定的状态片段,在调试过程中,也能轻松获取整个当前应用状态的快照。

引用Vuex官方文档的描述就是:"Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个'唯一数据源 (SSOT)'而存在。

这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。"

在Vue开发中,组件之间相互独立,组件间通信最常用的方式是props,但这种方式仅适用于父组件与子组件之间的通信。对于稍微复杂一些的场景,比如兄弟组件间通信,我们可以通过自定义简单的事件监听函数来解决。

然而,当项目中组件数量众多、组件间关系错综复杂且嵌套层级较深时,这种原始的通信方式会使逻辑变得异常复杂,难以维护。

此时,最佳解决方案是将共享数据抽取出来,放置在全局,供组件按照一定规则进行数据的存取操作,确保状态以可预测的方式发生变化。 于是,Vuex应运而生,这个用于存放共享数据的唯一数据源,就是Store。

当然 , 我更喜欢使用 pinia 这个数据状态管理 , vuex 和 pinia 之间的区别

  • API 设计上,Pinia 更为简洁直观,摒弃了 Vuex 繁杂的模块嵌套与 mutation 命名空间,让开发者能更轻松地定义和使用状态、方法;
  • 开发体验上,Pinia 支持在 setup 函数中使用,契合 Vue3 组合式 API,代码逻辑更清晰、可维护性更强,且对 TypeScript 的支持更为友好;
  • 实现机制看,Pinia 的 Store 是真正意义上的单例,而 Vuex 的 Store 虽在实践中类似单例,但从构造函数源码层面并未严格实现单例逻辑 。

上面提到 vuex 的类 Store 是"假单例" , 我们一起探讨一下 :

首先,我们需要明确"假单例"的概念。在这里,"假单例"指的是虽然没有严格遵循单例模式的设计原则,但在实际应用中依然能够保证实例的唯一性。Vuex中的Store就是这样一个"假单例"------尽管在实际应用中,Store通常仅有一个全局实例,但从实现层面来看,它并非严格意义上的单例模式。接下来,我们将结合Vuex Store的源码进行详细说明。

以下是Store类构造函数的部分源码

js 复制代码
class Store {
    constructor (options = {}) {
        // ...
        this._actions = Object.create(null);
        this._mutations = Object.create(null);
        this._wrappedGetters = Object.create(null);
        this._modulesNamespaceMap = Object.create(null);
        this._subscribers = [];
        this._watcherVM = new Vue();

        // 将 this 赋值给 store,这是为了在后续的函数中使用 Store 实例的上下文
        const store = this;
        // 将 this 中的 dispatch 和 commit 方法解构出来,以便在后续的函数中使用
        const { dispatch, commit } = this;
        // 分别为 dispatch 和 commit 方法绑定上下文
        this.dispatch = function boundDispatch (type, payload) {
            return dispatch.call(store, type, payload);
        };
        this.commit = function boundCommit (type, payload, options) {
            return commit.call(store, type, payload, options);
        };
        // ...
    }
}

在Vuex中,我们可通过new Vuex.Store(options)调用构造函数来创建新的Store实例。从上述贴出的Store构造函数关键源码中可以发现,其中并未包含任何与单例相关的识别或拦截逻辑。

这意味着开发者能够通过new关键字创建多个Store实例,这显然与我们对单例模式的预期不符。

以下是一个创建多个Store实例的示例代码:

js 复制代码
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

// 创建一个 store 对象 1 号
const store1 = new Vuex.Store({
    state: { count: 0 },
    mutations: {
        increment(state) {
            state.count++;
        }
    }
});

// 创建一个 store 对象 2 号
const store2 = new Vuex.Store({
    state: { count: 0 },
    mutations: {
        increment(state) {
            state.count++;
        }
    }
});

// false,说明 store1 和 store2 是完全不同的两个 store
console.log(store1 === store2);

由此可见,尽管Store在实际应用中通常表现得如同单例,但从其自身实现来看,并没有真正实现单例相关的逻辑。那么,没有实现单例的Store,究竟是如何展现出单例般的行为的呢?这就需要从Vuex的整体设计层面进行深入分析。

Vuex如何确保Store 单例特征呢 ?
  • Vuex工作原理分析 :Store虽未实现标准的单例模式,却能展现出类似单例的行为,这得益于Vuex从整体设计层面保证了Store在同一个Vue应用中的唯一性。具体而言,我们首先要关注Vue.use()方法,该方法允许我们为Vue应用安装诸如Vuex这样的插件。Vuex插件本质上是一个对象,其内部实现了一个install方法,在插件安装时,该方法会被调用,从而将Store注入到Vue应用中。也就是说,每调用一次install,Vuex都会尝试向Vue应用注入一个Store。

在install函数源码中,存在一段与前文提到的getInstance()方法极为相似的逻辑:

js 复制代码
let Vue; // 这个Vue的作用和楼上的instance作用一样
...
export function install (_Vue) {
    // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的 store)
    if (Vue && _Vue === Vue) {
        if (process.env.NODE_ENV!== 'production') {
            console.error(
                '[vuex] already installed. Vue.use(Vuex) should be called only once.'
            );
        }
        return;
    }
    // 若没有,则为这个Vue实例对象install一个唯一的Vuex
    Vue = _Vue;
    // 将Vuex的初始化逻辑写进Vue的钩子函数里
    applyMixin(Vue);
}

面试

请手写一个单例模式 😏

基于 Static
js 复制代码
// 定义Storage
class Storage {
    constructor() {
        this.data = {};
    }
    static getInstance() {
        // 判断是否已经new过1个实例
        if (!Storage.instance) {
            // 若这个唯一的实例不存在,那么先创建它
            Storage.instance = new Storage();
        }
        // 如果这个唯一的实例已经存在,则直接返回
        return Storage.instance;
    }
    getItem(key) {
        return this.data[key];
    }
    setItem(key, value) {
        this.data[key] = value;
        return value;
    }
}

const storage1 = Storage.getInstance();
const storage2 = Storage.getInstance();

storage1.setItem('name', '李雷');
// 李雷
console.log(storage1.getItem('name'));
// 也是李雷
console.log(storage2.getItem('name'));

// 返回true
console.log(storage1 === storage2);
    
基于闭包

ES5

js 复制代码
// 先实现一个基础的StorageBase类,把getItem和setItem方法放在它的原型链上
function StorageBase() {
    this.data = {};
}

StorageBase.prototype.getItem = function (key) {
    return this.data[key];
};

StorageBase.prototype.setItem = function (key, value) {
    this.data[key] = value;
    return value;
};

// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function () {
    let instance = null;
    return function () {
        // 判断自由变量是否为null
        if (!instance) {
            // 如果为null则new出唯一实例
            instance = new StorageBase();
        }
        return instance;
    };
})();

// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果 
const storage1 = new Storage();
const storage2 = new Storage();

storage1.setItem('name', '李雷');
// 李雷
console.log(storage1.getItem('name'));
// 也是李雷
console.log(storage2.getItem('name'));

// 返回true
console.log(storage1 === storage2);

ES6

js 复制代码
// 实现一个基础的 StorageBase 类
class StorageBase {
    constructor() {
        this.data = {};
    }

    getItem(key) {
        return this.data[key];
    }

    setItem(key, value) {
        this.data[key] = value;
        return value;
    }
}

// 以闭包的形式创建一个引用自由变量的构造函数
const Storage = (function () {
    let instance = null;
    return function () {
        // 判断自由变量是否为 null
        if (!instance) {
            // 如果为 null 则 new 出唯一实例
            instance = new StorageBase();
        }
        return instance;
    };
})();

// 这里其实不用 new Storage 的形式调用,直接 Storage() 也会有一样的效果 
const storage1 = new Storage();
const storage2 = new Storage();

storage1.setItem('name', '李雷');
// 李雷
console.log(storage1.getItem('name'));
// 也是李雷
console.log(storage2.getItem('name'));

// 返回 true
console.log(storage1 === storage2);
相关推荐
沐土Arvin几秒前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年2 分钟前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖66611 分钟前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
bao_lanlan1 小时前
兰亭妙微:用系统化思维重构智能座舱 UI 体验
ui·设计模式·信息可视化·人机交互·交互·ux·外观模式
L耀早睡1 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer1 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿1 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹2 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹2 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年2 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net