IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 观察者模式与发布/订阅模式 -> 正文阅读

[JavaScript知识库]观察者模式与发布/订阅模式

最近突然因为工作涉及到这一块的内容,网上搜罗了一些文章,有了一些自己的理解,遂记📝。


前言

  • 观察者模式

定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

  • 发布/订阅模式

早期只是 观察者模式的一种别称,后面慢慢的发展独立开来,成为一种不同的模式。

发布订阅模式描述了对象,典型的应用就是 eventEmmiter,主要实现了事件的调度,主要包含三个角色

  • 发布者:只负责发布事件,不关心会被谁什么时候调用。
  • 调度中心: 存取事件
  • 订阅者 :只负责订阅事件,不关心事件是什么时候被谁发布的。

做个类比方便理解:就像某些平台(消息代理或调度中心或中间件,如抖音)你(用户,订阅者)关注了某些主播或up,(发布者),他们只负责往平台发布他们的动态,而你(用户)就会收到他们的推送。

另外 MQTTjs 主要利用的就是发布订阅模式
推荐 vue中使用mqtt详细教程(兼容ie)?


实现

观察者模式

基本实现

对于对象 subject, 所有观察者(Observer)都对其都依赖的关系。对于 subject 的一切变化都会自动出发依赖它(subject)的观察者。

// 观察者
class Observer {
    id = 0
    constructor() {
        ++this.id;
    }
    update(val) {
        console.log(this.id + ' updated: ', val)
    }
}
// 观察者列表
class ObserverList {
    constructor() {
        this.observerList = []
    }
    add(observer) {
        return this.observerList.push(observer);
    }
    remove(observer) {
        this.observerList = this.observerList.filter(ob => ob !== observer);
    }
    count() {
        return this.observerList.length;
    }
    get(index) {
        return this.observerList[index];
    }
}
// 目标
class Subject {
    constructor() {
        this.observers = new ObserverList();
    }
    addObserver(observer) {
        this.observers.add(observer);
    }
    removeObserver(observer) {
        this.observers.remove(observer);
    }
    notify(...args) {
        let obCount = this.observers.count();
        for (let index = 0; index < obCount; index++) { 
            this.observers.get(index).update(...args); 
        } 
    } 
}
// 正常情况下 一般是目标 subject有变化时自动去触发 notify。
const sub = new Subject();
const obs1 = new Observer();
sub.addObserver(obs1);
sub.notify('假装有变化1')
// 1 updated:  假装有变化1
const obs2 = new Observer();
sub.addObserver(obs2);
sub.notify('假装有变化2') // 可以想想id为啥都为1
// 1 updated:  假装有变化2
// 1 updated:  假装有变化2

实际应用场景

  • Vue2.x 相关设计

vue中将 数据作为被观察者 (subject),利用 defineProperty / Proxy 拦截,一旦发生任何变化都通知到 他的依赖者(上文的 Observer, 下面代码中的 Dep) ,然后触发相应的更新。其中相关代码 猛戳这里? 如下。

id 自增是为了防止 添加 依赖(dep)时避免重复的检测标识

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid++;
  this.subs = [];
};

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
// 通知更新
Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if (!config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

发布订阅者模式

基本实现

主要实现 订阅取消订阅发布

  • 订阅: 为 某种类型事件触发时 添加一个操作(fn)
  • 取消订阅: 取消 某种类型事件触发时 的某个操作(fn)
  • 发布:触发某种类型事件。
class PubSub {
    constructor() {
        this.subscribers = {}
    }
    subscribe(type, fn) {
        if (!Object.prototype.hasOwnProperty.call(this.subscribers, type)) {
            this.subscribers[type] = [];
        }
        this.subscribers[type].push(fn);
    }
    unsubscribe(type, fn) {
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        this.subscribers[type] = listeners.filter(v => v !== fn);
    }
    publish(type, ...args) {
        let listeners = this.subscribers[type];
        if (!listeners || !listeners.length) return;
        listeners.forEach(fn => fn(...args));
    }
}

let ob = new PubSub();
ob.subscribe('add', (...args) => {
    const val = [...args].reduce((acc, cur) => acc += cur, 0)
   console.log('add: ', val) 
});
ob.publish('add', 1,3);// add: 4

前面说过,发布订阅模式的额典型的应用就是 eventEmmiter

eventEmmiter

对象实现

let event = {
   list: {},
   on(key, fn) {
       if (!this.list[key]) {
           this.list[key] = [];
       }
       this.list[key].push(fn);
   },
   // 监听一次
   once (key, fn) {
       // 先绑定,调用后删除
       let _this = this;
       _this.on(key, () => {
       		fn();
			_this.off(key, fn)
	   });
   },
   emit(...args) {
       let key = [].shift.call([...args]),
           events = this.list[key];

       if (!events || fns.length === 0) {
           return false;
       }
       events.forEach(evt => {
           evt.apply(this, args);
       });
   },
   off(key, evt) {
       let events = this.list[key];
       if (!events) return false;
       if (!evt) {
           events && (events.length = 0);
       } else {
           events.forEach((cb, i) => {
               if (cb === evt) {
                   events.splice(i, 1);
               }
           });
       }
   }
 };

Class实现

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(event, fn) {
        if (this.cache[event]) {
            this.cache[event].push(fn)
        } else {
            this.cache[event] = [fn]
        }
    }
    // 是否一次触发写在一起
    emit(event, once = false, ...args) {
        if (this.cache[event]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[event].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[event]
            }
        }
    }
    off(key, fn) {
        let events= this.cache[key]
        if(!events) return false;
        if(!fn) {
		   events.length = 0;	
		}else{
            events.forEach((cb, i) => {
               if (cb === evt) {
                   events.splice(i, 1);
               }
           });
        }
    }
}

Vue2.x中的实现

用过vue的同学知道,vue 有一个 简单的通信模式 是利用 $emit$on 实现,其实就是基于发布订阅者模式,以下为相关代码。

删除了部分注释与边界代码,想看更多的话 猛戳这里?

function eventsMixin (Vue) {
  var hookRE = /^hook:/;
  Vue.prototype.$on = function (event, fn) {
    var vm = this;
    // 当多个事件执行同一个fn时
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
    }
    return vm
  };
  //添加只执行一次的事件
  Vue.prototype.$once = function (event, fn) {
    var vm = this;
    function on () {
      vm.$off(event, on);
      fn.apply(vm, arguments);
    }
    on.fn = fn;
    vm.$on(event, on);
    return vm
  };
  // 取消订阅
  Vue.prototype.$off = function (event, fn) {
    var vm = this;
    // all
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
        vm.$off(event[i$1], fn);
      }
      return vm
    }
    // specific event
    var cbs = vm._events[event];
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null;
      return vm
    }
    var cb;
    var i = cbs.length;
    // 遍历,找到相同删除。
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break
      }
    }
    return vm
  };
  // 触发
  Vue.prototype.$emit = function (event) {
    var vm = this;
    var cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      // 获取传入多余的参数
      var args = toArray(arguments, 1);
      var info = "event handler for \"" + event + "\"";
      for (var i = 0, l = cbs.length; i < l; i++) {
      	// 执行函数,并能处理错误 (https://github.com/vuejs/vue/blob/dev/dist/vue.js#L1863)
        invokeWithErrorHandling(cbs[i], vm, args, vm, info);
      }
    }
    return vm
  };

轻量级库 mitt 的实现

这是 vue 官方推荐的库 tiny-emitter? ,下面为其实现。

另外还有个纯 TypeScript 实现的库 mitt?

非常简单精悍👍👍👍

function E () {
  // Keep this empty so it's easier to inherit from
  // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}

E.prototype = {
  on: function (name, callback, ctx) {
    var e = this.e || (this.e = {});

    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });

    return this;
  },

  once: function (name, callback, ctx) {
    var self = this;
    function listener () {
      self.off(name, listener);
      callback.apply(ctx, arguments);
    };

    listener._ = callback
    return this.on(name, listener, ctx);
  },

  emit: function (name) {
    var data = [].slice.call(arguments, 1);
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    var i = 0;
    var len = evtArr.length;
    for (i; i < len; i++) {
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }
    return this;
  },

  off: function (name, callback) {
    var e = this.e || (this.e = {});
    var evts = e[name];
    var liveEvents = [];

    if (evts && callback) {
      for (var i = 0, len = evts.length; i < len; i++) {
        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i]);
      }
    }

    (liveEvents.length)
      ? e[name] = liveEvents
      : delete e[name];

    return this;
  }
};

发布订阅模式与观察者模式的关系

其实这两种模式不过是实现上的差异,发布订阅模式多了个事件池。

其实说不同也不同(间接通知与直接通知),说相同也相同(都是发布更新,获取更新的过程)。个人更愿意将它们理解为同一种模式的不同实现。

如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级
--------发布订阅模式与观察者模式?


参考

站在别人肩膀上能看的更远,谢谢💖💖💖以下文章作者~~~
🔗JS兵法36 计,你会多少?

🔗JavaScript 发布-订阅模式

🔗发布订阅模式,在工作中它的能量超乎你的想象

🔗JavaScript 发布-订阅模式

🔗观察者模式和发布订阅模式的区别

🔗谈谈JS的观察者模式(自定义事件)

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-29 11:32:24  更:2021-07-29 11:32:44 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/8 21:03:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码