订阅者-发布模式/观察者模式-vue(旧)响应式原理

定义一个普通对象:houseObj 房子对象

定义一个消息列表,房子的信息,list,待会儿会从消息列表中筛选出合适的信息发布。不用数组,是因为数组会把key当作索引处理,再去取里面的内容的话,会进行数组的深拷贝,增加性能负担,而使用对象就可以省去深拷贝

 listen,增加订阅者,(对房子的要求,key是对房型的规定,fn往下看是一个打印订阅者信息的方法而已)

trigger,是发布消息

最终是通过key来判断每个订阅者的需求,并进行正确的消息发布

    let houseObj = {} // 发布者
    houseObj.list = {} // 缓存列表,不用数组会把key当作索引处理,再去取的话,会进行数组的深拷贝,增加性能负担,而使用对象就可以省去深拷贝

    // 增加订阅者
    houseObj.listen = function (key, fn) {
      // 判断有无
      (this.list[key] || (this.list[key] = [])).push(fn)
    }

    // 发布消息
    houseObj.trigger = function () {
      // 取出消息名称,类数组转换数组
      let key = Array.prototype.shift.call(arguments)
      let fns = this.list[key]
      if (!fns || fns.length === 0) {
        return
      }
      for (let i = 0, fn; fn = fns[i++];) {
        fn.apply(this, arguments) // arguments是形参列表,是一个内置对象,可以拿到这个函数的所有形参
      }
    }

    // 需求(订阅的操作),上面是载体,消息放在缓存列表里面
    houseObj.listen('small', function (size) {
      console.log('小红要买' + size + '平米房子')
    })
    houseObj.listen('big', function (size) {
      console.log('小明要买' + size + '平米房子')
    })

    // 正式发布
    houseObj.trigger('small', 100)
    houseObj.trigger('big', 150)

对上述代码进行封装(解耦)

用一个对象event来存:消息列表、增加订阅者、发布的功能

定义一个初始化的方法init,传进一个对象,把event中所有内容都遍历地复制给传进来的对象参数,这样子,新创建的对象就拥有了消息列表、订阅和发布的功能

下面对houseObj这个房子对象的需求和消息存储都可以在正式发布消息的时候一一对应起来。

    let event = {
      list: {},
      listen: function (key, fn) {
        (this.list[key] || (this.list[key] = [])).push(fn)
      },
      trigger: function () {
        let key = Array.prototype.shift.call(arguments)
        let fns = this.list[key]
        if (!fns || fns.length === 0) {
          return
        }
        for (let i = 0, fn; fn = fns[i++];) {
          fn.apply(this, arguments)
        }
      }
    }



    let init = function (obj) {
      for (let i in event) {
        obj[i] = event[i]
      }
    }


    let houseObj ={}
    init(houseObj)




    // 需求(订阅的操作),上面是载体,消息放在缓存列表里面
    houseObj.listen('small', function (size) {
      console.log('小红要买' + size + '平米房子')
    })
    houseObj.listen('big', function (size) {
      console.log('小明要买' + size + '平米房子')
    })

    // 正式发布
    houseObj.trigger('small', 100)
    houseObj.trigger('big', 150)

这样就可以让新建立的对象具有订阅和发布的功能了

增加删除方法

    event.remove = function (key, fn) {
      // fns保存的是某个key值下的所有信息
      let fns = this.list[key]
      // 判断该key值下是否有信息
      if (!fns) {
        return false
      }
      // 判断删除方法中是否传入特定的参数
      // 如果没有,就将该key值下的所有信息清空
      if (!fn) {
        fns && (fns.length = 0)
      }
      // 如果有传入特定的参数,就让其一一对比,找到对应的参数,删除该项信息 
      else {
        for (let i = fns.length - 1; i >= 0; i--) {
          let _fn = fns[i]
          _fn === fn && (fns.splice(i, 1))
        }
      }
    }

    
    // 订阅信息
    houseObj.listen('big', fn1 = function (size) {
      console.log('小明要买'+ size +'平米房子')
    })
    houseObj.listen('big', fn2 = function (size) {
      console.log('小明要买'+ size +'平米房子')
      })
    houseObj.listen('small', fn3 = function (size) {
      console.log('小明要买'+ size +'平米房子')
    })

    // 删除
    // 删除方法中传入的key值是big,后面的特定参数是fn2,所以会执行event.remove中的else中的语句
    houseObj.remove('big',fn2)
    
    // 发布
    // key值为big的fn1参数下的信息不受影响,size=120可以传入订方法阅中,同理small也不受影响
    houseObj.trigger('big',120)
    houseObj.trigger('small',120)

详细说明由代码注释可见。

由于上述代码在初始化的过程中,要注明对象的作用,并给每个新对象都创建一个list和各种方法功能,但是不是每一个对象都需要,所以要再次细化,按需定义方法。

再次解耦,让发布-订阅者模式适用于各种情况

    // 封装一个全局的对象,抛出几个方法让用户选择
    let Event = (function (){
      let list = {},
        listen,
        trigger,
        remove;

        listen = function (key, fn) {
          (list[key] || (list[key] = [])).push(fn)
        };

      trigger = function () {
        let key = Array.prototype.shift.call(arguments),
          fns = list[key]
 
        if (!fns || fns.length === 0) {
          return
        }
        for (let i = 0, fn; fn = fns[i++];) {
          fn.apply(this, arguments)
        }
      };

      remove = function (key, fn) {
        let fns = list[key]
        if (!fns) {
          return false
        }
        if (!fn) {
          fns && (fns.length = 0)
        } else {
          for (let i = fns.length - 1; i >= 0; i--) {
            let _fn = fns[i]
            _fn === fn && (fns.splice(i, 1))
          }
        }
      };

      return {
        // 可以简写
        listen: listen,
        trigger,
        remove
      }
    })();

定义一个全局的对象,里面是自执行的函数,将缓存列表list和各种方法都分别声明,最终抛出。让用户来自行选择需要的功能方法。提高了性能。

使用:

    // 使用方式
    // 订阅需求
    Event.listen('big', fn1 = function (size) {
      console.log("小明要买" + size + "平米的房子")
    })
    Event.listen('small', fn2 = function (size) {
      console.log("小明要买" + size + "平米的房子")
    })
    
    // 删除和发布
    Event.remove('big',fn1)  // 删除key为big,特殊参数为fn1的记录
    Event.trigger('big', 100)  // 发布big消息
    Event.trigger('big', 120)  // 发布big消息
    Event.trigger('small', 90)  // 发布small消息

Vue中使用Object.defineProperty()来实现响应式

创建订阅器模型:

//订阅器模型
var Dep = {
  clientList: {},
  listen: function (key, fn) {
    (this.clientList[key] || (this.clientList[key] = [])).push(fn);
  },
  trigger: function () {
    let key = Array.prototype.shift.call(arguments),
      fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      return;
    }
    for (let i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments); //发布消息附带的参数
    }
  }
};

创建劫持方法、绑定观察者:

// 劫持方法
let dataHijack = function ({
  data,
  tag,
  datakey,
  selector
}) {
  let value = '',
  el = document.querySelector(selector)
  Object.defineProperty(data, datakey, {
    get: function () {
      console.log("我获取到值了")
      return value;
    },
    set: function (newValue) {
      console.log("我改变值了")
      value = newValue
      Dep.trigger(tag, newValue)
    }
  });
  // 绑定观察者
  Dep.listen(tag, function (text) {
    el.innerHTML = text
  })
}

在html文件中使用:

<body>
  <div id="app">
    订阅视图-1:<span class="box1"></span>
    订阅视图-2:<span class="box2"></span>
  </div>
  <script src="./obs.js"></script>

  <script>
    // 数据
    var dataObj = {};
    // 数据劫持
    dataHijack({
      data: dataObj,
      tag: 'view-1',
      datakey: 'one',
      selector: '.box1'
    });

    dataHijack({
    data: dataObj,
    tag: 'view-2',
    datakey: 'two',
    selector: '.box2'
    });

    dataObj.one = '这是第一个值'
    dataObj.two = "这是第二个值"
  </script>
</body>

使用dataHijack的方法先劫持数据,通过selector来获取el,再将数据通过Object.defineProperty()的get()或者set()赋值。

猜你喜欢

转载自blog.csdn.net/michaelxuzhi___/article/details/106201584