三十八、用js实现一下观察者模式?简单说一下原理

168 阅读8分钟

一、概念

观察者模式,属于行为型模式的一种,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

注意:有些人认为观察者模式就是发布订阅模式,但实际上观察者模式和发布订阅模式是有区别的。

区别:观察者模式只有两个,一个是观察者一个是被观察者。发布订阅模式不一样,发布订阅模式还有一个中间层,发布订阅模式的实现是,发布者通知给中间层 => 中层接受并通知订阅者 => 订阅者收到通知并发生变化

如下图所示:

上图左边是观察者模式右边是发布订阅模式

二、解释

例如:

以学生为例,上课和下课的铃声就是被观察者,学生就是观察者,当下课铃声响了,学生就知道下课了,就出去跑着玩了,然后过了10分钟,上课铃声又响了,然后学生听到上课铃,又开始从外面往教室跑,去上课。

其实我们在平时也用到过观察者模式,只是我们没有注意到而已,举一个简单的例子:我们曾经在DOM节点上面绑定过事件函数,那我们就使用过观察者模式,因为JS和DOM之间就是实现了一种观察者模式。


document.body.addEventListener("click", function() {

alert("Hello World")

}, false )

document.body.click() //模拟用户点击

三、实现

1、原生实现

(1)定义观察者类Pubsub


/* Pubsub */

function Pubsub(){

//存放事件和对应的处理方法

this.handles = {};

}

(2)实现事件订阅on


//传入事件类型type和事件处理handle

on: function (type, handle) {

if(!this.handles[type]){

this.handles[type] = [];

}

this.handles[type].push(handle);

}

(3)实现事件发布emit


emit: function () {

//通过传入参数获取事件类型

var type = Array.prototype.shift.call(arguments);

if(!this.handles[type]){

return false;

}

for (var i = 0; i < this.handles[type].length; i++) {

var handle = this.handles[type][i];

//执行事件

handle.apply(this, arguments);

}

}

需要说明的是Array.prototype.shift.call(arguments)这句代码,arguments对象是function的内置对象,可以获取到调用该方法时传入的实参数组。

shift方法取出数组中的第一个参数,就是type类型。

(4)实现事件取消订阅off


off: function (type, handle) {

handles = this.handles[type];

if(handles){

if(!handle){

handles.length = 0;//清空数组

}else{

for (var i = 0; i < handles.length; i++) {

var _handle = handles[i];

if(_handle === handle){

handles.splice(i,1);

}

}

}

}

}

(5)完整代码


/* Pubsub */

function Pubsub() {

//存放事件和对应的处理方法

this.handles = {};

}

Pubsub.prototype = {

//传入事件类型type和事件处理handle

on: function (type, handle) {

if(!this.handles[type]){

this.handles[type] = [];

}

this.handles[type].push(handle);

},

emit: function () {

//通过传入参数获取事件类型

var type = Array.prototype.shift.call(arguments);

if(!this.handles[type]){

return false;

}

for (var i = 0; i < this.handles[type].length; i++) {

var handle = this.handles[type][i];

//执行事件

handle.apply(this, arguments);

}

},

off: function (type, handle) {

handles = this.handles[type];

if(handles){

if(!handle){

handles.length = 0;//清空数组

}else{

for (var i = 0; i < handles.length; i++) {

var _handle = handles[i];

if(_handle === handle){

handles.splice(i,1);

}

}

}

}

}

}

(6)测试


var p1 = new Pubsub();

p1.on('one', function (name) {

console.log('message: '+ name);

});

p1.emit('one','one消息被触发');

console.log('===============');

var p2 = new Pubsub();

var fn1 = function (name) {

console.log('fn1: '+ name);

};

var fn2 = function (name) {

console.log('fn2: '+ name);

};

p2.on('two', fn1);

p2.on('two', fn2);

p2.emit('two','two消息被触发');

console.log('-------------');

p2.off('two', fn1);

p2.emit('two','two消息再次被触发');

console.log('-------------');

p2.off('two');

p2.emit('two','two消息触发again');

console.log('-------------');

2、vue框架中v-model双向绑定的实现


// 创建对象

var targetObj = {

name:'小王'

}

var targetObj2 = {

name:'小王'

}

// 定义值改变时的处理函数(观察者)

function observer(oldVal, newVal) {

// 其他处理逻辑...

targetObj2.name = newVal

console.info('targetObj2的name属性的值改变为 ' + newVal);

}

// 定义name属性及其set和get方法(name属性为被观察者)

Object.defineProperty(targetObj, 'name', {

enumerable: true,

configurable: true,

get: function() {

return name;

},

set: function(val) {

//调用处理函数

observer(name, val)

name = val

}

});

targetObj.name = '张三';

targetObj.name = '李四';

console.log(targetObj2.name)

以上代码输出的结果为:

可以看出,虽然我们只是改变了targetObj的name属性,但是因为观察者模式的设计,targetObj2的name属性同样被改变,这就实现了一个简单的观察者模式。

四、理解Object.defineProperty的作用

对象是由多个名/值对组成的无序的集合。对象中每个属性对应任意类型的值。

定义对象可以使用构造函数或字面量的形式:


var obj = new Object; //obj = {}

obj.name = "张三"; //添加描述

obj.say = function(){}; //添加行为

除了以上添加属性的方式,还可以使用Object.defineProperty定义新属性或修改原有的属性。

1、Object.defineProperty()

语法:


Object.defineProperty(obj, prop, descriptor)

参数说明:

  • obj:必需。目标对象

  • prop:必需。需定义或修改的属性的名字

  • descriptor:必需。目标属性所拥有的特性

返回值:

  • 传入函数的对象。即第一个参数obj

针对属性,我们可以给这个属性设置一些特性,比如是否只读不可以写;是否可以被for..in或Object.keys()遍历。

给对象的属性添加特性描述,目前提供两种形式:数据描述和存取器描述。

2、数据描述

当修改或定义对象的某个属性的时候,给这个属性添加一些特性:


var obj = {

test:"hello"

}

//对象已有的属性添加特性描述

Object.defineProperty(obj,"test",{

configurable:true | false,

enumerable:true | false,

value:任意类型的值,

writable:true | false

});

//对象新添加的属性的特性描述

Object.defineProperty(obj,"newKey",{

configurable:true | false,

enumerable:true | false,

value:任意类型的值,

writable:true | false

});

数据描述中的属性都是可选的,来看一下设置每一个属性的作用。

(1)value

属性对应的值,可以使任意类型的值,默认为undefined


var obj = {}

//第一种情况:不设置value属性

Object.defineProperty(obj,"newKey",{

});

console.log( obj.newKey ); //undefined

------------------------------

//第二种情况:设置value属性

Object.defineProperty(obj,"newKey",{

value:"hello"

});

console.log( obj.newKey ); //hello

(2)writable

属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false。


var obj = {}

//第一种情况:writable设置为false,不能重写。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false

});

//更改newKey的值

obj.newKey = "change value";

console.log( obj.newKey ); //hello

//第二种情况:writable设置为true,可以重写

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:true

});

//更改newKey的值

obj.newKey = "change value";

console.log( obj.newKey ); //change value

(3)enumerable

此属性是否可以被枚举(使用for...in或Object.keys())。设置为true可以被枚举;设置为false,不能被枚举。默认为false。


var obj = {}

//第一种情况:enumerable设置为false,不能被枚举。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false,

enumerable:false

});

//枚举对象的属性

for( var attr in obj ){

console.log( attr );

}

//第二种情况:enumerable设置为true,可以被枚举。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false,

enumerable:true

});

//枚举对象的属性

for( var attr in obj ){

console.log( attr ); //newKey

}

(4)configurable

是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。

这个属性起到两个作用:

  1. 目标属性是否可以使用delete删除

  2. 目标属性是否可以再次设置特性


//-----------------测试目标属性是否能被删除------------------------

var obj = {}

//第一种情况:configurable设置为false,不能被删除。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false,

enumerable:false,

configurable:false

});

//删除属性

delete obj.newKey;

console.log( obj.newKey ); //hello

//第二种情况:configurable设置为true,可以被删除。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false,

enumerable:false,

configurable:true

});

//删除属性

delete obj.newKey;

console.log( obj.newKey ); //undefined

//-----------------测试是否可以再次修改特性------------------------

var obj = {}

//第一种情况:configurable设置为false,不能再次修改特性。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false,

enumerable:false,

configurable:false

});

//重新修改特性

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:true,

enumerable:true,

configurable:true

});

console.log( obj.newKey ); //报错:Uncaught TypeError: Cannot redefine property: newKey

//第二种情况:configurable设置为true,可以再次修改特性。

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:false,

enumerable:false,

configurable:true

});

//重新修改特性

Object.defineProperty(obj,"newKey",{

value:"hello",

writable:true,

enumerable:true,

configurable:true

});

console.log( obj.newKey ); //hello

除了可以给新定义的属性设置特性,也可以给已有的属性设置特性


//定义对象的时候添加的属性,是可删除、可重写、可枚举的。

var obj = {

test:"hello"

}

//改写值

obj.test = 'change value';

console.log( obj.test ); //'change value'

Object.defineProperty(obj,"test",{

writable:false

})

//再次改写值

obj.test = 'change value again';

console.log( obj.test ); //依然是:'change value'

提示:一旦使用Object.defineProperty给对象添加属性,那么如果不设置属性的特性,那么configurable、enumerable、writable这些值都为默认的false


var obj = {};

//定义的新属性后,这个属性的特性中configurable,enumerable,writable都为默认的值false

//这就导致了neykey这个是不能重写、不能枚举、不能再次设置特性

//

Object.defineProperty(obj,'newKey',{

});

//设置值

obj.newKey = 'hello';

console.log(obj.newKey); //undefined

//枚举

for( var attr in obj ){

console.log(attr);

}

设置的特性总结:

  • value: 设置属性的值

  • writable: 值是否可以重写。true | false

  • enumerable: 目标属性是否可以被枚举。true | false

  • configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false

3、存取器描述

当使用存取器描述属性的特性的时候,允许设置以下特性属性:


var obj = {};

Object.defineProperty(obj,"newKey",{

get:function (){} | undefined,

set:function (value){} | undefined

configurable: true | false

enumerable: true | false

});

注意:当使用了getter或setter方法,不允许使用writable和value这两个属性

getter/setter

当设置或获取对象的某个属性的值的时候,可以提供getter/setter方法。

  • getter 是一种获得属性值的方法

  • setter是一种设置属性值的方法。

在特性中使用get/set属性来定义对应的方法。


var obj = {};

var initValue = 'hello';

Object.defineProperty(obj,"newKey",{

get:function (){

//当获取值的时候触发的函数

return initValue;

},

set:function (value){

//当设置值的时候触发的函数,设置的新值通过参数value拿到

initValue = value;

}

});

//获取值

console.log( obj.newKey ); //hello

//设置值

obj.newKey = 'change value';

console.log( obj.newKey ); //change value

注意:get或set不是必须成对出现,任写其一就可以。如果不设置方法,则get和set的默认值为undefined

configurable和enumerable同上面的用法。

4、兼容性

在ie8下只能在DOM对象上使用,尝试在原生的对象使用 Object.defineProperty()会报错。

五、参考资料