前端EventEmitter,发布/订阅模式,Vue双向数据绑定

前端EventEmitter,发布/订阅模式

  • 前言
  • 实现
  • 拓展
    • Vue 中非父子组件组件通信
    • 通俗易懂了解Vue双向绑定原理及实现
    • Vue双向绑定原理,教你一步一步实现双向绑定
  • 参考的代码1
  • 参考代码2`推荐`
  • [Source Code - JavaScript - 学习优雅的编码](https://segmentfault.com/a/1190000015043262)
  • 参考

前言

发布订阅模式,很多地方都用到的一种模式,简单的说就是预定一件事情,时机成熟通知你,比如我们nodejs中的fs的读写文件的流,消息事件的触发等都用到了这种方式了,虽然不是特别难,但是非常实用,实现方式也简单,基本思想就是内部保存了一个对象存储订阅的函数,调用者通过名字来触发函数,订阅多个就按照队列的形式触发。

DOM 的事件机制就是发布订阅模式最常见的实现,这大概是前端最常用的编程模型了,监听某事件,当该事件发生时,监听该事件的监听函数被调用。

实现

class EventEmitter {
     
  constructor() {
     
    this.events = Object.create(null)
  }
  on(type, handler) {
     
    ;(this.events[type] || (this.events[type] = [])).push(handler)
  }
  off(type, handler) {
     
    if (this.events[type]) {
     
      this.events[type].splice(this.events[type].indexOf(handler) >>> 0, 1)
    }
  }
  emit(type) {
     
    let args = [].slice.call(arguments, 1)
    let array = this.events[type] || []
    array.forEach(cb => {
     
      cb.apply(this, args)
    })
  }
  once(type, handler) {
     
    function _fn() {
     
      handler.apply(this, arguments)
      this.off(type, _fn)
    }
    this.on(type, _fn)
  }
}
export default EventEmitter

使用

let em = new EventEmitter()

function fn(price) {
     
  console.log('price', price)
}

em.once('work', fn)
em.off('work', fn)
em.emit('work', 100)

console.log(em)

拓展

Vue 中非父子组件组件通信

在 Vue 中不同组件之间通讯,有一种解决方案叫Event Bus,这其实就是发布订阅模式的实现,非常简单好用。
前端EventEmitter,发布/订阅模式,Vue双向数据绑定_第1张图片

通俗易懂了解Vue双向绑定原理及实现

Vue双向绑定原理,教你一步一步实现双向绑定

在正式开始之前我们先来说说数据绑定的事情,数据绑定我的理解就是让数据M(model)展示到 视图V(view)上。我们常见的架构模式有 MVC、MVP、MVVM模式,目前前端框架基本上都是采用 MVVM 模式实现双向绑定,Vue 自然也不例外。但是各个框架实现双向绑定的方法略有所不同,目前大概有三种实现方式。

  • 发布订阅模式
  • Angular 的脏查机制
  • 数据劫持

而 Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty来实现。

所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据.

实现Vue的数据双向绑定,需要如下:

  • Observer 监听器:用来监听属性的变化通知订阅者
  • Watcher 订阅者:收到属性的变化,然后更新视图
  • Dep 订阅器: 负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。
  • Compile 解析器:解析指令,初始化模版,绑定订阅者

总结:

实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。
前端EventEmitter,发布/订阅模式,Vue双向数据绑定_第2张图片
前端EventEmitter,发布/订阅模式,Vue双向数据绑定_第3张图片

参考的代码1

这个有问题,没有传参数, 需要自行修改一下

// 发布订阅模式
class EventEmitter {
     
    constructor() {
     
        // 事件对象,存放订阅的名字和事件
        this.events = {
     };
    }
    // 订阅事件的方法
    on(eventName,callback) {
     
       if (!this.events[eventName]) {
     
           // 注意时数据,一个名字可以订阅多个事件函数
           this.events[eventName] = [callback]
       } else  {
     
          // 存在则push到指定数组的尾部保存
           this.events[eventName].push(callback)
       }
    }
    // 触发事件的方法
    emit(eventName) {
     
        // 遍历执行所有订阅的事件
       this.events[eventName] && this.events[eventName].forEach(cb => cb());
    }
    // 移除订阅事件
    removeListener(eventName, callback) {
     
        if (this.events[eventName]) {
     
            this.events[eventName] = this.events[eventName].filter(cb => cb != callback)
        }
    }
    // 只执行一次订阅的事件,然后移除
    once(eventName,callback) {
     
        // 绑定的时fn, 执行的时候会触发fn函数
        let fn = () => {
     
           callback(); // fn函数中调用原有的callback
           this.removeListener(eventName,fn); // 删除fn, 再次执行的时候之后执行一次
        }
        this.on(eventName,fn)
    }
}

使用方式

let em = new EventEmitter();
let workday = 0;
em.on("work", function() {
     
    workday++;
    console.log("work everyday");
});

em.once("love", function() {
     
    console.log("just love you");
});

function makeMoney() {
     
    console.log("make one million money");
}
em.on("money",makeMoney)let time = setInterval(() => {
     
    em.emit("work");
    em.removeListener("money",makeMoney);
    em.emit("money");
    em.emit("love");
    if (workday === 5) {
     
        console.log("have a rest")
        clearInterval(time);
    }
}, 1);

参考代码2推荐

推荐看这个
前端EventEmitter,发布/订阅模式,Vue双向数据绑定_第4张图片

Source Code - JavaScript - 学习优雅的编码

  // source code
  all = all || Object.create(null);

Object.create(null):生成的对象是一个原型为空的对象。节约内存且避免冲突,因为没有原型,且普通对象原型上的属性和方法也相应没有了。

  // source code
  (all[type] || (all[type] = [])).push(handler);
  
  // my code - bad
  if (all[type]) {
     
    all[type].push(handler)
  } else {
     
    all[type] = [handler]
  }

简洁的队列赋值:短路逻辑判断 + 初始化 + 更新数组,简直不要太优雅。

  // source code
  all[type].splice(all[type].indexOf(handler) >>> 0, 1);
  • 按位操作符:1 >>> 0 = 1, -1 >>> 0 = 4294967295, 详情MDN
  • 补充:按位操作符~,可以结合.indexOf()使用,因为对任一数值 x 进行按位非操作的结果为 -(x + 1),即:~-1 = 0
  // source code
  (all[type] || []).slice().map((handler) => {
      handler(evt); });
  (all['*'] || []).slice().map((handler) => {
      handler(type, evt); });
  // source code
  !!(0)             // false
  !!(null)          // false
  !!('')            // false
  !!(undefined)     // false
  !!(NaN)           // false
  
  !!(2)             // true
  • !!: 强制转换成 boolean 类型,相当于 !(!val)。如果 val = 0/null/""/undefined/NaN 时,!!(val) = false,如果 val 是其他值,!!(val) = true
  • +: +val将字符串数字转为数字。如果 val 是非字符串数字,则 +val = NaN
  // source code
  +'123456'        // 123456, Number
  +new Date()      // 1527684413484, 相当于 new Date().getTime()

参考

TypeScript/理解Event Emitter (事件派发器)推荐
Understanding Event Emitters
上面案例的github代码
一个例子 - 看尽并手写JS发布订阅模式
前端必懂EventEmitter,不懂会丢人

你可能感兴趣的:(前端,面试,#,JS/TS,js)