【设计模式】观察者模式 和 发布订阅模式

目录:
观察者模式
发布-订阅模式
观察者模式和发布订阅模式的区别
实现vue数据双向绑定

(1)观察者模式

(1)概念
  • 对程序中的某个对象的状态进行观察,并且在其发生改变的时候得到通知
  • 存在(观察者)和(目标)两种角色
(2)subject对象 和 observer对象
  • 目标对象 : subject
  • 观察者对象:observer
  • 在目标对象中存放观察者对象的引用
  • 一个目标对象对应多个观察者对象(一对多)
(3)在观察者模式中,目标对象与观察者对象,相互独立又相互联系
  • 两者都是相互独立的对象个体
  • 观察者对象在目标对象中订阅事件,目标对象广播发布事件
  • pattern:是模式的意思 (design pattern设计模式)
  • notify:通知
  • Subject:拥有添加,删除,通知(发布)等一系列观察者对象Observer的方法
  • Observer:拥有更新方法等
(4)ES5 代码实现过程
观察者模式 - ES5代码实现过程

1. 新建两个对象
- 目标对象:------ subject
- 观察者对象:---- observer
2. 在(目标对象)中存放(观察者对象)的引用
function Subject() { // 目标对象
  this.observers = new ObserverList()
}
function ObserverList() {
  this.observerList = []
}
function Observer() {} // 观察者对象       -- 函数都是Function构造函数的实例,即函数也是一个对象


3. 对于目标对象中的引用,我们必须可以动态的控制:
Subject:拥有添加,删除,通知(发布)等一系列观察者对象Observer的方法
Observer:拥有更新方法等
- add(obj) 添加订阅者对象
- count() 订阅者对象总数
- get(index) 获取某个订阅者对象
- indexOf(obj, startIndex) 某个订阅者对象的位置
- removeAt(index) 删除某个订阅者对象
- addObserver
- removeObserver
4. 目标对象广播发布消息
- 注意:
  - 目标对象并不能指定观察者对象做出什么相应的变化,目标对象只有通知的作用
  - 我们将具体的观察者对象该作出的变化交给了观察者对象自己去处理
  - 即 观察者对象拥有自己的 update(context) 方法来作出改变
  - 同时该方法不应该写在原型链上,因为每一个实例化后的 Observer 对象所做的响应都是不同的,需要独立存储 update(context)方法
Subject.prototype.notify = function(context) { // 目标对象发布消息事件
  var observerCount = this.observers.count() 
  for (var i = 0; i < observerCount; i++) {
    this.observers.get(i).update(context)
   // 取出所有订阅了目标对象的观察者对象,并执行他们各自的收到消息后的更新方法
   // 注意每个观察者对象的update()方法都是定义在自身的实例上的,各个观察者之间互不影响
  }
}
function Observer() {
  this.update = function() {
    // ...
  };
}






--------------
5. 完整代码

function ObserverList() { -------------------- 目标对象的引用
  this.observerList = [];
}
ObserverList.prototype = {
  add: function(obj) { // 添加观察者,方法挂载在ObserverList的原型连链上
    return this.observerList.push(obj); // this指向的是调用add方法时所在的对象,这里是  ObserverList的实例在调用
  },
  count: function() { // 总数量
    return this.observerList.length;
  },
  get: function(index) { // 获取某个观察者
    if (index > -1 && index < this.observerList.length) {
      return this.observerList[index];
    }
  },
  indexOf: function(obj, startIndex) { // 获取某个观察者对象的下标
    // obj某个观察者对象
    // startIndex 开始搜索的起始位置
    var i = startIndex;
    while (i < this.observerList.length) {
      if (this.observerList[i] === obj) {
        return i;
      }
      i++;
    }
    return -1; // 找到返回下标,没找到返回-1
  },
  removeAt: function(index) { // 删除某个观察者对象
    this.observerList.splice(index, 1);
  }
}


function Subject() { -------------------------------- 目标对象
  this.observers = new ObserverList(); // 实例化
}
Subject.prototype = { // 目标对象上包含添加,删除,通知观察者对象的方法
  addObserver: function(observer) {
    this.observers.add(observer); // --------------------------------调用ObserverList原型上的add方法
  },
  removeObserver: function(observer) {
    this.observers.removeAt(this.observers.indexOf(observer, 0)); // 调用ObserverList原型上的removeAt方法
  },
  notify: function(context) { // ---------------- 目标对象发布消息
    var observerCount = this.observers.count();
    for (var i = 0; i < observerCount; i++) {
      this.observers.get(i).update(context);
    }
  },
}


function Observer(content) {  ------------------------- 观察者对象
  this.update = function() {
    console.log(content)
  }
}
const observer1 = new Observer('111')
const observer2 = new Observer('222') // 观察者对象中一般挂载收到通知后的 更新方法
const subject = new Subject()
subject.observers.add(observer1)
subject.observers.add(observer2)
subject.notify() // 目标对象上一般都挂载添加,删除,通知 观察者对象的方法
观察者模式

详细 https://juejin.im/post/5cc57704e51d456e5a072975

精简 https://juejin.im/post/5bb1bb616fb9a05d2b6dccfa

实现vue数据双向绑定 https://juejin.im/post/5bce9a35f265da0abd355715

(5)ES6 代码实现过程
// 观察者模式 es6
// 目标类
class Subject {
  constructor() {
    this.observers = []
  }
  // 添加
  add = (obj) => {
    this.observers.push(obj)
  }
  // 删除
  remove(index) {
    if (index > -1 && index < this.observers.length) {
      this.observers.splice(index, 1)
    }
  }
  // 通知(发布消息)
  notify() {
    for(let i = 0; i < this.observers.length; i++) {
      this.observers[i].update()
    }
  }
}
// 观察者类
class Observer {
  constructor(content) {
    this.update = () => {
      console.log(content)
    }
  }
}
const subject = new Subject()
const observer1 = new Observer('111')
const observer2 = new Observer('222')
subject.add(observer1)
subject.add(observer2)
subject.notify()
splice()
- splice()用于删除数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是删除的元素
- 返回值:删除的元素组成的数组
- 参数:
  - 第一个参数:删除的起始位置,默认从0开始
  - 第二个参数:被删除的元素个数
  - 如果有更多的参数:表示要插入数组的元素
- 该方法改变原数组
const arr = [1, 2, 3]
const res = arr.splice(1, 2, 222, 333) // 从下标为1的位置开始删除2个元素,并将222,333插入到被删除的位置之后
console.log(res) // [2,3] 返回值是被删除的元素组成的数组
console.log(arr) // [1, 222, 333]改变原数组
for of
for in

1. 数组
- for(let i of arr)  ----------- i表示值
- for(let i in arr)  ----------- i 表示下标
2. 对象
- 对象只能用for in循环,不能用for of循环
- 因为对象没有部署iterator接口,不用使用for of循环
- for (let i in obj) ----------- i表示下标

https://www.jianshu.com/p/3e3451708143









(2)发布订阅模式

(1)角色
  • 发布者:Publisher
  • 订阅者:Subscriber
  • 中介:Topic/Event Channel
  • 中介既要接收发布者所发布的消息事件,又要将消息派发给订阅者
  • 所以中介要根据不同的事件,储存相应的订阅者信息
  • 通过中介对象,完全解偶了发布者和订阅者
(2)实例
1. 创建中介对象 ------------------------------ pubsub

2. 给中介对象的每个订阅者对象一个标识 ---------- subUid
  - 每当有一个新的订阅者对象 订阅事件的时候,就给新的订阅者对象一个 subUid

3. topics对象的结构:
  - key:  对应不同的事件名称
  - value:是一个数组,每个成员是一个对象,存放订阅该事件的(订阅者对象)及发生事件之后作出的(响应)





4. 完整代码:
  // 发布-订阅模式
  var pubsub = {} // 中介
  (function(myObject) {
  var topics = {} // 存放(订阅了某事件)对应的 (订阅者对象数组)
  var subUid = -1 // 每个订阅者的唯一ID

  // 发布
  myObject.publish = function(topic, args) {
    if (!topics[topic]) {
      return false
    }
    var subscribes = topics[topic]
    var len = subscribes ? subscribes.length : 0
    for (let i = len; i >= 0; i--) {
      subscribes[i].func(args)
    }
    // 上面的for循环,也可以用for of 实现
    // for(let obj of topics[topic]) {
    //   obj.func.call(this, args)
    // }
    return this // 这里可以不return this,不需要链式调用
  }
 
 // 订阅
  myObject.subscribe = function(topic, func) {
  // topic:指订阅的事件名称
  // func: 订阅者对象收到消息通知后的响应事件
  if (!topics[topic]) {
    // 如果事件不存在,则新建该事件,值是数组,数组中存放订阅该事件的 订阅者对象相关信息
    // 相关信息包括
      // 1. token -----订阅者对象的唯一标识符ID
      // 2. func ------发生该事件(订阅了该事件的订阅者对象,收到发布的该事件的通知时,执行的响应函数)
    topics[topic] = []
  }
  topics[topic].push({
    token: (++subUid).toString(),
    func: func // 这里可以简写
  })
  // 返回订阅者对象的唯一标识,用于取消订阅时是根据token来删除该订阅者对象
  return token
  }

  // 取消订阅
  myObject.unsubscribe = function(token) {
   // 利用token删除该事件对应的 订阅者对象
    for (let j in topics) {
      if (topics[j]) {
        for(let i = 0; i < topics[j].length; i++) {
          if (topics[j][i].token === token) {
            topics[j].splice(i, 1)
            return token
          }
        }
      }
    }
    return this // 可以不return this
  }
})(pubsub)



--------------
1. 多个订阅者对象,订阅了同一个事件 go 事件
pubsub.subscribe('go', function(args) {
  console.log(args)
})
pubsub.subscribe('go', function(args) {
  console.log(args + 'oher subscriber')
})
pubsub.publish('go', 'home')



--------------
2. 取消订阅,发布的go事件,将不会触发 订阅者的响应函数
pubsub.subscribe('go', function(args) {
  console.log(args)
})
pubsub.unsubscribe('0')
pubsub.publish('go', 'home')
发布-订阅模式

(3)观察者模式,发布-订阅模式的区别和联系

(1)区别
  • 观察者模式:需要观察者自己定义事件发生时的响应函数
  • 发布-订阅模式:在(发布者对象),和(订阅者对象)之间,增加了(中介对象)
(2)联系
  • 二者都降低了代码的(耦合性)
  • 都具有消息传递的机制,以(数据为中心)的设计思想

(4)vue数据双向绑定

  • directive:指令
  • compile:编译
前置知识:

1. Element.children
- 返回一个类似数组的对象(HTMLCollection实例)
- 包括当前元素节点的所有子元素
- 如果当前元素没有子元素,则返回的对象包含0个成员

2. Node.childNodes
- 返回一个类似数组的对象(NodeList集合),成员包括当前节点的所有子节点
- NodeList是一个动态集合

3. Node.childNodes 和 Element.children 的区别
- Element.children只包含元素类型的子节点,不包含其他类型的子节点


-----
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="name"  id="text">
        <div v-text="name"></div>
    </div>
    <script>
        // const tex = document.getElementById('text')
        // tex.addEventListener('input', function() {
        //     console.log('input')
        // })
    </script>
    <script>
        class Watcher {
            constructor(name, el, vm, exp, attr) {
                this.name = name
                this.el = el
                this.vm = vm
                this.exp = exp
                this.attr = attr

                this._update()
            }
            _update() {
                this.el[this.attr] = this.vm.$data[this.exp]
            }
        }
        class MyVue {
            constructor(options) {
                this.option = options
                this.$el = document.querySelector(options.el)
                this.$data = options.data

                this._directive = {}
                this._observes(this.$data)
                this._compile(this.$el)
            }
            // 数据拦截
            // get set 获取和重写
            // 并发布通知
            _observes(data) {
               let val;
               for (let key in data) {
                   if ( data.hasOwnProperty(key) ) {
                       this._directive[key] = []
                   }
                   val = data[key]
                   // val 可能是对象,和数组
                   if (typeof val === 'object') {
                       this._observes(val)
                   }
                   let _dir = this._directive[key]
                   Object.defineProperty(this.$data, key, {
                       enumerable: true,
                       configurable: true,
                       get() {
                           return val
                       },
                       set(newValue) {
                           if (val !== newValue) {
                               val = newValue
                               _dir.forEach(item => item._update())
                           }
                       }
                   })
               }
            }
            _compile(el) {
                // 子元素
                let nodes = el.children
                for (let i in Array.from(nodes)) {
                    const node = nodes[i]
                    console.log(node, 'xxx')
                    if (node.children.length) {
                        this._compile(node)
                    }
                    if (node.hasAttribute('v-text')) {
                        const attrValue = node.getAttribute('v-text')
                        this._directive[attrValue].push(new Watcher('text', node, this, attrValue, 'innerHTML'))
                    }
                    if (node.hasAttribute('v-model') && ( node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
                        let _this = this

                        node.addEventListener('input', (function() {
                            let attrValue = node.getAttribute('v-model')
                            _this._directive[attrValue].push(new Watcher('input', node, _this, attrValue, 'value'))

                            return function() {
                                console.log(typeof attrValue)
                                _this.$data[attrValue] = node.value
                            }
                        })())
                    }
                }
            }
        }
        const ins = new MyVue({
            el: '#app',
            data: {
                name: 'wang'
            }
        })
    </script>
</body>
</html>

实现vue数据双向绑定 https://juejin.im/post/5bce9a35f265da0abd355715

https://segmentfault.com/a/1190000016789934

https://juejin.im/post/5c10edc36fb9a049e307f67a

推荐阅读更多精彩内容