JavaScript 发布订阅模式详解与购物车重构实战

作者:半夏时间:2026-10-22 07:46:02浏览:449

本文深入解析 JavaScript 中的发布订阅者模式,通过房产中介与网站订阅等生活案例阐明其核心逻辑。重点展示如何从基础实现演进到支持分类订阅的通用框架,并基于该模式对高耦合的面向过程购物车进行重构,提升代码的可维护性与复用性。

发布订阅模式概述

发布订阅模式是软件开发中广泛采用的一种设计模式,其核心思想在于解耦消息发送者与接收者。

房产交易场景

一、房产交易场景

在现实生活的房屋买卖中,买家、卖家与中介机构共同构成了一个典型的发布订阅模型。买家作为订阅者,关注房源详情、价格及面积等关键信息。

中介机构扮演发布者角色,它掌握着卖家的房源数据,并依据系统中存储的客户联系方式(如买家手机号)向潜在买家推送信息。

卖家若想出售房屋,需将房源信息发布给中介,由中介负责向匹配的买家进行广播通知。

网站信息订阅场景

二、网站信息订阅场景

在此场景中,订阅者指代那些希望获取特定类型资讯的用户群体,例如关注 JavaScript 技术文章的技术人员。

发布者则由邮箱服务器担任,它依据网站收集的用户订阅邮箱列表,向用户发送通知邮件。

网站运营者若希望触达订阅用户,需将文章内容提交给邮箱服务器,由服务器执行发送操作。

此类应用场景在软件系统中无处不在,此处不再赘述其他案例。

发布订阅框架推导

本文将以网站订阅为例,推导发布订阅者的基础框架,并进一步利用该框架重构一个简易购物车功能。


var Site = {};
    Site.userList = [];
    Site.subscribe = function( fn ){
      this.userList.push( fn );
    }
    Site.publish = function(){
      for( var i = 0, len = this.userList.length; i < len; i++ ){
        this.userList[i].apply( this, arguments );
      } 
    }
    Site.subscribe( function( type ){
      console.log( "网站发布了" + type + "内容" );
    });
    Site.subscribe( function( type ){
      console.log( "网站发布了" + type + "内容" );
    });
    Site.publish( 'javascript' );
    Site.publish( 'html5' );

代码中 Site.userList 数组用于存储所有已注册的订阅者函数。

Site.subscribe 方法负责注册具体的订阅行为,将每个订阅者函数推入 Site.userList 集合中保存。

Site.publish 方法实现发布者逻辑:遍历 Site.userList 中保存的所有函数,并依次执行其中的业务逻辑。

然而,发布订阅者模式存在一个缺陷:无法按类型订阅。前例中添加了两个订阅者(第11行、第14行),只要网站发布消息,所有订阅者均会接收。但部分用户可能仅希望接收javascript或html5相关内容。因此,需进一步完善机制,确保能接收具体信息,若未订阅特定类型则不接收。

支持分类订阅的实现


var Site = {};
    Site.userList = {};
    Site.subscribe = function (key, fn) {
      if (!this.userList[key]) {
        this.userList[key] = [];
      }
      this.userList[key].push(fn);
    }
    Site.publish = function () {
      var key = Array.prototype.shift.apply(arguments),
        fns = this.userList[key];
      if ( !fns || fns.length === 0) {
        console.log( '没有人订阅' + key + "这个分类的文章" );
        return false;
      }
      for (var i = 0, len = fns.length; i < len; i++) {
        fns[i].apply(this, arguments);
      }
    }

    Site.subscribe( "javascript", function( title ){
      console.log( title );
    });

    Site.subscribe( "es6", function( title ){
      console.log( title );
    });

    Site.publish( "javascript", "[js高手之路]寄生组合式继承的优势" );
    Site.publish( "es6", "[js高手之路]es6系列教程 - var, let, const详解" );
    Site.publish( "html5", "html5新的语义化标签" );

执行输出结果如下:

[js高手之路]寄生组合式继承的优势

[js高手之路]es6系列教程 - var, let, const详解

控制台提示:没有人订阅 html5 这个分类的文章

观察输出可知,仅当用户订阅了 JavaScript 类型时,才能收到“寄生组合式继承的优势”这篇文章;而当发布 HTML5 类型内容时,因无人订阅该分类,故无任何用户收到通知。

同理,只有订阅了 es6 类别的用户,才能接收到 es6 类型的发布内容。

购物车重构实战

基于上述基本实现,我们将其完善为一个通用的发布订阅框架,以便在其他功能模块或网站系统中复用。


var Event = {
      userList : {},
      subscribe : function (key, fn) {
        if (!this.userList[key]) {
          this.userList[key] = [];
        }
        this.userList[key].push(fn);
      },
      publish : function () {
        var key = Array.prototype.shift.apply(arguments),
          fns = this.userList[key];
        if (!fns || fns.length === 0) {
          console.log('没有人订阅' + key + "这个分类的文章");
          return false;
        }
        for (var i = 0, len = fns.length; i < len; i++) {
          fns[i].apply(this, arguments);
        }
      }
    };

    var extend = function( dstObj, srcObj ){
      for( var key in srcObj ){
        dstObj[key] = srcObj[key];
      }
    }

    var Site = {};
    extend( Site, Event );
     Site.subscribe( "javascript", function( title ){
      console.log( title );
    });

    Site.subscribe( "es6", function( title ){
      console.log( title );
    });

    Site.publish( "javascript", "寄生组合式继承的优势" );
    Site.publish( "es6", "es6系列教程 - var, let, const详解" );
    Site.publish( "html5", "html5新的语义化标签" );

接下来,我们利用该框架重构一个购物车实例。在重构前,原购物车采用面向过程的方式编写。

重构前后代码对比


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <script src="js/cart.js"></script>
</head>
<body>
<div id="box">
  <ul>
    <li>
      <input type="button" value="-">
      <span class="num">0</span>
      <input type="button" value="+">
      <span>单价:</span>
      <span class="unit">15元;</span>
      <span class="label">小计:</span>
      <span class="subtotal">0</span>元
    </li>
    <li>
      <input type="button" value="-">
      <span class="num">0</span>
      <input type="button" value="+">
      <span>单价:</span>
      <span class="unit">10元;</span>
      <span class="label">小计:</span>
      <span class="subtotal">0</span>元
    </li>
    <li>
      <input type="button" value="-">
      <span class="num">0</span>
      <input type="button" value="+">
      <span>单价:</span>
      <span class="unit">5元;</span>
      <span class="label">小计:</span>
      <span class="subtotal">0</span>元
    </li>
    <li>
      <input type="button" value="-">
      <span class="num">0</span>
      <input type="button" value="+">
      <span>单价:</span>
      <span class="unit">2元;</span>
      <span class="label">小计:</span>
      <span class="subtotal">0</span>元
    </li>
    <li>
      <input type="button" value="-">
      <span class="num">0</span>
      <input type="button" value="+">
      <span>单价:</span>
      <span class="unit">1元;</span>
      <span class="label">小计:</span>
      <span class="subtotal">0</span>元
    </li>
  </ul>
  <div class="total-box">
    商品一共
    <span id="goods-num">0</span>
    件;
    一共花费
    <span id="total-price">0</span>
    元;
    其中最贵的商品单价是<span id="unit-price">0</span>元
  </div>
</div>
</body>
</html>

对应 cart.js 文件内容如下:


function getByClass(cName, obj) {
  var o = null;
  if (arguments.length == 2) {
    o = obj;
  } else {
    o = document;
  }
  var allNode = o.getElementsByTagName("*");
  var aNode = [];
  for( var i = 0 ; i < allNode.length; i++ ){
    if( allNode[i].className == cName ){
     aNode.push( allNode[i] );
    }
  }
  return aNode;
}

function getSubTotal( unitPrice, goodsNum ){
  return unitPrice * goodsNum;
}

function getSum(){ //计算总花费
  var aSubtotal = getByClass("subtotal");
  var res = 0;
  for( var i = 0; i < aSubtotal.length; i++ ){
    res += parseInt(aSubtotal[i].innerHTML);
  }
  return res;
}

function compareUnit() { //比单价,找出最高的单价
  var aNum = getByClass( "num");
  var aUnit = getByClass( "unit");
  var temp = 0;
  for( var i = 0; i < aNum.length; i++ ){
    if( parseInt(aNum[i].innerHTML) != 0 ){
      if( temp < parseInt(aUnit[i].innerHTML) ) {
        temp = parseInt(aUnit[i].innerHTML);
      }
    }
  }
  return temp;
}

window.onload = function () {
  var aInput = document.getElementsByTagName("input");
  var total = 0;
  var oGoodsNum = document.getElementById("goods-num");
  var oTotalPrice = document.getElementById("total-price");
  var oUnitPrice = document.getElementById("unit-price");

  for (var i = 0; i < aInput.length; i++) {
    if (i % 2 != 0) { //加号
      aInput[i].onclick = function () {
        //当前加号所在行的数量
        var aNum = getByClass( "num", this.parentNode );
        var n = parseInt( aNum[0].innerHTML );
        n++;
        aNum[0].innerHTML = n;
        //获取单价
        var aUnit = getByClass( "unit", this.parentNode );
        var unitPrice = parseInt(aUnit[0].innerHTML);
        var subtotal = getSubTotal( unitPrice, n );
        var aSubtotal = getByClass( "subtotal", this.parentNode );
        aSubtotal[0].innerHTML = subtotal;
        total++; //商品总数
        oGoodsNum.innerHTML = total;
        oTotalPrice.innerHTML = getSum();
        oUnitPrice.innerHTML = compareUnit();
      }
    }else {
      aInput[i].onclick = function(){
        var aNum = getByClass( "num", this.parentNode );
        if ( parseInt( aNum[0].innerHTML ) != 0 ){
          var n = parseInt( aNum[0].innerHTML );
          n--;
          aNum[0].innerHTML = n;
          //获取单价
          var aUnit = getByClass( "unit", this.parentNode );
          var unitPrice = parseInt(aUnit[0].innerHTML);
          var subtotal = getSubTotal( unitPrice, n );
          var aSubtotal = getByClass( "subtotal", this.parentNode );
          aSubtotal[0].innerHTML = subtotal;
          total--; //商品总数
          oGoodsNum.innerHTML = total;
          oTotalPrice.innerHTML = getSum();
          oUnitPrice.innerHTML = compareUnit();
        }
      }
    }
  }
}

原有实现耦合度过高,导致代码可维护性极差。

重构后的购物车结构如下:


window.onload = function () {
  var Event = {
    userList: {},
    subscribe: function (key, fn) {
      if (!this.userList[key]) {
        this.userList[key] = [];
      }
      this.userList[key].push(fn);
    },
    publish: function () {
      var key = Array.prototype.shift.apply(arguments),
        fns = this.userList[key];
      if (!fns || fns.length === 0) {
        return false;
      }
      for (var i = 0, len = fns.length; i < len; i++) {
        fns[i].apply(this, arguments);
      }
    }
  };
  (function(){
    var aBtnMinus = document.querySelectorAll( "#box li>input:first-child"),
      aBtnPlus = document.querySelectorAll( "#box li>input:nth-of-type(2)"),
      curNum = 0, curUnitPrice = 0;

    for( var i = 0, len = aBtnMinus.length; i < len; i++ ){
      aBtnMinus[i].index = aBtnPlus[i].index = i;
      aBtnMinus[i].onclick = function(){
        (this.parentNode.children[1].innerHTML > 0) && Event.publish( "total-goods-num-minus" );
        --this.parentNode.children[1].innerHTML < 0 && (this.parentNode.children[1].innerHTML = 0);
        curUnitPrice = this.parentNode.children[4].innerHTML;
        Event.publish( "minus-num" + this.index, 
          parseInt( curUnitPrice ),
          parseInt( this.parentNode.children[1].innerHTML )
        );
      };
      aBtnPlus[i].onclick = function(){
        (this.parentNode.children[1].innerHTML >= 0) && Event.publish( "total-goods-num-plus" );
        this.parentNode.children[1].innerHTML++;
        curUnitPrice = this.parentNode.children[4].innerHTML;
        Event.publish( "plus-num" + this.index, 
          parseInt( curUnitPrice ),
          parseInt( this.parentNode.children[1].innerHTML )
        );
      }
    }
  })();
  (function(){
    var aSubtotal = document.querySelectorAll("#box .subtotal"),
      oGoodsNum = document.querySelector("#goods-num"),
      oTotalPrice = document.querySelector("#total-price");
      Event.subscribe( 'total-goods-num-plus', function(){
        ++oGoodsNum.innerHTML;
      });
      Event.subscribe( 'total-goods-num-minus', function(){
        --oGoodsNum.innerHTML;
      });
    for( let i = 0, len = aSubtotal.length; i < len; i++ ){
      Event.subscribe( 'minus-num' + i, function( unitPrice, num ){
        aSubtotal[i].innerHTML = unitPrice * num;
      });
      Event.subscribe( 'plus-num' + i, function( unitPrice, num ){
        aSubtotal[i].innerHTML = unitPrice * num;
      });
    }
  })();
  console.log( Event.userList );
}

以上即为关于发布订阅模式及其在购物车重构中应用的完整分享,希望能为读者提供有益参考,并欢迎大家继续支持本站内容。

作者最新文章