还不会在前端项目里上设计模式?单例模式篇

303 阅读4分钟

前言

开个新坑,准备聊一下设计模式。网上很多教程、书籍还是停留在 es5,说实话有些老旧,一些类的东西现在几乎不会手动去调 property,用 class 显然更简单明了。

另外,设计模式源于面向对象的语言,而前端的 JS 还是偏向于面向过程。如果直接生搬硬套的方案,只会不伦不类,我们的项目中几乎用不到。

我更希望能学以致用,而非单纯学习理论知识。

所以本系列会更多的加入自己的思考和观点,可能设计模式并没有书上那么标准,但是会更容易应用到我们的日常工作中。

单例模式

假设有这样一个场景,点击按钮,打开对话框

实现它很简单,先写好一个对话框,并将其隐藏掉,用点击目标按钮来显示对话框

<button id="open-dialog-btn">打开弹窗</button>
<div id="dialog" style="display: none;">
  <p>这是一个简单的弹窗。</p>
</div>

<script>
  const dialog = document.getElementById("dialog");
  document.getElementById("open-dialog-btn").addEventListener("click", () => {
    dialog.style.display = "block";
  });

  document.getElementById("close-dialog-btn").addEventListener("click", () => {
    dialog.style.display = "none";
  });
</script>

但是如此一来,会导致一进入页面就需要加载这个模态框。

如果我们完全不需要点击它,那这里的加载就是完全浪费资源的,我们最好将其改造为点击后再进行渲染的方案。

<button id="open-dialog-btn">打开弹窗</button>

<script>
  document.getElementById("open-dialog-btn").addEventListener("click", () => {
    if (!document.getElementById("dialog")) {
      const dialog = document.createElement("div");
      dialog.id = "dialog";
      dialog.innerHTML = `<p>这是一个简单的弹窗。</p>`;
      document.body.appendChild(dialog);
    }
  });
</script>

好的,那么问题就来了。当你点击第二次的时候,这里就又需要再次创建一个弹窗,若是 n 次点击,那就要消耗 n 倍的资源

也许已经有同学想到了,面对这种情况,我们可以选择在创建前进行检测。若是节点已经创建,那么我们只需要将其显示出来即可

<button id="open-dialog-btn">打开弹窗</button>

<script>
  let dialog = null;

  document.getElementById("open-dialog-btn").addEventListener("click", () => {
    if (!dialog) {
      // 如果对话框尚未创建,进行创建
      dialog = document.createElement("div");
      dialog.id = "dialog";
      dialog.innerHTML = `
        <p>这是一个简单的弹窗。</p>
        <button onclick="dialog.style.display = 'none'">关闭</button>
      `;
      document.body.appendChild(dialog);
    } else {
      // 如果对话框已经存在,只是显示它
      dialog.style.display = "block";
    }
  });
</script>

很好,或许你没有意识到,你已经完成了一个单例模式 🎉🎉🎉

单例模式

  • 特点:保证仅有一个实例,并提供一个访问它的全局访问点。
  • 用途:Axios 实例Redux Store缓存池
  • 实现:本质就是将实例对象记录起来,下次再创建的时候直接给旧的而非新建

回到刚才的代码上,目前的dialog方案还不够好,这会产生一个额外的全局变量,而且可能会被其他的地方误用。

有没有办法将其隐藏起来呢?有的兄弟有的,用闭包就行了。

<button id="open-dialog-btn">打开弹窗</button>

<script>
  function createDialog() {
    let dialog = null; // 利用闭包将其封住

    return function () {
      if (!dialog) {
        // 只创建一次弹窗
        dialog = document.createElement("div");
        dialog.id = "dialog";
        dialog.innerHTML = `
          <p>这是一个简单的弹窗。</p>
          <button onclick="this.parentElement.style.display = 'none'">关闭</button>
        `;
        document.body.appendChild(dialog);
      } else {
        // 如果已经存在,直接显示
        dialog.style.display = "block";
      }
    };
  }

  const showDialog = createDialog();
  document.getElementById("open-dialog-btn").addEventListener("click", showDialog);
</script>

非常漂亮,最后,我们或许可以稍微抽象一下,将这套单例模式变得更加可复用。于是我们的单例模式就设计好了:

  function createSingletonDialog(content) {
    let dialog = null;

    return function () {
      if (!dialog) {
        dialog = document.createElement("div");
        dialog.innerHTML = `
          <div>
            ${content}
            <button onclick="this.parentElement.style.display = 'none'">关闭</button>
          </div>
        `;
        document.body.appendChild(dialog);
      } else {
        dialog.style.display = "block";
      }
    };
  }

来应用一下试试,使用这段单例模式函数,我们就能写出以下 漂亮优雅 的代码

<button id="info-btn">显示信息弹窗</button>
<button id="warn-btn">显示警告弹窗</button>
<button id="error-btn">显示错误弹窗</button>

<script>
  function createSingletonDialog(content) {
    let dialog = null;

    return function () {
      if (!dialog) {
        dialog = document.createElement("div");
        dialog.innerHTML = `
          <div>
            ${content}
            <button onclick="this.parentElement.style.display = 'none'">关闭</button>
          </div>
        `;
        document.body.appendChild(dialog);
      } else {
        dialog.style.display = "block";
      }
    };
  }

  // 创建不同类型的弹窗
  const showInfoDialog = createSingletonDialog("<p>这是一个信息弹窗。</p>");
  const showWarnDialog = createSingletonDialog("<p>这是一个警告弹窗。</p>");
  const showErrorDialog = createSingletonDialog("<p>这是一个错误弹窗。</p>");

  // 绑定点击事件
  document.getElementById("info-btn").addEventListener("click", showInfoDialog);
  document.getElementById("warn-btn").addEventListener("click", showWarnDialog);
  document.getElementById("error-btn").addEventListener("click", showErrorDialog);
</script>

其他

我们回看一下整体的代码,其实并没有用传统的 class 来实现,比如这个标准的单例模式:

class Singleton {
    static instance = null;
    
    constructor(data) {
        if (Singleton.instance) return Singleton.instance;
        
        this.data = data;
        Singleton.instance = this;
    }
}

let ob1 = new Singleton("one");
let ob2 = new Singleton("two");
let ob3 = new Singleton("three");
ob2.name = 'ob2';

console.log(ob1 === ob2); // true
console.log(ob1 === ob3); // true

console.log(ob1); // Singleton { data: 'one', name: 'ob2' }
console.log(ob2); // Singleton { data: 'one', name: 'ob2' }
console.log(ob3); // Singleton { data: 'one', name: 'ob2' } 

这样的应用面就太窄了,很难落地在项目中。

相反,用js更擅长的面向过程编程,反而能更好应用。

当你项目中需要使用全局唯一的变量时,不妨回头看本篇文章的思路。

借用一个大佬发言:我写代码会尽可能追求高逼格,让大家觉得我牛逼,这样既有成就感,又能提升技术。

如果有帮助,不妨点个赞吧~