前端常见设计模式之发布订阅模式
基础
观察者模式是对象的行为模式,又叫发布订阅者模式,模型视图模式,监听者模式或者 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 种变化
add
update
delete
reconfigure
setPrototype
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);
})
}
数据劫持的缺点:
一个属性我们不可能提前定义,也不可能一个属性一个属性的写上去.
代码耦合度很高。
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 的 优缺点:
Object.defineProperty 的第一个缺陷,无法监听数组变化。
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();
最后更新于
这有帮助吗?