发布/订阅模式
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。处理一对多的场景:如ajax的网站登录、微信公众号的自动回复
发布订阅模式的具体示例
假如懒羊羊有一天要结婚了,曾经他的小伙伴们和懒羊羊说过,你结婚的时候一定要通知我,我一定到,我一定会随礼等等。如今他结婚了,需要发布这个消息,通知那些曾经订阅过他要结婚消息的小伙伴们,就可以使用发布订阅模式。
// 调度中心
class Obsever{
event = {} // 存储事件的容器
// {"marry":[xiMarry,feiMarry],"baby":[xiBaby,huiBaby]} 这是容器存储内容的格式,将来发布marry消息,就执行订阅了marry的函数xiMarry和feiMarry;发布baby消息,就执行订阅了baby的函数xiBaby,huiBaby
// 订阅
subscribe(type,fn){ // 订阅的消息类型 与 回调函数
if(typeof this.event[type] === "undefined"){ // 如果该事件类型等于undefined,说明是初次订阅,则在里面创建一个该事件类型
this.event[type] = [fn] // "baby":[xiBaby]
}else{ // 否则就是该事件类型已经创建了,后面如果还有人订阅相同的事件,直接push进去
this.event[type].push(fn) // "baby":[xiBaby,huiBaby]
}
}
// 发布
publish(type,args = {}){ // 发布的消息类型 与 一个对象
if(!this.event[type]) return // 如果该事件没有被人订阅,就直接退出
// 否则就是该事件被人订阅了
// {"marry":[xiMarry,feiMarry],"baby":[xiBaby,huiBaby]} 这是容器存储内容的格式
// 假如type为marry,则this.event[marry]就是上面对象中的"marry"属性,则其对应的值就是一个数组[xiMarry,feiMarry],length为2
for(let index = 0; index < this.event[type].length; index++){
this.event[type][index].call(this,args) // 事件类型 所对应存储的 回调函数 执行
// this.event[type]属性所对应的属性值是一个数组,数组取值用[index],所以this.event[type][index]就是对应的回调函数xiMarry等,这里用call把回调函数的this指向生成的实例对象obsever
// 数组中回调函数xiMarry,feiMarry等,是来自订阅subscribe中接收的回调函数,所以args参数也会传入到这些回调函数中被e接收
}
}
}
let obsever = new Obsever() // 生成调度中心的实例,将来发布/订阅都需要通过该调度中心才能实现
// 创建一些函数作为回调函数
let xiMarry = function (e){ //回调函数用e接收传入的参数args对象
console.log("喜羊羊!懒羊羊我要结婚了," + e.message)
}
let feiMarry = function (e){
console.log("沸羊羊!懒羊羊我要结婚了," + e.message)
}
let xiBaby = function (e){
console.log("喜羊羊!懒羊羊我的孩子满月了," + e.message)
}
let huiBaby = function (e){
console.log("灰太狼!懒羊羊我的孩子满月了," + e.message)
}
// 下面是订阅
// 喜羊羊订阅了结婚"marry"
obsever.subscribe("marry", xiMarry)
// 沸羊羊订阅了结婚"marry"
obsever.subscribe("marry", feiMarry)
// 喜羊羊订阅了孩子满月"baby"
obsever.subscribe("baby", xiBaby)
// 灰太狼订阅了孩子满月"baby"
obsever.subscribe("baby", huiBaby)
// 下面是发布
// 发布结婚"marry"的消息
obsever.publish("marry",{message:"一定要来哦"})
// 发布孩子满月"baby"的消息
obsever.publish("baby",{message:"来参加孩子的满月酒,记得随礼"})
复制代码
观察者模式
目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。
// 目标 主题类
class Subject{
constructor() {
this.watcher = [] // 保存观察者容器
}
addWatcher(watcher){ // 添加观察者
this.watcher.push(watcher) // 把观察者添加到保存观察者的容器中
}
notify(){ // 通知
this.watcher.forEach((item) => { // item表示观察者的容器中的每一个观察者实例
item.update() // 观察者实例调用更新方法
})
}
}
// 观察者类
class Watcher{
constructor(name) {
this.name = name
}
update(){ // 更新
console.log(this.name + "我更新了")
}
}
let subject = new Subject() // 目标主题生成实例对象
subject.addWatcher(new Watcher("张三")) // 实例对象调用添加观察者方法,把我们生成的观察者实例对象传入进去
subject.addWatcher(new Watcher("李四")) // 实例对象调用添加观察者方法,把我们生成的观察者实例对象传入进去
subject.addWatcher(new Watcher("王五")) // 实例对象调用添加观察者方法,把我们生成的观察者实例对象传入进去
subject.notify() // 实例对象调用通知方法
复制代码
观察者模式实现数据双向绑定MVVM
下面是html
部分的代码:
<div id = "app">
<h1>数据响应式</h1>
<div>
<div v-text = "myText"></div>
<div v-text = "myBox"></div>
<input type = "text" v-model = "myText">
<input type = "text" v-model = "myBox">
</div>
</div>
复制代码
下面是js
部分的代码:
// 1.首先根据上图实现整体的一个架构(包括MVVM类或者VUE类、Watcher类),这里运用到了观察者模式
// 2.然后实现MVVM中的由M到V,把模型里面的数据绑定到视图
// 3.最后实现V-M,当文本框输入文本的时候,由文本事件触发更新模型中的数据
// vue 主题
class Vue{
constructor(options) {
this.$data = options.data // 获取里面的data值
this.$el = document.querySelector(options.el) // 获取元素对象
this._watcher = {} // 加下划线表示内部私有,容器,用于存放观察者对象
this.Obsever(this.$data) // 当new完实例对象之后就执行Obsever方法,传入数据
this.Compile(this.$el) // 当new完实例对象之后就执行Compile方法,传入元素对象
}
// 劫持数据,并将解析后的数据存放进对应的容器保存
Obsever(data){ // 接收数据
// 容器{}要变成这种格式{myText:[],myBox:[]}来存放对应的观察者
for(let key in data){ // 遍历data,给里面的每一个key下标对应的元素开一个数组容器[]来保存观察者对象
this._watcher[key] = [] // v-text对应保存v-text的数组容器,v-model对应保存v-model的数组容器
let val = data[key] // 获取属性值
let watch = this._watcher[key] // 获取观察者集合,是一组数组
// 使用Object.defineProperty方法为遍历到的每一个属性添加get set拦截,即为某一个对象(this.$data)的某一个属性(key)设置拦截(get和set)
// 为啥把Object.defineProperty写在for in 循环中,就是因为Object.defineProperty每次只能为一个属性设置,写在循环内疚免去了多次设置
Object.defineProperty(this.$data,key,{
get:function (){ // get进行依赖收集
return val
},
set:function (newVal){ // set用于派发更新
if(val !== newVal){ // 如果原来的值与新设置的值不一样
val = newVal // 重新设置值
notify(watch) // 传入观察者集合,执行notify函数通知视图更新
}
}
})
}
// 数据改变了发送通知
let notify = function (watch){ // 接收观察者集合
watch.forEach(item => {
item.update() // 通知在该集合中的每一个观察者都进行更新操作
})
}
}
// 解析指令
Compile(el){ // 接收指令
// 怎么解析指令,就是通过搜索html里面对应的标签名,全部找到它们进行操作
let nodes = el.children // el是app,它的子类就是h1和div
for(let i = 0; i < nodes.length; i++){
let node = nodes[i] // 当i等于0时,就是h1标签,当i等于1时,就是div
// 但是有个问题,循环只循环了app里面的第一层,只找到了第一层子类h1和div,它的子类div里面还有一层,里面有v-text,v-model没有被找到
// 所以要在循环里使用递归来查找,判断是否还存在子节点
if(node.children.length){ // 如果子类node有长度,说明子类node里面还有子类,需要递归查找
this.Compile(node) // 那就把子类node传入Compile函数中递归查找
}
// 判断是否存在"v-text"属性
if(node.hasAttribute("v-text")){ // hasAttribute方法是查找我们自己定义的属性名
let attrVal = node.getAttribute("v-text") // 用变量attrVal保存查找到的v-text
// 添加观察者
this._watcher[attrVal].push(new Watcher(node,this,"innerHTML",attrVal)) // 把查找到的v-text观察者添加进存放观察者的对应数组容器[]
// new Watcher中接收的node为元素对象(div、input),this表示vue的实例,要是看不懂new Watcher中为何这样传值,可以看下面class Watcher中的注释
}
// 判断是否存在"v-model"属性
if(node.hasAttribute("v-model")){ // hasAttribute方法是查找我们自己定义的属性名
let attrVal = node.getAttribute("v-model") // 用变量attrVal保存查找到的v-model
// 添加观察者
this._watcher[attrVal].push(new Watcher(node,this,"value",attrVal)) // 同理,把查找到的v-model观察者添加进存放观察者的对应数组容器[]
// 监听input输入事件,当有输入的时候就更新this.$data[attrVal]里面的值
node.addEventListener("input",(e)=>{
this.$data[attrVal] = e.target.value // 把模型数据更新,意味着接下来就要触发set
})
}
}
}
}
// 观察者
class Watcher{ // new实例化Watcher观察者后就会自动执行下面这些内部代码,执行this.update()就会需要用到接收的el,vue,exp,attrVal这些值
constructor(el,vue,exp,attrVal) {
this.el = el
this.vue = vue
this.exp = exp
this.attrVal = attrVal
this.update() // 初始化时执行更新视图
}
// constructor中这样接收值是因为后面调用update函数需要用到这些值:如首先需要知道是什么el元素对象(div),该元素的什么exp属性(innerHTML)需要更新视图,还需要知道vue实例中的data数据,并且需要知道更新后取的值(attrVal)
update(){ // 更新视图
// obj.innerHTML = vue.data.myBox
// obj.value = vue.data.myBox
// 上面两行代码都可以用下面这行代码表示
this.el[this.exp] = this.vue.$data[this.attrVal]
}
}
// 生成vue的实例对象并传入参数
const app = new Vue({
el:"#app",
data:{
myText:"大吉大利",
myBox:"今晚吃鸡",
},
})
复制代码
下面是响应式数据更新前即初始化的情况:
下面是响应式数据更新后的情况:
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END