阅读 750

JavaScript设计模式第1篇:单例模式

什么是单例模式

单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个访问它的全局访问点

单例模式是设计模式中较为简单,好理解的一种模式,但是它的使用场景是很广泛的,包括大名鼎鼎的 Vuex 也使用了单例模式。

本文会介绍单例模式的两种实现方法:类和闭包,同时也会对Vuex中的单例模式进行介绍。

实现方式

类Class是ES6新增的语法,在之前我们想要新建一个对象实例,是通过new构造函数的方式来实现的。我们每次调用new的时候,都会生成一个新的实例对象,每个实例对象之间是完全独立的。

function Car (name) {
    this.name = name;
}

var car1 = new Car('benz');
var car2 = new Car('audi');

console.log(car1 === car2)      // false
复制代码

那么我们来思考一下,怎么才能每次new都返回同一个实例对象呢?

肯定是有一个变量将第一次new生成的实例对象保存了下来,后面再执行new的时候,就直接返回第一次生成的实例对象,这样就实现了单例。

我们通过两种方法来实践一下:类和闭包。

类实现

class SingletonCar {
    constructor () {
        this.name = 'benz';
    }
    static getInstance () {
        if (!SingletonCar.instance) {
            SingletonCar.instance = new SingletonCar();
        }
        return SingletonCar.instance;
    }
}

let car1 = SingletonCar.getInstance();
let car2 = SingletonCar.getInstance();

console.log(car1 === car2)      // true
复制代码

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

静态方法可以直接在父类上调用SingletonCar.getInstance(),而不是在实例对象上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

用类来实现单例模式,只要记住这个getInstance()静态方法就可以了。

闭包实现

var SingletonCar = (function () {
    var instance;
    
    var SingletonCarTemp = function () {
        this.name = 'benz';
    };
    
    return function () {
        if (!instance) {
            instance = new SingletonCarTemp();
        }
        return instance;
    }
})();

var car1 = new SingletonCar();
var car2 = new SingletonCar();

console.log(car1 === car2)      // true
复制代码

借助闭包,在内存中保留了 instance 变量,不会被垃圾回收,用来保存唯一的实例,多次调用 new 的时候,只返回第一次创建的实例。

Vuex中的单例模式

Vuex 是 Vue 的状态管理类库,类似于 Redux 之于 React,其实他们的理念都是来自于 Flux 架构,用一个全局的 Store 存储应用所有的状态,然后提供一些 API 供用户去读取和修改。一看到全局唯一的 Store,就可以想到是单例模式了。

如何引用 Vuex

import Vue from 'vue'
import Vuex form 'vuex'
import store from './store'

Vue.use(Vuex)

new Vue({
  el: '#app',
  store
})
复制代码

Vuex 是一个插件,通过调用 Vue.use() 方法可以安装这个插件,具体 Vue 插件的写法可以参考官方文档

Vuex 在内部实现了一个 install 方法,该方法会在 Vue.use() 时被调用,Vue 会被作为参数传入 install 方法中,从而把 Store 注入到 Vue 中。但是如果你试过在开发环境多次调用 Vue.use(Vuex) 的话,就会知道是浏览器是会报错的,接下来我们看一下 Vuex 的内部实现。

Vuex 如何保证唯一 Store

上 Vuex 部分源码:

let Vue

...

export function install (_Vue) {
  // 是否已经执行过了 Vue.use(Vuex),如果在非生产环境多次执行,则提示错误
  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.use(Vuex),则把传入的 _Vue 赋值给定义的变量 Vue
  Vue = _Vue
  // Vuex 初始化逻辑
  applyMixin(Vue)
}
复制代码

完整源码路径在 [vuex/src/store.js](github.com/vuejs/vuex/…

我们可以看到,在 Vuex 内部,先定义了一个变量 Vue,注意这里不是真正的 Vue,只是一个变量,也叫 Vue。

在 Vue.use(Vuex) 的时候,会调用 install 方法,真正的 Vue 会被当做参数传入,如果多次执行 Vue.use(Vuex),也只会生效一次,也就是只会执行一次 applyMixin(Vue),所以只会有一份唯一的 Store,这就是 Vuex 中单例模式的实现。

练习

实现一个全局唯一的 Loading 遮罩。

思路

我们在业务开发的过程中,有很多需求都会有 Loading 状态,这时候直接掏出单例模式,记住上面的 getInstance 静态方法或者闭包 instance 变量,三下五除二即可实现。

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>单例模式全局Loading</title>
  <style>
    .loading {
      width: 100vw;
      height: 100vh;
      line-height: 100vh;
      position: absolute;
      top: 0;
      left: 0;
      background-color: #000;
      opacity: .7;
      color: #fff;
      text-align: center;
    }
  </style>
</head>
<body>
  <button id="startLoading">点击加载</button>

  <script>
    const Loading = (function () {
      let instance

      return function () {
        if (!instance) {
          instance = document.createElement('div')
          instance.innerHTML = '加载中...'
          instance.id = 'loading'
          instance.className = 'loading'
          document.body.appendChild(instance)
        }
        return instance
      }
    })()

    document.getElementById('startLoading').addEventListener('click', () => {
      const loading = new Loading()
    })
  </script>
</body>
</html>
复制代码

总结

单例模式,就是确保一个类只有一个实例,如果采用 class 来实现,记住 getInstance 静态方法,如果采用闭包来实现,记住 instance 变量。当我们在业务开发中,遇到类似于 Vuex 这种需要全局唯一状态的时候,就是单例模式登场的时候。