前端常见设计模式之发布订阅模式
基础
观察者模式是对象的行为模式,又叫发布订阅者模式,模型视图模式,监听者模式或者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,
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一下的原因了。
数据劫持的优点:
- 无需显示调用。 这也是 vue 和 react 的差别 。
- 可得知数据变化。 我们在 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);
})
<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();
后记
欢迎关注微信公众号!,进行更好的交流。
