本文正在参加「金石计划」 单例模式的意思是保证一个类只有一个实例,即使多次实例化该类,也只返回第一次实例化后的实例对象。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}`})
}
},
看到即使 this.$showLoginModal 调用多次,也只有一个登录弹出框。(样式有点丑,请忽略)
登录弹出框 (vue3 版本)
在Vue3中,全局 API 有发生不兼容的变化,具体如下表:
| 2.x Global API | 3.x Instance API (app) |
|---|---|
| Vue.config | app.config |
| Vue.config.productionTip | removed (see below) |
| Vue.config.ignoredElements | app.config.compilerOptions.isCustomElement (see below) |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use (see below) |
| Vue.prototype | app.config.globalProperties (see below) |
| Vue.extend | removed (see below) |
跟本文相关的是 Vue.prototype 需要使用 app.config.globalProperties 写法替换,同时废弃了 Vue.extend 方法。那么实例化一个组件,并把它挂在页面上的方法需要用到 h 和 render 函数。
登录组件:
<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 看完这篇,你就学废了。 - 掘金