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

什么是单例模式?

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

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

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

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);
相关推荐
郑祎亦27 分钟前
CSS 实现 文本垂直居中
前端·css
zhongshizhi9130 分钟前
CSS元素层叠顺序规则
前端·css
申朝先生2 小时前
用CSS画一条0.5px的线
前端·javascript·css
wenbin_java2 小时前
设计模式之组合模式:原理、实现与应用
设计模式·组合模式
雪碧聊技术2 小时前
element-plus中Autocomplete自动补全输入框组件的使用
前端·javascript·vue.js·自动补全输入框·autocomplete
浪遏3 小时前
当你向面试官朗诵单例模式时 ,ta说talk is cheep , show me the code🤡
前端·设计模式·面试
zczlsy113 小时前
webpack介绍
前端·webpack·node.js
六个点3 小时前
关于vue的面试考点总结🤯
前端·vue.js·面试
驯龙高手_追风5 小时前
谷歌Chrome或微软Edge浏览器修改网页任意内容
前端·chrome·edge