【javaScript随笔】07 从vueBus到观察者

从vueBus到观察者

文 / 三和小钢炮


本文所要阐述的知识有以下

  • 面向对象编程
  • js模块化多规范兼容
  • vue的插件编写
  • js分层编程
  • 观察者模式
  • 生产者/消费者

前言

如果没有vueBus,那就自己写一个。

01 观察者模式

观察者模式(Observer):又被称作发布-订阅模式或者消息机制,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合。

一个观察者应该具有以下接口:

public interface IObserver {
    /**
     * 发布信息接口
     */
    public void fire();
    /**
     * 注册信息接口
     */
    public void regist();
    /**
     * 移除信息接口
     */
    public void remove();
}

02 实现一个观察者

function Event(type, args) {
  this.type = type || 'default';
  this.args = args || {};
}

function Observer() {
  var _msg = {};
  this.regist = function(type, fn) {
    if (typeof _msg[type] === 'undefined') {
      _msg[type] = [fn];
    } else {
      _msg[type].push(fn);
    };
  };
  this.fire = function(type, args) {
    if (typeof _msg[type] === 'undefined') {
      return;
    };
    for (var i = 0, len = _msg[type].length; i < len; i++) {
      _msg[type][i].call(this, new Event(type, args));
    };
  };
  this.remove = function(type, fn) {
    if (_msg[type] instanceof Array) {
      for (var i = _msg[type].length - 1; i >= 0; i--) {
        if (_msg[type][i] === fn) {
          _msg[type].splice(i, 1);
        };
      };
    };
  };
};

03 适配主流js规范

任何一个插件都要适应于主流规范
常用的规范有:CommonJS、CMD、AMD
我们做以下适配,保证它在任何环境下都可以正常运行

Observer.js文件如下:

/* eslint-disable */
;(function(global, factory) {
  if (typeof module !== 'undefined') {
    // nodeJs端
    module.exports = factory();
  } else if (typeof define === 'function') {
    if (define.amd) {
      // amd规范
      define(factory);
    } else if (define.cmd) {
      // cmd规范
      define(function(require, exports, module) {
        module.exports = factory();
      });
    }
  } else {
    global.Observer = factory();
  }
})(this, function () { 'use strict';

function Event(type, args) {
  this.type = type || 'default';
  this.args = args || {};
}

function Observer() {
  // 此消息应该为私有变量
  // 考虑到_msg不对外暴露,不采用prototype
  var _msg = {};
  this.regist = function(type, fn) {
    if (typeof _msg[type] === 'undefined') {
      _msg[type] = [fn];
    } else {
      _msg[type].push(fn);
    };
  };
  this.fire = function(type, args) {
    if (typeof _msg[type] === 'undefined') {
      return;
    };
    for (var i = 0, len = _msg[type].length; i < len; i++) {
      _msg[type][i].call(this, new Event(type, args));
    };
  };
  this.remove = function(type, fn) {
    if (_msg[type] instanceof Array) {
      for (var i = _msg[type].length - 1; i >= 0; i--) {
        if (_msg[type][i] === fn) {
          _msg[type].splice(i, 1);
        };
      };
    };
  };
};

return new Observer();

});

04 包装成vue插件

封装任何一个工具,最忌讳的就是和其他第三方耦合,大凡是依赖es6、jquery、vue等等,我们只能称之为组件或者插件。所以要把核心模块和其他适配模块抽离,核心模块不应该依赖任何东西。
这里,我们编写一个vue的包裹层,这里我们只需要保证它能够在vuejs里面适用。

ObserverServer.js文件:

/* eslint-disable */
/**
 * vue适配层
 */
import Observer from './Observer.js';
const ObserverServer = {};

ObserverServer.install = function (Vue, options) {
  Vue.Observer = Observer;
  Vue.prototype.$Observer = Observer;
}
export default ObserverServer;

05 在vue中使用

经过上面的操作,我们实际上是实现了一个vueBus,我们可以如下使用:

index.js文件:

import Observer from '~js/observer/ObserverServer.js';
Vue.use(Observer);

a.vue文件:

this.$Observer.regist('type1', function(msg) {
  console.log(msg);
});

this.$Observer.regist('type2', function(msg) {
  console.log(msg);
});

this.$Observer.fire('type1', '你好');
this.$Observer.fire('type2', 'hello');

06 问题

上面的观察者,看似完美,实际上存在一个非常严重的问题:
注册和发布的加载次序问题

在js中经常存在下面的情况:
注册往往会在发布之后

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="./Observer.js"></script>
</head>
<body>
  <button onclick="fn()">点击</button>
  <script>
    Observer.fire('type1', '你好');
    function fn() {
      Observer.regist('type1', function (msg) {
        console.log(msg);
      });
    };
  </script>
</body>
</html>

其实我们可以寻求下面的解决:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="./Observer.js"></script>
</head>
<body>
  <button onclick="fn()">点击</button>
  <script>
    // 采用两次发布,构成一次通信
    Observer.regist('type1.before', function(msg) {
      Observer.fire('type1', '你好');
    });

    var isRegist = false;
    function fn() {
      // 防止多次注册
      if (!isRegist) {
        Observer.regist('type1', function (msg) {
          console.log(msg);
          isRegist = true;
        });
      }
      Observer.fire('type1.before', 'ready');
    };
  </script>
</body>
</html>

07 生产者/消费者

我们上面的设计,是利用一个注册容器,将{type, fn} (消息类型和函数) 装入其中,一旦消息发布,便会回调触发函数。

实际上,上面的设计存在一个问题:
消息发布者和消息注册者都不应该关心消息被如何调用。

我们之前的结构为:
注册者 –> 注册表 <– 发布者

此时发布者需要处理和他不相干的消息,我们可以把消息发布者看作生产者,而注册者则是消费者,于是我们可以做如下调整。

注册者 –> 注册表 <==> 消息列表 <–发布者

注册者只关心到注册表里面注册,发布者只要关心将消息推送到消息列表里面。
至于注册表和消息列表之间的消费关系,用一个中间的消费机制去处理。
此时如果有消息发布未被消费,我们就采取将消息保留,直到被消费为止。

实现逻辑如下:

/* eslint-disable */
(function(global, factory) {
  if (typeof module !== 'undefined') {
    // nodeJs端
    module.exports = factory();
  } else if (typeof define === 'function') {
    if (define.amd) {
      // amd规范
      define(factory);
    } else if (define.cmd) {
      // cmd规范
      define(function(require, exports, module) {
        module.exports = factory();
      });
    }
  } else {
    global.Observer = factory();
  }
})(this, function () { 'use strict';

// 消息实体类
function Msg(type, args) {
  this.type = type || 'default';
  this.args = args || {};
  this.time = new Date().getTime();
}

function Observer() {
  var _registTable = {}; // 注册表
  var _msgQueue = []; // 消息队列

  //消费机制
  function consume() {
    for (var i = 0; i < _msgQueue.length; i++) {
      var msg = _msgQueue[i];
      var registList = _registTable[msg.type];
      if (registList instanceof Array) {
        for (var j = 0; j < registList.length; j++) {
          registList[j].call(this, msg);
        }
        _msgQueue.splice(i, 1);
        i--;
      }
    }
  }

  // 注册
  this.regist = function(type, fn) {
    if (typeof _registTable[type] === 'undefined') {
      _registTable[type] = [fn];
    } else {
      _registTable[type].push(fn);
    };
    consume();
  };

  // 发布
  this.fire = function(type, args) {
    _msgQueue.push(new Msg(type, args));
    consume();
  };

  // 注销
  this.remove = function(type, fn) {
    if (_registTable[type] instanceof Array) {
      for (var i = _registTable[type].length - 1; i >= 0; i--) {
        if (_registTable[type][i] === fn) {
          _registTable[type].splice(i, 1);
        };
      };
    };
  };
};

return new Observer();

});

这样我们前后依赖的问题就解决了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <script src="./Observer.js"></script>
</head>
<body>
  <button onclick="fn()">点击</button>
  <script>
    Observer.fire('type1', '你好');
    function fn() {
      Observer.regist('type1', function (msg) {
        console.log(msg);
      });
    };
  </script>
</body>
</html>

08 消费记录

上面的实现的设计想法是:保证消息要全部被消费
这里又会有一个问题:

<script src="./Observer.js"></script>
<script>
  Observer.fire('type1', '你好');
  Observer.regist('type1', function (msg) {
    console.log('No1', msg);
  });
  Observer.regist('type1', function (msg) {
    console.log('No2', msg);
  });
</script>

此时只会有一个注册者能收到消息。因为消息一旦被消费,就会被删除~。

那么我们需要保留每个消息的消费记录,每次消费消息的时候,先去查询是否有消费记录。
代码如下:


/* eslint-disable */
(function(global, factory) {
  if (typeof module !== 'undefined') {
    // nodeJs端
    module.exports = factory();
  } else if (typeof define === 'function') {
    if (define.amd) {
      // amd规范
      define(factory);
    } else if (define.cmd) {
      // cmd规范
      define(function(require, exports, module) {
        module.exports = factory();
      });
    }
  } else {
    global.Observer = factory();
  }
})(this, function () { 'use strict';

// 消息实体类
function Msg(type, args) {
  this.type = type || 'default';
  this.args = args || {};
  this.timeId = new Date().getTime() + '_' + Msg.ID++;
}
// 消息id
Msg.ID = 0;

function Observer() {
  var _registTable = {}; // 注册表
  var _msgQueue = []; // 消息队列
  var _consumeHistory = {}; // 消费历史

  // 增加消费记录
  function addConsumeHistory(fnID, msgID) {
    if (typeof _consumeHistory[msgID] === 'undefined') {
      _consumeHistory[msgID] = [fnID];
    } else {
      _consumeHistory[msgID].push(fnID);
    };
  };

  // 查询消费记录
  function getConsumeHistory(fnID, msgID) {
    var isConsume = false;
    if (typeof _consumeHistory[msgID] === 'undefined') return isConsume;
    var historyList = _consumeHistory[msgID];
    if (historyList.indexOf(fnID) > -1) {
      isConsume = true;
    }
    return isConsume;
  };

  //消费机制
  function consume() {
    for (var i = 0; i < _msgQueue.length; i++) {
      var msg = _msgQueue[i];
      var registList = _registTable[msg.type];
      if (registList instanceof Array) {
        for (var j = 0; j < registList.length; j++) {
          if (!getConsumeHistory(registList[j].registID, msg.timeId)) {
            registList[j].call(this, msg);
            addConsumeHistory(registList[j].registID, msg.timeId);
          }
        }
      }
    }
  }

  function regist(type, fn) {
    if (typeof _registTable[type] === 'undefined') {
      _registTable[type] = [fn];
    } else {
      _registTable[type].push(fn);
    };
    fn.registID = regist.ID++;
  }
  // 注册id
  regist.ID = 0;

  // 注册
  this.regist = function(type, fn) {
    regist(type, fn);
    consume();
  };

  // 发布消息
  this.fire = function(type, args) {
    _msgQueue.push(new Msg(type, args));
    consume();
  };

  // 注销
  this.remove = function(type, fn) {
    if (_registTable[type] instanceof Array) {
      for (var i = _registTable[type].length - 1; i >= 0; i--) {
        if (_registTable[type][i] === fn) {
          _registTable[type].splice(i, 1);
        };
      };
    };
  };
};

return new Observer();

});

09 消息过期

运行上面的代码

<script src="./Observer.js"></script>
<script>
  Observer.fire('type1', '你好');
  Observer.regist('type1', function (msg) {
    console.log('No1', msg);
  });
  Observer.regist('type1', function (msg) {
    console.log('No2', msg);
  });
</script>

上面就能收到两条消息。

那么问题来了:
虽然页面的注册数有限,但是一直长期保存历史记录,必然损耗性能。
我们需要设置一个消息的过期时间。

代码如下:

/* eslint-disable */
(function(global, factory) {
  if (typeof module !== 'undefined') {
    // nodeJs端
    module.exports = factory();
  } else if (typeof define === 'function') {
    if (define.amd) {
      // amd规范
      define(factory);
    } else if (define.cmd) {
      // cmd规范
      define(function(require, exports, module) {
        module.exports = factory();
      });
    }
  } else {
    global.Observer = factory();
  }
})(this, function () { 'use strict';

// 消息实体类
function Msg(type, args) {
  this.type = type || 'default';
  this.args = args || {};
  this.timeId = new Date().getTime() + '_' + Msg.ID++;
  this.getTime = function() {
    return this.timeId.split('_')[0];
  }
}
Msg.ID = 0;

function Observer() {
  var _registTable = {}; // 注册表
  var _msgQueue = []; // 消息队列
  var _consumeHistory = {}; // 消费历史

  // 增加消费记录
  function addConsumeHistory(fnID, msgID) {
    if (typeof _consumeHistory[msgID] === 'undefined') {
      _consumeHistory[msgID] = [fnID];
    } else {
      _consumeHistory[msgID].push(fnID);
    };
  };

  // 查询消费记录
  function getConsumeHistory(fnID, msgID) {
    var isConsume = false;
    if (typeof _consumeHistory[msgID] === 'undefined') return isConsume;
    var historyList = _consumeHistory[msgID];
    if (historyList.indexOf(fnID) > -1) {
      isConsume = true;
    }
    return isConsume;
  };

  // 清理过期消息 过期时间为五分钟
  function removeMsg() {
    var now = new Date().getTime();
    for (var i = 0, len = _msgQueue.length; i < len; i++) {
      if (now - _msgQueue[i].getTime() > 1000 * 60 * 5) {
        _msgQueue.splice(i, 1);
      }
    }
  }

  //消费机制
  function consume() {
    removeMsg();
    for (var i = 0; i < _msgQueue.length; i++) {
      var msg = _msgQueue[i];
      var registList = _registTable[msg.type];
      if (registList instanceof Array) {
        for (var j = 0; j < registList.length; j++) {
          if (!getConsumeHistory(registList[j].registID, msg.timeId)) {
            registList[j].call(this, msg);
            addConsumeHistory(registList[j].registID, msg.timeId);
          }
        }
      }
    }
  }

  function regist(type, fn) {
    if (typeof _registTable[type] === 'undefined') {
      _registTable[type] = [fn];
    } else {
      _registTable[type].push(fn);
    };
    fn.registID = regist.ID++;
  }
  regist.ID = 0;

  // 注册表
  this.regist = function(type, fn) {
    regist(type, fn);
    consume();
  };

  // 发布消息
  this.fire = function(type, args) {
    _msgQueue.push(new Msg(type, args));
    consume();
  };

  // 注销
  this.remove = function(type, fn) {
    if (_registTable[type] instanceof Array) {
      for (var i = _registTable[type].length - 1; i >= 0; i--) {
        if (_registTable[type][i] === fn) {
          _registTable[type].splice(i, 1);
        };
      };
    };
  };
};

return new Observer();

});

10 看似完美

到现在为止,看似完美的解决了一切问题。实际上引入了很多概念。也增加了性能的负担,但js本身存在的环境并不会有很多消息存在。这些是足够用了。

要完全解这些问题,我们需要一个统一的东西来兼容这一切问题。
我个人觉得已经超过了观察者这个范围了。等我下次详细讲述。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三和小钢炮

谢谢客官~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值