设计模式系列之单例模式及其应用场景

177 阅读4分钟
  • 设计模式系列之单例模式及其应用场景

  • 定义:全局单一实例

    • 维基百科:单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
    • 核心要点:
      • 一个类,能返回对象的一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法);
      • 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;
      • 将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
  • 类型

    • 第一版 【ES3/ES5时代】

        let thisInstance = null;
      
        // 构造函数中的this 表示实例对象本身
        function Singleton(){
          if (!thisInstance){
              thisInstance = this
            }
          return thisInstance
        }
      
        const n1 = new Singleton();
        const n2 = new Singleton();
        console.log(n1 === n2);
      
      • 分析上述例子会发现,thisInstance是无法控制,单例越多,这种全局标识越多,会有全局变量污染问题
    • 第二版 【ES3/ES5时代】

        const Singleton = (function () {
          let thisInstance = null;
      
          return function () {
            if (thisInstance) {
              return thisInstance;
            }
            return (thisInstance = this);
          };
        })();
      
        const n1 = new Singleton();
        const n2 = new Singleton();
      
        console.log(n1 === n2);
      
      • 这个例子,用了闭包,将变量封装起来,避免了全局污染问题
    • 第三版 【接受业务参数】【ES3/ES5时代】

        const SingletonCreator = (function () {
            let thisInstance = null;
            return function (param) {
              if (thisInstance) {
                return thisInstance;
              }else{
                  this.param=param;
                  return (thisInstance = this);
              }
              
              
            };
        })();
        const n1 = new SingletonCreator("6666");
        const n2 = new SingletonCreator("9999");
        console.log(n1 === n2, n1, n2);//true
      
      
        /* 稍微升级下 */
        function Singleton (param){
          this.param=param
        }
      
        const SingletonCreator = (function () {
          let thisInstance = null;
      
          return function (param) {
            if (thisInstance) {
              return thisInstance;
            }
            
            return (thisInstance = new Singleton(param));
          };
        })();
      
        const n1 = new SingletonCreator("6666");
        const n2 = new SingletonCreator("9999");
      
        console.log(n1 === n2, n1, n2);//true
      
      • 这个例子,演示了将创建层和业务赋值层进行了剥离【单一原则】
    • 第四版 【ES6时代】

      class singleton {
        constructor () {
          // 保证只有一个实例
          if (!singleton.instance) {
            singleton.instance = this;
          }
          return singleton.instance
        }
      }
    
      const n1 = new singleton();
      const n2 = new singleton();
      console.log(n1===n2) //true
    
    • 第五版 【懒汉式/惰性单例】【使用时创建】
      var createLoginLayer = (function() {
          var el;
          return function() {
              if(!el) {
                  el = document.createElement('h1');
                  el.innerHTML = "试一试1";
                  document.body.append(el);
              }
              return el;
          }
      })()
    
      document.querySelector("body").onclick = function() {
          var loginLayer = createLoginLayer();
      }
    
    • 这里面有个问题:创建弹窗被写在了内部,一旦用户创建message,创建alert...又得重新写一遍,于是继续优化

    • 第六版

      // 通用的惰性单例
      var getSingle = function(fn) {
          var result;
          return function() {
              return result || (result = fn.apply(this, arguments));
          }
      }
    
      // 创建登录弹窗的方法就可以改写成
      var createLoginLayer = function() {
          var el = document.createElement('h1');
           el.innerHTML = "试一试2";
          document.body.append(el);
          return el;
      }
    
      var createSingleLoginLayer = getSingle(createLoginLayer);
    
       document.querySelector("body").onclick = function() {
          var loginLayer = createSingleLoginLayer();
      }
    
  • 应用场景及其解析

    • 1、ESM模块系统的导入导出export和import【这个平时开发最常用】

      • ESM模块系统的导入导出export和import是单例的,这个估计是平时开发最常用,接触最多的,EX:
          import { A as A1 } from 'A';
          import { A as A2 } from 'A';
          import { A as A3 } from 'A';
          console.log(A1===A2===A3); //true
        
          // Tips:这里假设原来A上b属性【如果没有的话,赋值新新属性会报错】
          A1.b=666;
        
          console.log(A1.b===A2.b===A3.b===666); //true
        
    • 2、全局态管理 store (Vuex / Redux),甚至全局localStorage等都是单例

        /**
         * 看一看vuex源码
         * https://github.com/vuejs/vuex/blob/1.0/src/index.js
         */
        function install (_Vue) {
          if (Vue) {
            console.warn(
              '[vuex] already installed. Vue.use(Vuex) should be called only once.'
            )
            return
          }
          Vue = _Vue
          override(Vue)
        }
    
        // auto install in dist mode
        if (typeof window !== 'undefined' && window.Vue) {
          install(window.Vue)
        }
    
    • 3、弹窗(登录框,信息提升框)【这个最常见】

      • 弹窗可能是单例应用最常见的落地场景了,这里手写一个全局弹窗来加深理解
          /* 写一个通用的,符合单例,符合单一原则 */
          // 创建mark
          var createMask = function(){
              var el = document.createElement("div");
              var child = document.createElement("div");
              child.innerHTML = "我是弹窗";
              var childStyle={
                padding:"120px 200px",
                backgroundColor: "#fff",
                fontSize: "30px",
                color:"#333",
                margin: "0 auto",
                width: "550px",
                top: "50%",
                position: "relative",
                transform: "translateY(-50%)",
                borderRadius: "8px",
                textAlign:"center",
              }
              Object.keys(childStyle).forEach(item=>{child.style[item] = childStyle[item]})
              el.appendChild(child);
              var elStyle={
                display:'none',
                position: "fixed",
                left: 0,
                right: 0,
                top: 0,
                bottom: 0,
                zIndex: 600,
                backgroundColor: "rgba(0, 0, 0, 0.4)",
              }
              Object.keys(elStyle).forEach(item=>{el.style[item] = elStyle[item]})
              document.body.appendChild(el);
              return el;
          };
      
          //实例封装
          var getInstance = function(fn) {
              var result;
              return function(){
                  return result || (result = fn.apply(this,arguments));
              }
          };
      
          // 测试
          var createSingletonMask = getInstance(createMask);
      
          document.querySelector("body").onclick = function(){
              var modal = createSingletonMask();
              if( modal.style.display==="block"){
                 modal.style.display = "none"
              }else{
                 modal.style.display = "block";
              }
          };
      
        /**
         * 看一看antd的Notification源码
         * ant-design/components/notification/index.tsx
         */
        function getNotificationInstance(
          args: ArgsProps,
          callback: (info: {
            prefixCls: string;
            iconPrefixCls: string;
            instance: RCNotificationInstance;
          }) => void,
        ) {
          const {
            placement = defaultPlacement,
            top,
            bottom,
            getContainer = defaultGetContainer,
            prefixCls: customizePrefixCls,
          } = args;
          const { getPrefixCls, getIconPrefixCls } = globalConfig();
          const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
          const iconPrefixCls = getIconPrefixCls();
      
          const cacheKey = `${prefixCls}-${placement}`;
          const cacheInstance = notificationInstance[cacheKey];
      
          // 1、如果已经存在了实例,赋值已存在的实例,直接返回
          if (cacheInstance) {
            Promise.resolve(cacheInstance).then(instance => {
              callback({ prefixCls: `${prefixCls}-notice`, iconPrefixCls, instance });
            });
      
            return;
          }
      
          const notificationClass = classNames(`${prefixCls}-${placement}`, {
            [`${prefixCls}-rtl`]: rtl === true,
          });
      
          // 2、如果没有缓存实例,重新构建生成
          notificationInstance[cacheKey] = new Promise(resolve => {
            Notification.newInstance(
              {
                prefixCls,
                className: notificationClass,
                style: getPlacementStyle(placement, top, bottom),
                getContainer,
                maxCount,
              },
              notification => {
                resolve(notification);
                callback({
                  prefixCls: `${prefixCls}-notice`,
                  iconPrefixCls,
                  instance: notification,
                });
              },
            );
          });
        }
      
    • 后续模式敬请期待......