美文网首页
38_用js实现一下观察者模式?简单说一下原理

38_用js实现一下观察者模式?简单说一下原理

作者: 沐向 | 来源:发表于2020-04-09 14:38 被阅读0次

    一、概念

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

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

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

    如下图所示:

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

    二、解释

    例如:

    以学生为例,上课和下课的铃声就是被观察者,学生就是观察者,当下课铃声响了,学生就知道下课了,就出去跑着玩了,然后过了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()会报错。

    五、参考资料

    相关文章

      网友评论

          本文标题:38_用js实现一下观察者模式?简单说一下原理

          本文链接:https://www.haomeiwen.com/subject/lircmhtx.html