背景

最近在研究react的状态管理器zustand时,研究源码时发现其组件注册绑定是通过观察者模式结合react hooks实现更新的。而联想之前写vue的时候,经常会用到vue内置的自定义事件进行组件通信($emit/on),这个应该是发布订阅模式,搞得我有点头大,感觉这两种模式又十分相似,自己也是有点迷糊,感觉没有理解透,因此,这次就顺势深入研究下这两种模式,再尝试自己手写实现加深下理解。这篇文章是我个人的梳理心得,如有错误欢迎指正,共同进步~

对比

区别

观察者模式:在软件设计中是一个对象,维护一个依赖列表,当任何状态发生改变自动通知它们。

发布-订阅设计模式:消息的发送方(发布者)不会直接发送给特定的接收者(叫做订阅者),而是通过一个信息中介进行过滤和分配消息。

通俗形象点来说就是:

  • 察者模式没中间商赚差价,发布订阅模式 有中间商赚差价。
  • 观察者模式为一刀切模式,对所有订阅者一视同仁,发布订阅模式可以戴有色眼镜,有一层过滤或者说暗箱操作。

贴张图大家感受下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qqZ66s20-1631541451800)(https://user-gold-cdn.xitu.io/2017/11/22/15fe1b1f174cd376?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)]

总结一下

  • 观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。

  • 发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。

  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)。

  • 观察者模式需要在单个应用程序地址空间中实现,而发布-订阅更像交叉应用模式。

概念看上去似乎也挺清晰的,它们之间的差异点等也比较好理解。接下来我们就开始自己动手实现,深入其内部原理和运行逻辑。

发布订阅模式

vue自定义事件Event Bus就是发布订阅模式的实现,还有NodejsEmitter Event

实现一个支持订阅、解绑、发布、同类型事件支持多次绑定的发布订阅。

来个简单实现

上代码

// 订阅中心
const subscribers = {}
// 订阅
const subscribe = (type, fn) => {// 以数组模式添加队列,做到同一类型支持多个绑定if (!subscribers[type]) subscribers[type] = []subscribers[type].push(fn)
}
// 发布
const publish = (type, ...args) => {if (!subscribers[type] || !subscribers[type].length) returnsubscribers[type].forEach((fn) => fn(...args))
}
// 解绑订阅
const unsubscribe = (type, fn) => {if (!subscribers[type] || !subscribers[type].length) returnsubscribers[type] = subscribers[type].filter((n) => n !== fn)
}

验证测试

// console test ======>
subscribe("topic-1", () => console.log("suber-A 订阅了 topic-1"))
subscribe("topic-2", () => console.log("suber-B 订阅了 topic-2"))
subscribe("topic-1", () => console.log("suber-C 订阅了 topic-1"))publish("topic-1") // 通知订阅了 topic-1 的 A 和 C// 输出结果
// suber-A 订阅了 topic-1
// suber-C 订阅了 topic-1

实现一个Emitter类

上代码

class Emitter {constructor() {// 订阅中心this._event = this._event || {}}// 注册订阅addEventListener(type, fn) {const handler = this._event[type]if (!handler) {this._event[type] = [fn]} else {handler.push(fn)}}// 卸载订阅removeEventListener(type, fn) {const handler = this._event[type]if (handler && handler.length) {this._event[type] = handler.filter((n) => n !== fn)}}// 通知emit(type, ...args) {const handler = this._event[type]if (handler && handler.length) {handler.forEach((fn) => fn.apply(this, args))}}
}

验证测试

// console test ======>
const emitter = new Emitter()emitter.addEventListener("change", (obj) => console.log(`name is ${obj.name}`))emitter.addEventListener("change", (obj) => console.log(`age is ${obj.age}`))const sex = (obj) => console.log(`sex is ${obj.sex}`)emitter.addEventListener("change", sex)emitter.emit("change", { name: "xiaoming", age: 28, sex: "male" })console.log("event-A", emitter._event)emitter.removeEventListener("change", sex)console.log("====>>>>")emitter.emit("change", { name: "xiaoming", age: 28, sex: "male" })console.log("event-B", emitter._event)// 输出
// name is xiaoming
// age is 28
// sex is male
// event-A {change: Array(3)}// ====>>>>// name is xiaoming
// age is 28
// event-B {change: Array(2)}

vue Event Bus 实现

结构梳理

源码位置:src/core/instance/events.js

首先我们根据源码分析下结构,梳理一下vueevent实现逻辑

  1. 把事件中心 _events 挂载到 Vue 实例上:

    vm._events = {}

  2. 把所有的方法: $on$once$off$emit 挂载到Vue原型上

这样做的好处是可以在Vue组件中使用时直接 this.$onthis.$emit

// $on
Vue.prototype.$on = function(){}
// $once
Vue.prototype.$once = function(){}
// $once
Vue.prototype.$off = function(){}
// $once
Vue.prototype.$emit = function(){}

看代码

  1. $on 添加注册

    // $on
    Vue.prototype.$on = function (event, fn) {const vm = this// 如果传入的 event 监听事件类型为数组,递归调用 $on 方法if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$on(event[i], fn)}} else {// 如果存在直接添加,不存在新建后添加;(vm._events[event] || (vm._events[event] = [])).push(fn)}// 返回this,用于链式调用return vm
    }
    
  2. $once 单次执行

    // $once
    Vue.prototype.$once = function (event, fn) {const vm = this// 当该 event 事件触发时,调用 on 方法function on() {// 首先执行 $off 方法卸载 本回调方法vm.$off(event, on)// 再执行 本回调方法fn.apply(vm, arguments)}// 该赋值会在 $off 中使用:cb.fn === fn// 因为该 $once 方法调用的是 $on 添加回调,但是添加的是包装后的 on 方法而不是 fn 方法// 因此当我们单独调用 $off方法删除 fn 回调时,是找不到的,这时就可以通过 cb.fn === fn 判断on.fn = fn// 调用 $on 方法,把该回调添加到队列vm.$on(event, on)return vm
    }
    
  3. $off 卸载删除

    // $off
    Vue.prototype.$off = function (event, fn) {const vm = this// 如果不传入任何参数,清空所有的事件if (!arguments.length) {vm._events = Object.create(null)return vm}// 如果 event 为数组,同 $on 逻辑,递归卸载事件if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$off(event[i], fn)}return vm}// 回调列表const cbs = vm._events[event]// 如果该 event 事件不存在绑定回调,不处理if (!cbs) {return vm}// 如果未传入对应 event 的解绑回调,则清空该 event 的所有if (!fn) {vm._events[event] = nullreturn vm}// event 事件类型和 回调 都存在,遍历查找删除 指定 回调let cblet i = cbs.lengthwhile (i--) {cb = cbs[i]if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)break}}return vm
    }
    
  4. $emit 触发事件

    // $emit
    Vue.prototype.$emit = function (event) {const vm = this// 回调列表let cbs = vm._events[event]// 判断该 event 是否存在执行回调if (cbs) {// $emit方法可以传参,这些参数会在调用回调函数的时候传进去// 排除 event 参数的其他参数// toArray 是一个把类数组转换为数组的方法,并支持截取const args = toArray(arguments, 1)// 遍历回调函数for (let i = 0, l = cbs.length; i < l; i++) {cbs[i].apply(vm, args)}}return vm
    }
    

    toArray方法

    // Convert an Array-like object to a real Array.
    function toArray (list, start) {start = start || 0;var i = list.length - start;var ret = new Array(i);while (i--) {ret[i] = list[i + start];}return ret
    }
    

测试下

我们先模拟一个Vue类测试下

class Vue {constructor() {this._events = {}}// 提供一个对外获取 _events 的接口get event() {return this._events}
}

验证下结果

// 实例化
const myVue = new Vue()// 添加订阅
const update_user = (args) => console.log("user:", args)
const once_update_user = (args) => console.log("once_user:", args)myVue.$on("user", update_user)
myVue.$once("user", once_update_user) // 该订阅触发后自动卸载// 输出打印
console.log("events:", myVue.event)
// events: {user: [(args) => console.log("user:", args), ƒ on()]}// 触发通知
myVue.$emit("user", { name: "xiaoming", age: 18 })
console.log("events:", myVue.event)
// events: {user: [(args) => console.log("user:", args)]}
// user: {name: "xiaoming", age: 18}
// once_user: {name: "xiaoming", age: 18}// 卸载订阅
myVue.$off("user", once_update_user)
console.log("events:", myVue.event)
// events:{user: []}

小总结下

Vue 封装的这个发布订阅模式,可以说是很完善了,这个是完全可以独立抽取出来的在其他项目中使用的代码,再根据自身需求,调整下事件存储器的位置即可(Vue 放在了实例上)。

我们从最简单的几行代码,一直到框架中的细致完整实现,从中可以发现:其实只要我们思路对了,核心方法掌握理解了,很容易就可以弄明白其实现原理,而剩下的大多都是对各种异常情况的判断和处理。

观察者模式

只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

也来个简单实现

// 观察者列表
const observers = []// 添加
const addob = (ober) => {observers.push(ober)
}// 通知
const notify = (...args) => {observers.forEach((fn) => fn(args))
}// 测试 =======>
const subA = () => console.log("I am sub A")
const subB = (args) => console.log("I am sub B", args)addob(subA)
addob(subB)
notify({ name: "sss", site: "ssscode.com" })
// I am sub A
// I am sub B [{name: "sss", site: "ssscode.com"}]

实现一个观察者类

上代码

// 观察者
class Observer {constructor(name) {// 观察者 namethis.name = name}// 触发器update() {console.log("观察者:", this.name)}
}// 被观察者
class Subject {constructor() {// 观察者列表this._observers = []}// 获取 观察者列表get obsers() {return this._observers}// 添加add(obser) {this._observers.push(obser)}// 移除remove(obser) {this._observers = this._observers.filter((n) => n !== obser)}// 通知所有观察者notify() {this._observers.forEach((obser) => obser.update())}
}

验证测试下结果

// 观察者
const obserA = new Observer("obser-A")
const obserB = new Observer("obser-B")// 被观察者
const subject = new Subject()// 添加到 观察者列表
subject.add(obserA)
subject.add(obserB)// 通知
subject.notify()
console.log("观察者列表:", subject.obsers)
// 观察者: obser-A
// 观察者: obser-B
// 观察者列表: (2) [Observer, Observer]// 移除
subject.remove(obserA)// 通知
subject.notify()
console.log("观察者列表:", subject.obsers)
// 观察者: obser-B
// 观察者列表: [Observer]

Vue 双向数据绑定

vue的双向数据绑定就是观察者模式的实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWg3M622-1631541451802)(https://user-gold-cdn.xitu.io/2018/10/23/166a031209fc8da5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)]

利用 Object.defineProperty() 对数据进行劫持,设置一个监听器 Observer,用来监听所有属性,如果属性上发上变化了,就需要告诉订阅者 Watcher 去更新数据,最后指令解析器 Compile 解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了双向绑定~

vue2.x 核心是通过 Object.defineProperty() 这个方法对数据劫持,并重新定义 setget 方法,一旦数据变动,收到通知,更新视图。

Vue在初始化时会有个依赖收集处理,通过对属性和指令的遍历处理(这里的属性包括 propsdata 等,指令是通过 compile 编译进行过滤处理得到),得到需要进行响应式处理的属性,然后再通过 ObserverDepWatcher 实现监听、依赖收集、订阅。

有兴趣的朋友可以试着把源码下载下来,然后使用浏览器断点调试看一下 Vue 整个初始化的过程,对大家理解 Vue 的运行逻辑和过程还是很有帮助的。

这里我们简单看下 Vuedata 的处理。其他渲染过程暂不分析。

初始化 initData

// 这里的 $options 其实就是我们在写 Vue 的时候
// 其中的props、data、method、computed等属性。
function initData(vm) {// 对 data 进行处理,函数 / 对象let data = vm.$options.data// 为什么建议大家vue中的data使用函数式写法?// 当一个组件被定义, data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例// 如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象// 通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,// 从而返回初始数据的一个全新副本数据对象。// (js在赋值object对象时,是直接一个相同的内存地址。所以为了每个组件的data独立,采用了这种方式。)data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}// observe dataobserve(data, true /* asRootData */)
}

创建观察者 observe

function observe(value, asRootData) {let ob// Observerob = new Observer(value)// asRootData = trueif (asRootData && ob) {ob.vmCount++}//return ob
}

观察者类 Observer

class Observer {constructor(value) {this.value = valuethis.dep = new Dep()this.vmCount = 0if (Array.isArray(value)) {this.observeArray(value)} else {this.walk(value)}}// 处理所有属性,进行响应式处理walk(obj) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])}}// 数组 时遍历处理observeArray(items) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}
}

数据劫持,包装 set 方法,监听数据更新 defineReactive

由于 Object.defineProperty 不能够监听数组下标,所以这里 Vue 其实是把数组的原有方法进行了重写,比如pushpop,先执行原逻辑函数,如果是往数组新增元素,则把新增元素变成响应式。

function defineReactive(obj, key, val) {// 依赖收集const dep = new Dep()// 数据劫持,包装 set 方法添加 notify 通知Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter() {// 如果 watcher 存在,触发依赖收集if (Dep.target) {dep.depend()}return val},set: function reactiveSetter(newVal) {// ...// 数据变更 ==> 触发set方法 ==> 调用dep.notify()通知更新dep.notify()},})
}

依赖收集类 Dep

class Dep {constructor() {this.id = uid++this.subs = [] // 用于存放订阅者 Watcher}addSub(sub) {// sub ===> Watcher// 该方法会在 watcher 添加订阅时被执行this.subs.push(sub)}removeSub(sub) {// sub ===> Watcherremove(this.subs, sub)}// Dep.target===watcher 即 watcher.addDepdepend () {if (Dep.target) {Dep.target.addDep(this)}}notify() {// subsconst subs = this.subs.slice()// 调用 watcher 的 updatefor (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}
// 存放唯一 watcher
Dep.target = null
const targetStack = []function pushTarget (target) {targetStack.push(target)Dep.target = target
}function popTarget () {targetStack.pop()Dep.target = targetStack[targetStack.length - 1]
}

订阅者 watcher

// 删减了部分,只看核心代码
class Watcher {constructor(vm, expOrFn, cb, options, isRenderWatcher) {this.vm = vmvm._watchers.push(this)this.cb = cbthis.deps = []this.newDeps = []this.value = this.get()this.getter = expOrFn}// 获取最新 value, 收集依赖get() {pushTarget(this)let valueconst vm = this.vmvalue = this.getter.call(vm, vm)if (this.deep) {// 收集嵌套属性的每个依赖traverse(value)}popTarget()this.cleanupDeps()return value}// 添加依赖// dep === class DepaddDep(dep) {this.newDeps.push(dep)dep.addSub(this)}// 清除依赖cleanupDeps() {let i = this.deps.lengthwhile (i--) {const dep = this.deps[i]dep.removeSub(this)}this.deps = this.newDeps}// 提供更新的接口update() {this.run()}// 通知执行更新run() {const value = this.get()this.cb.call(this.vm, value, oldValue)}// 通过 watcher 收集所有依赖depend() {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}
}

通过上面代码我们可以发现,Vue 在初始化的时候对整个 data 对象中的每个属性都进行了添加订阅监听处理,而通过对 set 的改写使得我们在修改数据的时候可以触发通知,这样便可以使所有添加订阅了的属性进行更新,然后再结合 Vuecompiler 编译进行 render 即可完成视图层的更新。

到这一步已经可以做到对数据的通知更新,但是我们都知道 vue 是双向数据绑定的,在数据变更的同时会继续通知视图也进行更新。即在模板编译器 complie 的时候会对指令(v-bindv-modle等)进行过滤并添加 Watcher 订阅,实现 observe <===> watcher <===> complie 三者之间的绑定与通信 。

watcher 源码: https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/watcher.js

这里我就不继续深入了,感觉有点说不完了

发布订阅模式vs观察者模式相关推荐

  1. JavaScript设计模式之发布-订阅模式(观察者模式)-Part1

    <JavaScript设计模式与开发实践>读书笔记. 发布-订阅模式又叫观察者模式,它定义了对象之间的一种一对多的依赖关系.当一个对象的状态发生改变时,所有依赖它的对象都将得到通知. 例如 ...

  2. Labview Actorfromwork ESA(发布订阅模式,即观察者模式)Demo,整体操作过程已录制视频

    Labview Actorfromwork ESA(发布订阅模式,即观察者模式)Demo,整体操作过程已录制视频(视频时长2小时),整体程序涉及GOOP,event for ui actor indi ...

  3. 发布订阅模式与观察者模式

    背景 设计模式并非是软件开发的专业术语,实际上,"模式"最早诞生于建筑学. 设计模式的定义是:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案.通俗一点说,设计模式是在某 ...

  4. 【EventBus】发布-订阅模式 ( EventBus 组成模块 | 观察者模式 )

    文章目录 一.发布-订阅模式 二.EventBus 组成模块 三.观察者模式 一.发布-订阅模式 发布订阅模式 : 发布者 Publisher : 状态改变时 , 向 消息中心 发送事件 ; 订阅者 ...

  5. 观察者模式VS发布订阅模式区别

    观察者模式VS发布订阅模式区别 观察者模式:订阅者收集函数,发布者循环调用 发布订阅:收集发布单独给一个中介 对比 以结构来分辨模式,发布订阅模式相比观察者模式多了一个调度中心: 以意图来分辨模式,都 ...

  6. JS观察者模式和发布订阅模式

    观察者模式 观察者模式在前端工程中是很常见的设计模式,因为前端交互中充斥着大量多控件联动的交互,当参与联动的组件数量比较多或者组件数量可能变化的时候,代码就会变得难以维护.但是如果我们写代码时遵循了观 ...

  7. JavaScript 发布-订阅模式

    发布-订阅模式,看似陌生,其实不然.工作中经常会用到,例如 Node.js EventEmitter 中的 on 和 emit 方法:Vue 中的 $on 和 $emit 方法.他们都使用了发布-订阅 ...

  8. 从发布-订阅模式到Vue响应系统

    概念 发布-订阅模式又称为观察者模式,它定义的是一种一对多的依赖关系,当一个状态发生改变的时候,所有以来这个状态的对象都会得到通知. 生活中的发布-订阅模式 上面事发布-订阅模式的一个比较正式的解释, ...

  9. 设计模式 —— 发布订阅模式

    设计模式 -- 发布订阅模式 <工欲善其事,必先利其器> 我在之前有写过一篇关于 <观察者模式> 的文章,大家有兴趣的可以去看看,个人认为那个例子还是挺生动的.(狗头) 不过今 ...

最新文章

  1. php controller 间调用,php – 在CodeIgniter中的另一个Controller中调用Controller函数
  2. Android性能优化典范第四季
  3. Py之xlwt:python库之xlwt的简介、安装、使用方法之详细攻略
  4. 清华出版社送书 50 本,倒计时!
  5. 647. Palindromic Substrings 回文子串
  6. yelee主题博客四周变透明
  7. virsh 网络设置_KVM使用Network Filters进行虚拟机网络管理 | leon的博客
  8. mongodb存入mysql_存储到Mysql、mongoDB数据库
  9. Java过滤emoji表情,找出emoji的unicode范围。
  10. 带你认识PLC输入的源型与漏型接法
  11. 虚拟机VMware安装windows7 64位操作系统(图文版详解版)
  12. Webmagic爬虫框架
  13. 应用之星:H5制作又出一利器,分分钟刷爆朋友圈
  14. HTML5七夕情人节表白网页❤抖音超火的樱花雨3D相册❤ HTML+CSS+JavaScript
  15. POI获取文本单元格的数字变成科学计数法的处理方法
  16. 电脑摄像头未能创建连接服务器,电脑提示未能创建视频预览,请检查设备连接的原因及解决办法...
  17. gcc编译工具常用命令以及汇编语言
  18. 含文档+PPT+源码等]精品微信小程序家教信息管理系统+后台管理系统|前后分离VUE[包运行成功]微信小程序毕业设计项目源码计算机毕设
  19. java中定义坐标_Java 基础接口——坐标
  20. Java面对对象概念,什么是面向对象

热门文章

  1. 使用计算机VLOOKUP函数需注意什么,vlookup函数怎么用-vlookup函数使用方法介绍 - Iefans...
  2. linux查看操作系统版本的命令
  3. C++中的stack容器适配器
  4. OGG(ORACLE GOLDENGATE 12.3)安装与学习文档教程
  5. 遗忘曲线艾宾浩斯规律
  6. IDEA自带插件的实体生成详细教程,离线情况下如何导入MySQL的驱动
  7. unity中使用AO贴图和自发光emission的简单应用
  8. 全网 Vue 最XXXXXXX...... 男人看了沉默,女人看了流泪
  9. android webview aosp com.android.webview
  10. java经典题之冒泡排序