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

基础

观察者模式是对象的行为模式,又叫发布订阅者模式,模型视图模式,监听者模式或者 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,
    configure: 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 我们在 data 中将数据定义好之后,能够直接修改值,而不用像 react 在进行 setstate ,在接着发布-订阅模式,修改模型层,模型层能够跟着同步的原因。

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);
});
// 渲染列表
const Render = {
  // 初始化
  init: function (arr) {
    const fragment = document.createDocumentFragment();

    arr.forEach((a) => {
      var li = document.createElement("li");
      li.textContent = a;
      fragment.appendChild(li);
    });
    ul.appendChild(fragment);
  },
  change(value) {
    var li = document.createElement("li");
    li.textContent = value;
    const fragment = document.createDocumentFragment();
    fragment.appendChild(li);
    ul.appendChild(fragment);
  },
};
// 初始化
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);

复杂版

class EventEmitter {
  constructor() {
    // 初始化事件存储对象
    this._events = {};
  }

  // 订阅事件
  on(event, listener) {
    if (typeof listener !== "function") {
      console.error("Listener must be a function");
      return;
    }

    // 如果该事件还没有订阅过,初始化为数组
    if (!this._events[event]) {
      this._events[event] = [];
    }

    // 将回调函数添加到事件对应的订阅列表中
    this._events[event].push(listener);
  }

  // 触发事件
  emit(event, ...args) {
    // 获取该事件的回调函数数组
    const listeners = this._events[event];

    // 如果有回调函数,依次执行它们
    if (listeners && listeners.length > 0) {
      listeners.forEach((listener) => {
        listener(...args); // 使用扩展运算符传递所有的参数
      });
    }
  }

  // 取消订阅某个事件的某个回调函数
  off(event, listener) {
    const listeners = this._events[event];

    if (!listeners) return;

    // 如果提供了回调函数,移除它
    if (listener) {
      this._events[event] = listeners.filter((fn) => fn !== listener);
    } else {
      // 如果没有提供回调函数,移除该事件的所有回调
      this._events[event] = [];
    }
  }

  // 订阅事件一次,触发一次后自动取消订阅
  once(event, listener) {
    const wrapper = (...args) => {
      listener(...args); // 执行回调
      this.off(event, wrapper); // 执行后取消订阅
    };
    this.on(event, wrapper); // 用包装函数订阅事件
  }
}

// 测试代码
const eventEmitter = new EventEmitter();

// 订阅事件
eventEmitter.on("greet", (name) => {
  console.log(`Hello, ${name}!`);
});

// 触发事件
eventEmitter.emit("greet", "Alice");
eventEmitter.emit("greet", "Bob");

// 取消订阅
const greetListener = (name) => {
  console.log(`Goodbye, ${name}!`);
};
eventEmitter.on("greet", greetListener);
eventEmitter.emit("greet", "Charlie");
eventEmitter.off("greet", greetListener);
eventEmitter.emit("greet", "Dave"); // 此时 'Goodbye, Charlie!' 不会被打印出来

// 订阅一次性事件
eventEmitter.once("onceEvent", () => {
  console.log("This will run only once.");
});
eventEmitter.emit("onceEvent");
eventEmitter.emit("onceEvent"); // 第二次不会触发

原生 与 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();

最后更新于

这有帮助吗?