单例模式

127 阅读2分钟

本文正在参加「金石计划」 单例模式的意思是保证一个类只有一个实例,即使多次实例化该类,也只返回第一次实例化后的实例对象。ES module 导出的对象,可以看成是单例的,全局只有一个。

// 全局只有一个
export const globalStr = 'Hello world'

js 实现单例模式

const Singleton = function(name){
    this.name = name
}
// 获取单例的方法
Singleton.getInstance = (name) => {
    if(!this.instance){
        this.instance = new Singleton(name)
    }
    return this.instance
}
const a = Singleton.getInstance('a')
const b = Singleton.getInstance('b')
console.log(a===b)    // true

ES6 实现单例模式

借助 ES6 的 class static 语法实现:

class Singleton{
    constructor(name){
      this.name = name
    }
    static getInstance(name){
      if(!this.instance){
        this.instance = new Singleton(name)
      }
      return this.instance
    }
}

const a = Singleton.getInstance('a')
const b = Singleton.getInstance('b')
console.log(a===b)    // true

应用

登录弹出框 (vue2 版本)

登录弹出框组件一般需要全局可用,通过 this.$showLoginModal() 直接调用。在 vue2 中,想通过 this.XX 调用方法,需要将该方法绑定在 Vue.prototype 原型对象上。登录弹出框组件状态应该是内部 data 维护的,而不是通过 props 控制。
弹出框组件代码:

// src/components/LoginModal.vue
<template>
  <div class="login-modal-container" v-show="showModal">
    <div class="login-modal-container__wrap">
      <div class="login-modal-container__header">
        <span>登录弹出框</span>
        <span @click="closeLoginModal">close</span>
      </div>
      <div class="login-modal-container__content">content</div>
      <div class="login-modal-container__footer">footer</div>
    </div>
  </div>
</template>
<script lang="ts">
export default {
  name: 'LoginModal',                               
  data(){
    return{
      showModal: true
    }
  },
  methods: {
    closeLoginModal(){
      this.showModal = false
    }
  }
}
</script>
<style lang="scss" scoped>
.login-modal-container{
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: #00000073;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 2000;
  &__wrap{
    background: white;
    width: 400px;
    height: 350px;
    opacity: 1;
    display: flex;
    flex-direction: column;
  }
  &__header{
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  &__content{
    flex: 1;
  }
}
</style>

通过 Vue.prototype.$showLoginModal 挂载全局方法 this.$showLoginModal

// src/main.js
import LoginModal from './components/LoginModal.vue'
// 返回 LoginModalVue 构造函数
const LoginModalVue = Vue.extend(LoginModal)

const showLoginModal = () => {
 // 单例模式设计,只存在一个实例
  let instance;
  return (options)=>{
    if(!instance){
      instance = new LoginModalVue().$mount()
      document.body.appendChild(instance.$el)
    }
    // options 属性绑定
    for(let key in options){
      instance[key] = options[key]
    }
    instance.showModal = true
  }
}

Vue.prototype.$showLoginModal = showLoginModal()

使用:

// src/pages/energy/index.vue
  methods: {
    changeModal(){
      this.show = !this.show
      this.$showLoginModal({title: `测试${this.show}`})
      this.$showLoginModal({title: `测试${this.show}`})
      this.$showLoginModal({title: `测试${this.show}`})
    }
  },

image.png 看到即使 this.$showLoginModal 调用多次,也只有一个登录弹出框。(样式有点丑,请忽略)

登录弹出框 (vue3 版本)

在Vue3中,全局 API 有发生不兼容的变化,具体如下表:

2.x Global API3.x Instance API (app)
Vue.configapp.config
Vue.config.productionTipremoved (see below)
Vue.config.ignoredElementsapp.config.compilerOptions.isCustomElement (see below)
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use (see below)
Vue.prototypeapp.config.globalProperties (see below)
Vue.extendremoved (see below)

跟本文相关的是 Vue.prototype 需要使用 app.config.globalProperties 写法替换,同时废弃了 Vue.extend 方法。那么实例化一个组件,并把它挂在页面上的方法需要用到 hrender 函数。 登录组件:

<template>
  <div class="login-modal-container" v-show="showModal">
    <div class="login-modal-container__wrap">
      <div class="login-modal-container__header">
        <span>{{title}}</span>
        <span @click="closeLoginModal">close</span>
      </div>
      <div class="login-modal-container__content">content</div>
      <div class="login-modal-container__footer">footer</div>
    </div>
  </div>
</template>
<script lang="ts" setup>
  import {ref} from 'vue'
  const showModal = ref(true)
  const title = ref('登录弹出框')
  const closeLoginModal = ()=>{
    showModal.value = false
  }
</script>

在 main.js 中添加方法:

// src/main.ts
import { h, render, } from 'vue'
import LoginModal from './components/LoginModal.vue'

const showLoginModal = ()=>{
  let instance
  let showContainer
  return (options)=>{
     // 单例模式
    if(!instance){
      instance = h(LoginModal)
      showContainer = document.createElement('div')
      document.body.appendChild(showContainer)
      render(instance, showContainer)
    }
    // 属性绑定
    for(const key in options){
      instance.component.setupState[key] = options[key]
    }
    instance.component.setupState.showModal = true

  }
}

const app = createApp(App)
app.config.globalProperties.$showLoginModal = showLoginModal()

使用,需要注意:getCurrentInstance() 需要在 setup 中直接使用,不然会出现 null 的问题。

// src/views/HomeView.vue
  import { ref, onMounted, getCurrentInstance, } from 'vue'
  const showLoginModal = getCurrentInstance()?.proxy.$showLoginModal

  const showModal = ()=> showLoginModal({title: '测试'})

自己实现类似 VueX 或 pinia 全局状态管理

原理是利用 ES Module 的单例模式,通过 import 引入的变量是值的引用:

// src/store/useEnumStore.ts
import { ref, } from 'vue'

interface InitialMethod{
  (params?: any): Promise<any>
}

const defaultFn = () => new Promise((resolve)=>{
  console.log('request')
  resolve([{label: "菠萝", status: null, value: 1}, {label: "苹果", status: null, value: 2}])
}) 

// 全局唯一
const globalEnums = ref()
let isFetching = false

export default (initialFn?: InitialMethod) => {
  // 获取一个枚举对象里面的所有值
  const getEnum = ()=>{
    if(!globalEnums.value && !isFetching){
      isFetching = true
      const fetch = initialFn || defaultFn
      fetch().then(res=>{
        globalEnums.value = res
        isFetching = false
      })
    }
    return globalEnums
  }
  // setEnum
  const setEnum = (value: any[])=>{
    globalEnums.value = value
  }

  return{
    getEnum,
    setEnum
  }
}

使用:

const { getEnum, setEnum, } = useEnumStore()
const enum = getEnum()

从 Composition API 源码分析 getCurrentInstance() 为何返回 null
从ES6重新认识JavaScript设计模式(一): 单例模式 - 知乎
JS 单例设计模式解读与实践(Vue 中的单例登录弹窗)_vue 单例_流光D的博客-CSDN博客
Vue.extend 看完这篇,你就学废了。 - 掘金