又称发布-订阅模式,指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
在 JavaScript 中,观察者模式最主要是应用在事件模型中:
事件模型可以分为两种,首先是原生的 Dom 事件,比如 click,这就是观察者模式的应用,相信大家一定使用过,在此就不再赘述。
其次是自定义事件,随着Vue、Angular等框架的流行,自定义事件也一定不会陌生,我们可以自己写一个自定义事件,来更好的理解观察者模式。
比如,有个购物车的功能,当点击增加或减少商品数量的按钮时,其相对应的数值和价格也会改变。
首先,我们创建一个观察者对象的基本模型:一个缓存列表,和三个方法,分别为订阅方法、发布方法以及取消方法。
const event = function () { // 缓存列表 const _list = {}; return { // 订阅方法 listen() {}, // 发布方法 dispatch() {}, // 取消方法 remove() {}, }; };
接下来,就是实现这三个方法。
订阅方法:
// 参数 type:自定义事件名称 // 参数 fn:与 type 相对应的处理方法 listen(type, fn) { // 如果缓存列表中没有该事件,则注册一个 if (Object.is(_list[type], undefined)) { _list[type] = []; } // 如果缓存列表中有该事件,则把处理方法保存到队列中 _list[type].push(fn); }
发布方法:
// 参数 type:自定义事件名称 // 参数 args:函数参数 dispatch(type, ...args) { // 如果缓存列表中没有该事件或者该事件没有可执行的方法,则退出 if (!_list[type] || _list[type].length === 0) { return false; } // 否则依次执行该事件的处理方法 for (let fn of _list[type]) { fn(...args); } }
取消方法:
// 参数 type:自定义事件名称 // 参数 fn:与 type 相对应的处理方法 remove(type, fn) { // 如果缓存列表中有该事件,并且该事件有可执行的方法,则进行以下操作 if (Array.isArray(_list[type]) && _list[type].length > 0) { // 如果没有指定某个处理方法,则全部清空 if (!fn) { _list[type] = []; } else { // 如果指定了取消某个处理方法,则相应删除 _list[type] = _list[type].filter((v) => { return v !== fn; }); } } }
最后,我们去实现购物车的功能:
例子:
<button id="start">开始编辑</button> <br /> <br /> <br /> <button id="inc" disabled>+</button> <span>台灯</span> <button id="dec" disabled>-</button> <p>数量:<strong id="amount">1</strong></p> <p>价格:<strong id="price">50</strong></p> <br /> <br /> <br /> <button id="stop" disabled>完成</button> <script> const $event = (function () { // 缓存列表 const _list = {}; return { // 订阅方法 listen(type, fn) { if (Object.is(_list[type], undefined)) { _list[type] = []; } _list[type].push(fn); }, // 发布方法 dispatch(type, ...args) { if (!_list[type] || _list[type].length === 0) { return false; } for (let fn of _list[type]) { fn(...args); } }, // 取消方法 remove(type, fn) { if (Array.isArray(_list[type]) && _list[type].length > 0) { if (!fn) { _list[type] = []; } else { _list[type] = _list[type].filter((v) => { return v !== fn; }); } } }, // 观察缓存列表的数据变化 show() { console.log(_list); }, }; })(); const startBtn = document.querySelector('#start'); const stopBtn = document.querySelector('#stop'); const incBtn = document.querySelector('#inc'); const decBtn = document.querySelector('#dec'); const amount = document.querySelector('#amount'); const price = document.querySelector('#price'); let amountNum = 1; let priceNum = 50; const incFn = (n) => { amountNum++; priceNum = amountNum * 50; amount.innerHTML = amountNum; price.innerHTML = priceNum; console.log(n); }; const decFn = (n) => { if (amountNum === 1) { return false; } amountNum--; priceNum = priceNum - 50; amount.innerHTML = amountNum; price.innerHTML = priceNum; console.log(n); }; const changeBtnStates = (items = [startBtn, stopBtn, incBtn, decBtn]) => { items.forEach((item) => { typeof item.getAttribute('disabled') === 'string' ? item.removeAttribute('disabled') : item.setAttribute('disabled', 'disabled'); }); }; // 点击“开始编辑”按钮,订阅消息 startBtn.addEventListener('click', () => { // 订阅自定义事件 inc $event.listen('inc', incFn); // 订阅自定义事件 dec $event.listen('dec', decFn); // 其他代码,不是重点 $event.show(); changeBtnStates(); }); // 点击“+”按钮,发布(触发)自定义事件 inc incBtn.addEventListener('click', () => { $event.dispatch('inc', '+1'); }); // 点击“-”按钮,发布(触发)自定义事件 dec decBtn.addEventListener('click', () => { $event.dispatch('dec', '-1'); }); // 点击“完成”按钮,取消订阅 stopBtn.addEventListener('click', () => { // 取消自定义事件 inc $event.remove('inc', incFn); // 取消自定义事件 dec $event.remove('dec', decFn); // 其他代码,不是重点 $event.show(); changeBtnStates(); }); </script>
看了上面的例子,很多人会觉得,本来简单的功能,使用观察者模式反而变得复杂,意义何在?
其实,观察者模式的意义可以从两个方面体现出来:
如有错误,欢迎指正,本人不胜感激。