15-设计模式-发布订阅模式和观察者模式(实现数据双向绑定MVVM)

1,048 阅读6分钟

发布/订阅模式

image.png

订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。处理一对多的场景:如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:"来参加孩子的满月酒,记得随礼"})

观察者模式

image.png

目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。

// 目标 主题类
    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

image.png

下面是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:"今晚吃鸡",
        	},
    	})

下面是响应式数据更新前即初始化的情况:

响应式数据更新前.png

下面是响应式数据更新后的情况:

响应式数据更新后.png