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

目录:
观察者模式
发布-订阅模式
观察者模式和发布订阅模式的区别
实现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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容