前言
发布订阅模式
是 观察者模式
的改造,所以本文会先编写一个基础的 观察者模式
案例,在此基础上再编写一个 发布订阅模式
案例,然后结合两者的特点,分析一些前端开发过程中常见的实践案例,帮助大家学习理解这两者的基本概念和区别。
需要留意的是,两者都有共同性: 一对多的思想
订阅器 观察者
- 订阅器 也称为
主题(subject)、主题事件(topic event)
,本质上是一个对象,是用于保存观察者(订阅者)
的载体,同时拥有添加观察者、通知所有观察者
的方法,例如 公司团建 就是一个主题事件
,这个主题事件
包含 公司成员信息、地点、需要干的事等信息 - 观察者 也称为
订阅者
, 一般为对象(也可以是一个函数)
为了方便表述,本文以
订阅器、观察者
称呼这两者
观察者模式
我们设计一个观察者模式:以 click 为标识创建 订阅器 clickDep
,然后创建 2个 观察者 watch
订阅 click, 订阅器 clickDep
发布通知,观察者 watch
集体触发
//构造 订阅器 的类
class Dep{
constructor(id){
this.id = id; //订阅器 标识
this.subs = []
}
push(watch){
watch.bind_id = this.id; //给 观察者 标识 归属的订阅器
this.subs.push(watch)
}
notify(){
this.subs.forEach((watch)=>{
watch.update()
})
}
}
//构造 观察者 的类
class Watch{
constructor(update){
this.update = update
}
}
//创建一个订阅器实例 click dep
var clickDep = new Dep('click');
//创建观察者实例
var w1 = new Watch(()=>console.log('click watch 1'))
var w2 = new Watch(()=>console.log('click watch 2'))
//添加观察者
clickDep.push(w1)
clickDep.push(w2)
//发布消息
clickDep.notify()
发布订阅模式
创建一个 调度中心(事件中心)
: common
, 用来综合管理 订阅器
及 观察者
,同样把 添加观察者方法
,发布订阅器通知
也放在 common
进行管理
class Common{
constructor(id){
this.id = id
this.deps = {} //用于保存 订阅器
}
createDep(key){
this.deps[key] = []
}
createWatch(update){
return {
bind_id:null,
update
}
}
push(key,watch){
watch.bind_id = key
this.deps[key].push(watch)
}
notify(key){
this.deps[key].forEach((watch)=>{
watch.update()
})
}
}
//创建 调度中心
const common = new Common('my common')
//创建 订阅器
common.createDep('click')
//创建 观察者
var w1 = common.createWatch(()=>console.log('click watch 1'))
var w2 = common.createWatch(()=>console.log('click watch 2'))
//添加 观察者
common.push('click',w1)
common.push('click',w2)
//发布 通知
common.notify('click')
两种模式的区别
通过上述案例,我们可以发现,观察者模式的订阅器之间是各自独立的,通俗的讲:就是我们创建 clickDep , touchDep 两个订阅器实例,他们的存放位置是分散的,没有通过统一的对象进行管理
而 发布订阅模式,优化了这方面的问题,通过创建一个 common
对象,统一管理 订阅器
、以及它们映射的 观察者
,还有 添加观察者、发布订阅器通知
这些方法
应用案例分析
-
DOM2 事件绑定: 给
document
用addEventListener
绑定3个事件处理函数,点击页面,会集体触发这三个函数document.addEventListener('click',()=>{console.log('click handler 1')}) document.addEventListener('click',()=>{console.log('click handler 2')}) document.addEventListener('click',()=>{console.log('click handler 3')})
案例分析:
订阅器
: js 内部关于click事件处理函数收集器
观察者
: 三个事件处理函数
发布消息
: click 触发
-
Promise:实现一个简单的
Promise
class Promise2{ constructor(fn) { this.thenDep = []; // thenDep 订阅器 this.catchDep = []; fn( this.resolve.bind(this), this.reject.bind(this) ) } then(cb) { //thenDep 添加订阅者 cb this.thenDep.push(cb); return this; //返回自身进行链式调用 } catch(cb) { this.catchDep.push(cb); return this; } resolve(res) { // thenDep 发布消息 this.thenDep.forEach((cb) => { cb(res) }) } reject(res) { this.catchDep.forEach((cb) => { cb(res) }) } } new Promise2(function (resolve, reject) { console.log('handler') setTimeout(function () { resolve({ data: 'resolve message' }) }, 2000) }) .then((res) => { console.log('then 1', res) }) .then((res) => { console.log('then 2', res) }) .catch((error) => { console.log('catch 1', error) }) .catch((error) => { console.log('catch 2', error) })
案例分析:
-
订阅器
:Promise2
内部维护的[then/catch]Dep
-
观察者
: 使用then、catch
传入的回调函数
-
发布消息
:resolve、reject
触发
-
-
Vue模板更新订阅:在 vue 组件
data
中声明username
, 在页面三处地方(例如text、html、attr
)使用username
进行渲染,当点击<button>
触发username
被改变时,在<dl>
内的三次渲染位置都会得到更新<template> <dl> <dt>{{username}}</dt> <dd v-html="username" :data-username="username"></dd> <dd> <button @click="username = 'Jeck'">change username</button> </dd> </dl> </template> <script> export default { data() { return { username:'Tom' //vue内部根据该属性,创建一个订阅器:[username]Dep }; }, ... } </script>
案例分析:
-
订阅器
:vue内部创建[username]Dep
对象 -
观察者
:三次dom渲染 对应的更新函数[text/html/attr]Update
-
发布消息
:username
被修改
-
通过设计模式,理解 Vue
-
订阅器创建阶段: vue源码,通过对
data
每个属性进行绑定(Object.defineProperty
),同时创建每个属性的订阅器[key]Dep
,通过属性绑定时的set
函数,触发订阅器([key]Dep
)通知 -
观察者绑定阶段: 发生在
template
模板初始渲染时,vue会把模板更新函数
,以观察者的方式,绑定在属性的订阅器([key]dep
)中, -
订阅过程:
username
在<dt>{{username}}</dt>
使用一次,模板初始更新 dom(textUpdate
),同时把textUpdate
订阅在[username]Dep
-
发布过程:
username
被改变,[username]Dep
发布消息, 观察者textUpdate
被触发:把username 最新值
更新同步在 dom<dt>
模板更新函数: 基于原生js dom api进行封装的函数,例如参数传入
node 节点
及value 目标值
,执行以下操作
- 属性更新 el.setAttribute
- 文本更新 el.textContent
- html 更新 el.innerHTML
- input 更新 input.value
- class 更新 el.classList[add\remove]
- style 更新 el.style[name]
内容按个人学习经历、笔记总结,可能有一些知识点理解有出入,希望大家多多指教,提出建议。谢谢~
第二次发文,有收获能给一个赞嘛,谢谢~