前端常见设计模式之发布订阅模式

2,556 阅读6分钟

前端常见设计模式之发布订阅模式

基础

观察者模式是对象的行为模式,又叫发布订阅者模式,模型视图模式,监听者模式或者Dependents模式。
发布-订阅 脏检测 数据劫持 数据模型
publish-subscribe object.observe object.defineProperty  object.proxy
对象存在两种熟悉描述符:

+ 数据描述符  数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。
+ 存取描述符 存取描述符是由getter-setter函数对描述的属性。


描述符必须是这两种形式之一;不能同时是两者。

数据描述符 和 存取描述 都具有

+ configure 为true时,该属性描述符才能改变,默认为false
+ enumerable 为true时,该属性能够出现在对象的枚举属性中,默认为false

数据描述符:

+ value 可以是任何有效的 javascript,默认为undefined
+ writable 当且仅当为 true时, value才能被赋值运算符改变,默认为false

存取描述符:

+ get 给一个属性提供getter方法,如果没有 getter 则为 undefined。方法执行时没有参数传入,但是会传入this对象 默认为undefined
+ set 给一个属性提供setter方法,当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

object.observe()

object.observe API可以被称为一种“可以对任何对象的属性值修改进行监视的事件处理函数”。 可以观察的改变有 6 种变化.

  1. add
  2. update
  3. delete
  4. reconfigure
  5. setPrototype
  6. preventExtensions
var obj = {
    a: 1,
    b: 2
};
Object.observe(
    obj,
    function(changes) {
        for (var change of changes) {
            console.log(change);
        }
    },
);
obj.c = 3; {
    name: "c",
    object: obj,
    type: "add"
}
obj.a = 42; {
    name: "a",
    object: obj,
    type: "update",
    oldValue: 1
}
delete obj.b; {
    name: "b",
    object: obj,
    type: "delete",
    oldValue: 2
}

我们也可以自建监听事件

var obj3 = {
    _time: new Date(0)
};
var notifier = Object.getNotifier(obj3); //获取Notifier对象

Object.defineProperties(obj3, { //设置对象的可访问属性
    _time: {
        enumerable: false,
        configrable: false
    },
    seen: {
        set: function(val) {
            var notifier = Object.getNotifier(this);
            notifier.notify({
                type: 'time_updated', //定义time_updated事件
                name: 'seen',
                oldValue: this._time
            });
            this._time = val;
        },
        get: function() {
            return this._time;
        }
    }
});
Object.observe(obj3, function output(changes) {
    changes.forEach(function(change, i) {
        console.log(change, i)
    });
}); //为对象指定监视时调用的回调函数
obj3.seen = new Date(2013, 0, 1, 0, 0, 0); //触发time_updated事件
obj3.seen = new Date(2013, 0, 2, 0, 0, 0); //触发time_updated事件
obj3.seen = new Date(2013, 0, 3, 0, 0, 0); //触发time_updated事件
obj3.seen = new Date(2013, 0, 4, 0, 0, 0); //触发time_updated事件

代理是可以在动作发生之前拦截的。对象观察支持在变化(或一组变化)发生后响应。

object.defineProperty

object.defineProperty(obj, prop, descriptor)

obj: 定义属性的对象

prop: 定义或修改的属性的名称

descriptor:将被定义或修改的属性描述符

默认情况下,object.defineProperty()添加的属性值是不可修改的。

function Observer() {
    var result = null;
    Object.defineProperty(this, 'result', {
        get: function() {
            console.log('result');
            return result;
        },
        set: function(value) {
            result = value;
            console.log('你设置了 result = ' + value)
        }
    })
}

var app = new Observer();

app.result; // result
app.result = 11;

// object.defineProperty是 ES6新出的东西,这也是为什么vue 不支持 IE8一下的原因了。

数据劫持的优点:

  1. 无需显示调用。 这也是 vue 和 react 的差别 。
  2. 可得知数据变化。 我们在 defineProperty 中,劫持了setter能够直接拿到传入修改的值,而不需要diff比较,优化了性能。
//html
    <p>请输入:</p>
    <input type="text" id="input">
    <p id="p"></p>
//js
window.onload=function(){
  const obj = {};
  Object.defineProperty(obj, 'text',{
    configurable:true,
    enumerable: true,
    get(){
      console.log('获取值 +')
    },
    set(newVal){
      console.log('newVal: ', newVal);
      const  p = document.getElementById('p');
      const text = document.getElementById('input')
      text.value = newVal;
      p.innerHTML = newVal;
    }
  })

  const input = document.getElementById('input')
  input.addEventListener('keydown',function(e){
    obj.text = e.target.value
    console.log('keye: ', e.target.value);
  })
}

数据劫持的缺点:

  1. 一个属性我们不肯能提前定义,也不可能一个属性一个属性的写上去.
  2. 代码耦合度很高。

object.proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。 下面我们来看一个例子:

const me = {
    name: "小明",
    like: "小红 ",
    food: "香菇",
    musicPlaying: true
};

const meWithProxy = new Proxy(me, {
    get(target, prop) {
        if (prop === "like") {
            return "学习";
        }
        return target[prop];
    },
    set(target, prop, value) {
        if (prop === "musicPlaying" && value !== true) {
            console.log('value: ', target[prop], value);
            throw Error("音乐不停,生命不息!");
        }
    }
});


console.log("proxy", meWithProxy.food);
console.log("proxy", meWithProxy.like); //target为like时,代理为学习
console.log("proxy", meWithProxy.musicPlaying = false); //修改为false, 出现错误。

对比 object.defineProperty 和 proxy 的 优缺点:

  1. Object.defineProperty的第一个缺陷,无法监听数组变化。
  2. Proxy可以直接监听对象而非属性
//监听对象而非属性
  const obj ={};
  const  p = document.getElementById('p');
  const text = document.getElementById('input')
  const newObj = new Proxy(obj, {
    get(target, prop){
      return Reflect.get(target, prop, value);
    },
    set(target, prop, value){
      if(prop === 'text'){
        text.value = value;
        p.innerHTML = value;
      }
      return Reflect.set(target, prop, value );
    }
  })


  const input = document.getElementById('input')
  input.addEventListener('keyup',function(e){
    newObj.text = e.target.value
    // console.log('keye: ', e.target.value);
  })
<ul id="ul">
</ul>
<button id='btn'> +1 </button>
//监听数组的变化

// 渲染列表
const Render = {
  // 初始化
  init: function (arr) {
  const fragement = document.createDocumentFragment();

  arr.forEach(a => {
    var li = document.createElement('li');
    li.textContent = a;
      fragement.appendChild(li);
    })
    ul.appendChild(fragement);
  },
  change(value){
    var li = document.createElement('li');
    li.textContent = value;
    const fragement = document.createDocumentFragment();
    fragement.appendChild(li);
    ul.appendChild(fragement);
  }
};
// 初始化
window.onload = function () {
  const btn = document.getElementById('btn');
  const ul = document.getElementById('ul'); 

// 初始数组
const arr = [1, 2, 3, 4];
  Render.init(arr);

// 监听数组
const newArr = new Proxy(arr, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    if (key !== 'length') {
      Render.change(value);
    }
    return Reflect.set(target, key, value, receiver);
  },
});

  // push数字
  btn.addEventListener('click', function () {
    newArr.push(newArr.length+1)
  });
}

发布-订阅模式

简单版


function Pub(){
  this.handlers = {}
}

Pub.prototype = {
  //注册  绑定名称 和 回调
  on(name, cb){
    console.log('name: ', name);
    var self  = this;
    if( !(name in self.handlers)){
      self.handlers[name] = [];
    }
    self.handlers[name].push(cb)
    return this
  },
  //激活
  emit(name){
    var self = this;
    var handlerArg = Array.prototype.slice.call(arguments, 1);

    for(let i in self.handlers[name]){
      self.handlers[name][i].apply(self, handlerArg);
    }
    return self;
  },
    remove(name, cb){
     //移除事件
     console.log('name, cb: ', name, cb);

     let fns = this.handlers[name]
     if(!fns || !cb){
          return false
     }
     for(let l = fns.length -1; l >=0; l--){
          let _fn = fns[l];
          console.log('_fn: ', _fn);
          console.log('name, cb: ',  cb == _fn );

          if(_fn()  == cb()){
               console.log('1')
               fns.splice(l,1)
          };
     }
  }
}

var pub = new Pub();
pub.on('some', function(data){
  console.log('data: ', data);
  console.log('触发some', data+1)
})
pub.on('some1', function(data){
  console.log('data1: ', data);
  console.log('触发some1', data+2)
})

pub.emit('some', 2);
pub.emit('some1', 2);

pub.remove('some', function(data){
     console.log('data: ', data);
     console.log('触发some', data+1)
})
pub.emit('some', 4);

复杂版

//event.js

class Event {
    /** on 方法把订阅者所想要订阅的事件及相应的回调函数记录在 Event 对象的 _cbs 属性中*/
    on(event, fn) {
        if (typeof fn != "function") {
            console.error('fn must be a function')
            return
        }
        this._cbs = this._cbs || {};
        (this._cbs[event] = this._cbs[event] || []).push(fn)
    }
    /**emit 方法接受一个事件名称参数,在 Event 对象的 _cbs 属性中取出对应的数组,并逐个执行里面的回调函数 */
    emit(event) {
        this._cbs = this._cbs || {}
        var callbacks = this._cbs[event],
            args
        if (callbacks) {
            callbacks = callbacks.slice(0)
            args = [].slice.call(arguments, 1)
            for (var i = 0, len = callbacks.length; i < len; i++) {
                callbacks[i].apply(null, args)
            }
        }
    }
    /** off 方法接受事件名称和当初注册的回调函数作参数,在 Event 对象的 _cbs 属性中删除对应的回调函数。*/
    off(event, fn) {
        this._cbs = this._cbs || {}
        // all
        if (!arguments.length) {
            this._cbs = {}
            return
        }
        var callbacks = this._cbs[event]
        if (!callbacks) return
        // remove all handlers
        if (arguments.length === 1) {
            delete this._cbs[event]
            return
        }
        // remove specific handler
        var cb
        for (var i = 0, len = callbacks.length; i < len; i++) {
            cb = callbacks[i]
            if (cb === fn || cb.fn === fn) {
                callbacks.splice(i, 1)
                break
            }
        }
        return
    }
}

const myEvent = new Event();
export default myEvent;

必须导出 实例对象 类似 dio

原生 与 jquery

//原生
 var btn = document.getElementById("btn");
  // 创建事件
 var evt = document.createEvent('Event');
 // 定义事件类型
 evt.initEvent('event',true,true);
 // 监听事件
 console.log('test: ', test);
 btn('event',function(){
      console.log("hello world");
 },false);
 // 触发事件
 btn.dispatchEvent(evt);
 
 //自定义事件无非就是监听事件,然后自己运行回调函数,上面的initevent
 //第二个参数是是否冒泡
 //第三个参数是否使用preventDefault()阻止默认行为
//jquery
    jQuery.subscribe(“done”,fun2);
    function fun1(){
         jQuery.publish(“done”);
    }

Nodejs

const EventEmitter = require('./node-v10.16.0/lib/events.js');

const observer = new EventEmitter();

observer.on('topic', function(){
  console.log('topic has occured')
})

observer.once('topic', function(msg){
  console.log('message: '+ msg)
})

function main(){
  console.log('start');
  observer.emit('topic')
  console.log('end')
}

main();

后记

欢迎关注微信公众号!,进行更好的交流。

微信