我也来说说 JS 的事件机制

学js,不懂事件机制,基本可以说学了js,就是白学。

本人看了很多js相关书籍,评价一本说讲得好不好,我主要看两块儿,一块儿是js面向对象讲得怎么样,另一块儿就是这个事件机制这块儿。面向对象按下不表,这里就详细说说事件机制。

事件这个东西可以说js中核心之一。为啥如此重要,因为js是一门事件驱动的语言。

说说本文的结构。(真的好长,又不想写成一个系列,希望你坚持看下去{:5_353:} )
先说说怎么绑定事件,
说到事件,就得说说冒泡机制,
说到冒泡机制,就必须说委托,
说到事件,就得提提自定义事件,
说到自定义事件,就不得不说观察者模式和发布订阅模式。
很多书认为观察者模式和发布订阅模式是一回事情,也有不这样认为的,我个人倾向同意后一种观点。
其实这两个模式(或说同个一个模式的两个名字)本质上就是一种委托(且看我的想法)。
一、监听事件
绑定事件,ie有attachEvent,w3c有addEventListener,
解除绑定,ie有detachEvent,w3c有removeEventListener
jQuery做了很好的封装,api也比较多,bind,live,delegate,on等等,其不同api也所区别,由于版本问题,个别api也有所区别。我个人一般都用同一个,要么用bind,要么用on。
要自己封装也可以,下面的代码是《js高设》中的,

var EventUtil ={
    addHandler: function(element, type, handler){
        //w3c
        if(element.addEventListener){
            element.addEventListener(type, handler, false);
        }
        //ie
        else if(element.attachEvent){
            element.attachEvent('on' + type,handler);
        }else{
            element['on' + type] = handler;
        }
    },
    removeHandler: function(){
        if(element.removeEventListener){
            element.removeEventListener(type, handler, false);
        }else if(element.detachEvent){
            element.detachEvent('on' + type, handler);
        }else{
            element['on' + type] = null;
        }
    }
}

使用如下


var btn = document.getElementById('mybtn');
var handler = function(){
    alert("111")
};
//add click
EventUtil.addHandler(btn, 'click', handler);
//remove click
EventUtil.removeHandler(btn, 'click', handler);

二、冒泡机制和捕获机制

ie用的是冒泡机制,w3c都支持的。

element.addEventListener(type, handler, false)这句代码里的第三个参数就是说明是否使用捕获机制,一般我们都使用冒泡的。至于捕获机制,我个人目前尚未遇到具体的使用场景。

那啥叫冒泡机制和捕获机制呢?

看这样一种情况,父元素是ul,子元素是li,两个元素都绑定点击事件。如果我们点击了li,li的点击事件当然被触发了,那么ul是不是也触发了呢,答案是肯定的。那么就涉及到一个问题。谁先触发的问题。

冒泡机制是指先触发子元素的click事件,然后再触发父元素的click事件,是由内向外的。我觉得冒泡这个名字不好,冒泡给人的感觉是由下向上传播的感觉的。这个是根据dom树来的,不是太直观。是不是叫波纹机制比较好呢,因为在中间激起波纹,是由内向外传播的。

捕获机制就是从外向内传播的(捕获有一种“收网”的意味,或者说,捕获是“抓”,你想,用手抓自然就是由外向内嘛)。先触发父节点的点击事件,然后触发子节点的点击事件。

示例1:ie 678下,点击li触发,弹出顺序是li,ul,document





    
  

示例2:chrome下,点击li触发,弹出顺序是document,ul,li









    
  

至于如何阻止冒泡,如何阻止默认事件这里不提了。《js高设》也有封装,jquery也有e.stopPropagation和e.preventDefault();

三、事件委托

本文核心内容开始了。

说委托之前先来算算一道题94*5等于多少?
so easy,有两种算法,第一种(90+4)*5=90*5+4*5=450+20=470。这个可以说是正常思维。
第二种是(100-6)*5=500-30=470。从心理感觉上来看,还是第二种快些。
为啥说js说得好好的,突然算题呢。这里说这个事儿,主要是讲思维的问题。也许我们第一直觉会用第一种方式来算。然而一旦我们知道了第二种方式,速度就会快了起来。说要算94*5直觉想不起来,如果要算99*5,几乎所有人都会用第二种方式来算。
本文剩下的内容讲得东西都是类似上面的第二种方式。因为违反直觉的。可一旦我们懂了,其实没啥了不起的。就像你一旦知道,点蚊香其实不用掰开(两个绕在一起的),直接点着其中一头,第二天自动会剩下另一半(如果不好使说明那个蚊香不是好蚊香)这个道理是一样的。(题外话,以前我一直拿这句话对付问我一些刁钻问题的面试官,嘿嘿)。

废话不说了,说委托。
啥是委托呢?从字面意思上讲,就是本来交给你办的事,你不去自己办,而是让别人办。生活中比较简单例子就是,我们网购,快递送到公司传达室,然后我们再去传达室去取一个道理。
js委托是啥呢,因为冒泡机制,既然点击子元素时,也会触发父元素的点击事件。那么我们为啥不把点击子元素的事件要做的事情,写到父元素的事件里呢?是不是有点类似500-30的道理呢?谁第一想出来的,真叫人佩服。
先看例子(百度一大把的几乎都拿这个例子)
示例3:w3c下使用





    
  








    
  

示例4 li的mouseover和mouseout事件委托于父节点







    
  








    
  

示例5 这里再举个比较常见例子,使用场景是这样的,我们经常要使用一些表格,来展示数据,一般最后一列都是数据相关操作,比如查看、修改和删除等操作。这时也可以使用委托。







     
     showDetails 
     update 
     delete 
      
     
    






     
     showDetails 
     update 
     delete 
      
     
    

ext的gridPanel做的比较好,行点击事件或表格点击事件,其event都传进来了。easy-ui就不咋地了,坑。

这里先总结一下js委托相关。
1.好处大大的,因为把事件绑定到父节点上了,因此事件少了很多。就算新增的子节点自然也有了相关事件。删除部分子节点不用你销毁对应节点上绑定的事件。
2.父节点是通过event.target来找到对应的子节点的。

我们再来看看jquery怎么使用委托?
jquery那可真方便了,委托的关键是怎么通过父节点来找到对应的子节点的。可以通过event.target,如能拿到this,那就是想做啥就做啥。委托也有专门的api。如delegate和on了。
target方式,不演示了,这里大致说一下on,请看例子。注意回调函数里this是currentTarget的(原文此处写的是target,且看下面的“注意”)(这个跟jquery没关,绑定到dom上的回调函数里的this都是currentTarget的)。
on函数还是蛮吊的请点这里看详细说明
实例6:大致写个表单验证,估计也没什么人这么写,这里只是演示jquery委托。




    
      your name
     
     
your age
your sex male female shemale
submit cancel



    
      your name
     
     
your age
your sex male female shemale
submit cancel

注意 : 上面说的回调函数里this是target,说法是不正确的(差点误人子弟),其实是currentTarget。二者有什么区别呢?target是触发事件最开始的那个子节点。而currentTarget不言而喻,就是当前节点了,你绑定事件执行事件的那个节点。(之前例子一直是两层,我没仔细看)请看下面三层div的例子,注意currentTarget是中间层center(不是outer),而taget是最里层inner。说明匿名函数最后在center上执行的。







     
      
       
      
    



四、自定义事件

ok,先来看看jquery是怎么实现自定义事件的

jquery中用on或bind绑定自定义事件,用trigger来触发

示例


$(function(){
    $('button').on('mycustomEvent',function(){
        alert("trigger customEvent");
    });
    $('button').trigger('mycustomEvent');
});











jquery是怎么用的,先看到这,这时要来说一个关键的事情。必须得解释一下什么叫自定义事件。

一旦你理解其精髓,你的js世界观就会变了,会感觉到整个天空都亮了(且听我胡说)。

先来看一下什么叫事件呢?不用去找什么定义,自己闭眼睛想想就知道。用户要跟浏览器交互,浏览器要做出反应。这个反应就是事件。

以button绑定onclick为例,我们来看看事件的组成要素

第一,谁的事件,button的

第二,谁来触发,用户点击按钮时触发事件,可以说是用户来触发的

第三,谁来执行,button来执行,所以对应函数执行环境(执行上下文)this是指向button的。

第四,要做什么事情,要做的事情就是函数的执行。

第五,既然是函数,那么参数是什么,是event。那event是什么?event是事件的状态对象。

细心的我们,有没有发现一件事情,函数是整个事件中最重要的组成部分。下面来打通任督二脉。

事件==函数

来看一下函数的组成部分
为了方便说,举个例子

var obj ={
    fn : function(){
        console.log(this);
        console.log(arguments);
    }
}
obj.fn();

第一,谁的函数,obj的

第二,谁来触发,函数调用来触发的,或者说是代码obj.fn()来触发的

第三,谁来执行,obj来执行,因此对应函数的this是指向obj的。

第四,要做什么事情,不必说,

第五,既然是函数,那么参数是什么,也不必说。

其核心的地方就是触发等价于调用。再换个角度想想,事件不就是要做一些事情吗,而函数正是做一些事情的封装,二者肯定脱不了干系的。
那么什么叫自定义事件,狭义的来说,给dom元素绑定一些非浏览器默认支持的事件。广义的来说呢?要反过来来看上面的等式
函数==事件
函数其实就是自定义的事件
所以大家看那个jquery自定义事件例子,初看没啥用,其实就相当于

var obj = $('button')
obj.customEvent = function(){
    alert("trigger customEvent");
}
obj.customEvent();

但是,事实关键不在于,代码写的如何,代码谁多谁少的问题。而是思维的转变,如果你把函数当成事件来看,看事情的角度就发生变化了。

为啥说js是事件驱动的一门语言。现在能更好的理解这句话了。

扯远了,回到jquery自定义事件。
先看一下全选的例子,可以直接看运行效果。下面是直觉思维的实现方式。










 add

     
      
      
      
      
    

其核心代码是

实现1:

$(function(){
    //绑定按钮点击事件
    $('#add').on('click',function(){
        //看看是不是全选
        if($('#selectAll').is(':checked')){
            $('#container').append(' ');
        }else{
            $('#container').append(' ');
        }
    });
    //全选逻辑
    $('#selectAll').on('click',function(){
        if(this.checked){
            $('#container').find('input:checkbox').prop('checked','checked');
        }else{
            $('#container').find('input:checkbox').removeProp('checked');
        }
    });
})

从上面可以看出来

add要和container以及selectAll打交道

selectAll要和container打交道

如果把这三者,看做是三个模块,那么可以说这三者强耦合在一起了。

下面换一种方式来做
此时自定义事件出马,只让add、selectAll分别和container打交道
实现2

$(function(){
    var $container =$('#container');
    
    //绑定两个自定义事件
    $container.on('selectCheckbox',function(e,checked){
        //缓存状态
        var $this = $(this).data('state',checked);
        if(checked){
            $this.find('input:checkbox').prop('checked','checked');
        }else{
            $this.find('input:checkbox').removeProp('checked');
        }
    }).on('addCheckbox',function(){
        var $this = $(this);
        if($this.data('state')){
            $this.append(' ');
        }else{
            $this.append(' ');
        }
    });
    
    //全选按钮触发自定义事件
    $('#selectAll').on('click',function(){
        $container.trigger('selectCheckbox',[this.checked])
    });
    
    //新增按钮触发自定义事件
    $('#add').on('click',function(){
        $container.trigger('addCheckbox');
    });
})

情况好了一些,还不够彻底,能不能这三个模块一点也不发生耦合呢?

貌似很难,其实再多出一个模块就ok了(就这个例子而言,代码确实比最开始的实现多了很多,这里为了把道理说明白,真实大才小用了)

实现3

$(function(){
    var $common =$({});
    
    //绑定两个自定义事件
    $common.on('selectCheckbox',function(e,checked){
        //缓存状态(原先是缓存到container中的)
        var $this = $(this).data('state',checked);
        if(checked){
            $('#container').find('input:checkbox').prop('checked','checked');
        }else{
            $('#container').find('input:checkbox').removeProp('checked');
        }
    }).on('addCheckbox',function(){
        if($(this).data('state')){
            $('#container').append(' ');
        }else{
            $('#container').append(' ');
        }
    });
    
    //全选按钮触发自定义事件
    $('#selectAll').on('click',function(){
        $common.trigger('selectCheckbox',[this.checked])
    });
    
    //新增按钮触发自定义事件
    $('#add').on('click',function(){
        $common.trigger('addCheckbox');
    });
})









 add

     
      
      
      
      
    

如果,需求变更了,还要实现反选,即只要一个不选,selectall要变成不选的,以及container中所有都挑上,selectall也要选上,在第三种逻辑上改应该是最轻松,为啥?解耦了呗。

现在来尝试打通这个任督二脉
组件交互==回调函数==绑定事件==事件监听==调用方法==传递消息

之前全选那个例子一直说三者如果是三个模块,如果真是三个模块怎么办?且看我用回调函数来模拟,再来看看是否真的可以把函数当做事件来理解
实现4



























原生js实现自定义事件代码待续(要么你百度)
本节也待续
五、发布订阅模式
在说发布订阅模式之前,让我们再重新看看委托。
这次再说委托可不是仅仅说由于冒泡机制的那个委托。
我们要站在一个更高层次上来研究研究这个东西(正所谓站得高尿得远,嘿嘿)。
委托本质是啥呢?还是你句话,你要办的事情,不是自己直接去办,而是拜托别人去办。
那好,由于主宾不同,这就引出了两种思路。
我委托别人,那相当于我是借鸡下蛋。(直觉思维)
别人委托我,那相当于我为其他人做嫁衣。
那么我们就来看看怎么做的嫁衣、怎么下的蛋

var sum = function(a, b){
    return a + b;
}
var mysum = function(a, b){
    return sum(a, b);
}
alert(mysum(2,3));

mysum借用了sum下了蛋。

这时,你恐怕说,老姚,我读书少,你别忽悠我。这不就是简单函数调用嘛,跟委托有毛关系。

没错,这是函数调用,但我们要用委托的眼光来看我们的代码,由于角度不同,想法也许就会不同。

为了讲下去,把例子复杂一下

var operation = function(a,b,oper){
    var r = 0
    switch(oper){
        case '+': r = a+b;break;
        case '-': r = a-b;break;
        case '*': r = a*b;break;
        case '/': r = a/b;break;
        case '%': r = a%b;break;
        default : r = NAN;
    }
    return r;
}
var minus = function(a,b){
    return operation(a,b,'-');
}
alert(minus(4,2));

可以说写operation就是为minus做嫁衣的。代码虽简单其实这也是一种模式:门面模式。

你说直接要用operation来做减法,当然可以的,但是minus是不是更简单一些,更有语意化呢。

jquery中那可是大量用了这种方法,on是bind的门面,$.get是$.ajax的门面等等。

我们在做项目中,开始写代码时,对功能没有完全认识之前,思路基本上都是借鸡下蛋的,
总想着怎么使用之前的函数。这是可以理解的,功能都完成后,这时千万别认为活干完了。还有一个事情要做,那就是重构。
重构的思路是反过来的,是想办法来做各种嫁衣。
还有一种情况下蛋和嫁衣是同一个东西,你猜到是什么了吗?
递归。递归说简单也很简单,说难也很难,当初本人可是没少在上面耗时间,以后可能会出一篇文章专门讲这个的。
写文章其实也是为他人做嫁衣。我们学习嘛,当然就是借鸡下蛋了。你看看,委托的概念随随便便就还可以上升到哲学层次上哈。

扯远了,继续下蛋。

var P = function(name){
    this.name = name;
}

var Man = function(name,sex){
    P.call(this,name);
    this.sex = sex;
}

var m = new Man('laoyao','male');
alert(m.name);

这个是类式继承的简单实现。Man也要写this.name = name的,结果一看P都写过了,那好我借你的鸡下我的蛋.

事件委托是通过冒泡来的,函数的委托呢?没错,就是调用,如果想把你的this变成我的,那得通过call和apply的。

arguments是伪数组,要变成真正的数组的话,我们得写一些逻辑。结果写完了,会发现跟数组的slice内部基本逻辑差不多。

那好你写完了,我就不写了。直接委托给你得了。[].slice.call(arguments,0);(当然,我这是马后炮,没看到别人这么用时自己哪里想得到)

组件间通信,怎么个交互法,有没有什么委托呢?没错,你猜对了,就是本节主题发布订阅模式。

还记得那个全选的例子吧,上面有一种实现(实现3)就是让各个模块充分解耦,填了一个新的模块$common,对的,它就是那个托,所有人之间的通信都是通过它来的。

而发布订阅模式做得更绝秒了,让我们甚至都觉察不到它的存在。

让我们来看看它是怎么实现的。

(function($){
    var o = $({});
    $.sub = function(){
        o.on.apply(o,arguments);
    }
    $.unsub = function(){
        o.off.apply(o,arguments);
    }
    $.pub = function(){
        o.trigger.apply(o,arguments);
    }
})(jQuery);

使用如下:

$.sub('customEvent',function(e,text){
    alert(text);
})
$.pub('customEvent',['oh yeah']);
$.pub('customEvent',['haha']);
$.unsub('customEvent');
$.pub('customEvent',['can not see']);

运行如下:









先来简单看看那几行代码

给jQuery函数添加了三个静态函数,订阅、取消订阅、发布。函数内部通过on和trigger来实现的。是委托到$({})这个对象上的。

内部虽然是通过绑定自定义事件和触发自定义事件来实现的。自定义事件只不过就是个名字而已。我们可以按照官方来理解,把它看做是一个通道或者是主题,订阅我这个主题,我发消息你自然就能看到。

下面我们来改改全选的实现3那个例子。

实现5

$(function(){  
    var currentChecked = false;
    //订阅主题
    $.sub('selectCheckbox',function(e,checked){
        currentChecked = checked;
        if(checked){
            $('#container').find('input:checkbox').prop('checked','checked');
        }else{
            $('#container').find('input:checkbox').removeProp('checked');
        }
    })
    //订阅主题
    $.sub('addCheckbox',function(){
        if(currentChecked){
            $('#container').append(' ');
        }else{
            $('#container').append(' ');
        }
    });
    
    //全选按钮发布主题
    $('#selectAll').on('click',function(){
        $.pub('selectCheckbox',[this.checked])
    });
    
    //新增按钮发布主题
    $('#add').on('click',function(){
        $.pub('addCheckbox');
    });
})









 add

     
      
      
      
      
    

同样的,我们也可以来改写一下全选的实现4的controller层

//controller逻辑层(可以写成类)
$(function(){
    var currentChecked = false;
    $.sub('selectAll',function(e,flag){
        currentChecked = flag;
        container.selectOrCacelAll(flag);
    });
    $.sub('addBox',function(e){
        container.add(currentChecked);
    });
    
    //selectAll实例
    var selectAll = new SelectAll({
        listeners : {
            onSelectAll :function(flag){
                $.pub('selectAll',[flag]);
            }
        }
    });
    //add实例
    var add = new Add({
        listeners : {
            onAdd : function(){
                $.pub('addBox');
            }
        }
    });
    //container实例
    var container = new Container({});
});

原生js实现,有很多种实现方法,这里我简单写了一个,匿名函数还是删不掉的(如果要实现的话,得弄个uid才行)。

var obj = (function(){
    return {
        topics : {},
        sub : function(topic,fn){
            if(!this.topics[topic]){
                this.topics[topic] = [];
            }
            this.topics[topic].push(fn);
        },
        pub : function(topic){
            var arr = this.topics[topic];
            var args = [].slice.call(arguments,1);
            if(!arr){
                return;
            }
            for(var i = 0, len = arr.length;i < len;i++){
                arr[i](args);
            }
        },
        unsub : function(topic,fn){
            var arr = this.topics[topic];
            if(!arr){
                return;
            }
            if(typeof fn !== 'function'){
                delete this.topics[topic];
                return;
            }
            for(var i = 0, len = arr.length;i < len;i++){
                if(arr[i] == fn){
                    arr.splice(i,1);
                }
            }
        }
    };
})();











六、观察者模式

提到observer,你能想到什么?

我最开始想到的美剧《危机边缘》里面那些大光头,说是每次重大历史事件里面都有个大光头,在观察着一切的发生。

很少有人会直接想到浏览器。其实我们的浏览器整整就是一个观察者模式。

先说几个概念:
你说是观察者模式,肯定有观察者吧,
那观察谁呢,即被观察者,我们姑且称其为目标吧。
观察你的什么呢?相貌吗?观察的是状态。
我观察你干啥呢,看看你有啥变化?根据你的状态来做我的事情。

这里还是拿那个全选的例子来说说:
目标 : 全选复选框
观察者:container里的复选框,和add按钮
状态:全选按钮是否选上
行为:你选,container我就选上,点击add时,添加选上的按钮,反之,反然。

那怎么实现呢?
这里有两种思路,我观察你,想看看你的状态,我可以到你那去看,还有一种就是你的状态变化时,你来告诉我。
说白了就是“取”和“送”的概念。
观察者模式是采用“送”的概念。
那也涉及到一个问题,既然是“送”,你怎么来找到我呢?那你得保存我给你的地址。

现在可以来看看定义了。
Observer(观察者模式):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
ok,我们先按照这种思路来实现一下
实现6

$(function(){

    //缓存三个元素
    var $selectAll = $('#selectAll');
    var $add = $('#add');
    var $container = $('#container');
    
    //目标保存观察者
    $selectAll.cbList = [];
    $selectAll.addBtn = $add;
    
    //目标通知函数,通知观察者更新
    $selectAll.notify = function(value){
        $.each(this.cbList,function(index,checkbox){
            checkbox.update(value);
        });
        this.addBtn.update(value);
    };
    
    
    $selectAll.click(function(){
        $selectAll.notify(this.checked);
    })
    $add.click(function(){
        addCheckbox($add.data('checked'));
    });
    
    //观察者add的更新方法
    $add.update = function(value){
        this.data('checked',value);
    }
    function addCheckbox(flag){
        var $cb;
        if(flag){
            $cb = $('');
        }else{
            $cb = $('');
        }
        //观察者复选框更新方法
        $cb.update = function(value){
            this[0].checked = value; 
        }
        
        //目标存入观察者
        $selectAll.cbList.push($cb);
        $('#container').append($cb);
    }
})








 add

     
    

看到这里,难免会想这里哪有观察者模式的“模式”影子啊。

下来就要抽出目标和观察者的抽象接口,然后才是真正观察模式,才能得以复用。

var ObserverPatten = (function(){
    var Subject = function(){
        this.observerlist = [];
    }; 
    Subject.prototype = {
        add : function(observer){
            this.observerlist.push(observer);
        },
        notify : function(state){
            var arr = this.observerlist;
            for(var i = 0, len = arr.length;i
    

因此全选的例子可以改成如下

实现7

$(function(){

    //缓存三个元素
    var $selectAll = $('#selectAll');
    var $add = $('#add');
    var $container = $('#container');
    
    //具体目标实现目标接口
    $selectAll.extend(new ObserverPatten.Subject);
    $selectAll.add($add);
    $selectAll.click(function(){
        $selectAll.notify(this.checked);
    })
    
    $add.click(function(){
        addCheckbox($add.data('checked'));
    });
    
    //观察者add实现观察者接口
    $add.extend(new ObserverPatten.Observer);
    
    //复写update方法
    $add.update = function(value){
        this.data('checked',value);
    }
    function addCheckbox(flag){
        var $cb;
        if(flag){
            $cb = $('');
        }else{
            $cb = $('');
        }
        
        //观察者复选框实现观察者接口
        $cb.extend(new ObserverPatten.Observer);
        
        //复写update方法
        $cb.update = function(value){
            this[0].checked = value; 
        }
        
        //目标存入观察者
        $selectAll.add($cb);
        $('#container').append($cb);
    }
})









 add

     
    

七、后记

写到这里,写了一周也没写完,真的好长。

回顾本文,主要就讲了一件事情,特意去模糊事件和函数在我们头脑中的概念区别。目的也只是让我们理解代码更轻松些。

至于后面说的发布订阅模式和观察者模式,也只是大概地说说,发订模式在全选这个例子实现,其实是其一种特殊情况,就是中介者模式。

订阅者也可以作为另一个订阅者的发布者。也就是说发布者和订阅者,可以多对多的。

至于发订是观察者的一个分支,也好理解的。全选那个例子,三个模块就是观察者,目标就是发布订阅实现里面的$({})。

这个东西,本来想展开的,想写多对多的例子。一看本文太长了,还是算了。

写文章确实不容易,不仅要考虑说出去的话,有没有歧义。还要为自己的话,负责的。尽量不误人子弟。

还有一个事情,比较麻烦。就是案例的设计,还得尽量不去copy网上类似的文章,免得别人说你抄的。真头疼的。

其实还有几个点,原本也想展开说说的。念在眉毛胡子一把抓,没有重点,留给以后吧。

本文完。 学js,不懂事件机制,基本可以说学了js,就是白学。

本人看了很多js相关书籍,评价一本说讲得好不好,我主要看两块儿,一块儿是js面向对象讲得怎么样,另一块儿就是这个事件机制这块儿。面向对象按下不表,这里就详细说说事件机制。

事件这个东西可以说js中核心之一。为啥如此重要,因为js是一门事件驱动的语言。

说说本文的结构。(真的好长,又不想写成一个系列,希望你坚持看下去{:5_353:} )
先说说怎么绑定事件,
说到事件,就得说说冒泡机制,
说到冒泡机制,就必须说委托,
说到事件,就得提提自定义事件,
说到自定义事件,就不得不说观察者模式和发布订阅模式。
很多书认为观察者模式和发布订阅模式是一回事情,也有不这样认为的,我个人倾向同意后一种观点。
其实这两个模式(或说同个一个模式的两个名字)本质上就是一种委托(且看我的想法)。
一、监听事件
绑定事件,ie有attachEvent,w3c有addEventListener,
解除绑定,ie有detachEvent,w3c有removeEventListener
jQuery做了很好的封装,api也比较多,bind,live,delegate,on等等,其不同api也所区别,由于版本问题,个别api也有所区别。我个人一般都用同一个,要么用bind,要么用on。
要自己封装也可以,下面的代码是《js高设》中的,

var EventUtil ={
    addHandler: function(element, type, handler){
        //w3c
        if(element.addEventListener){
            element.addEventListener(type, handler, false);
        }
        //ie
        else if(element.attachEvent){
            element.attachEvent('on' + type,handler);
        }else{
            element['on' + type] = handler;
        }
    },
    removeHandler: function(){
        if(element.removeEventListener){
            element.removeEventListener(type, handler, false);
        }else if(element.detachEvent){
            element.detachEvent('on' + type, handler);
        }else{
            element['on' + type] = null;
        }
    }
}

使用如下


var btn = document.getElementById('mybtn');
var handler = function(){
    alert("111")
};
//add click
EventUtil.addHandler(btn, 'click', handler);
//remove click
EventUtil.removeHandler(btn, 'click', handler);

二、冒泡机制和捕获机制

ie用的是冒泡机制,w3c都支持的。

element.addEventListener(type, handler, false)这句代码里的第三个参数就是说明是否使用捕获机制,一般我们都使用冒泡的。至于捕获机制,我个人目前尚未遇到具体的使用场景。

那啥叫冒泡机制和捕获机制呢?

看这样一种情况,父元素是ul,子元素是li,两个元素都绑定点击事件。如果我们点击了li,li的点击事件当然被触发了,那么ul是不是也触发了呢,答案是肯定的。那么就涉及到一个问题。谁先触发的问题。

冒泡机制是指先触发子元素的click事件,然后再触发父元素的click事件,是由内向外的。我觉得冒泡这个名字不好,冒泡给人的感觉是由下向上传播的感觉的。这个是根据dom树来的,不是太直观。是不是叫波纹机制比较好呢,因为在中间激起波纹,是由内向外传播的。

捕获机制就是从外向内传播的(捕获有一种“收网”的意味,或者说,捕获是“抓”,你想,用手抓自然就是由外向内嘛)。先触发父节点的点击事件,然后触发子节点的点击事件。

示例1:ie 678下,点击li触发,弹出顺序是li,ul,document





    
  

示例2:chrome下,点击li触发,弹出顺序是document,ul,li









    
  

至于如何阻止冒泡,如何阻止默认事件这里不提了。《js高设》也有封装,jquery也有e.stopPropagation和e.preventDefault();

三、事件委托

本文核心内容开始了。

说委托之前先来算算一道题94*5等于多少?
so easy,有两种算法,第一种(90+4)*5=90*5+4*5=450+20=470。这个可以说是正常思维。
第二种是(100-6)*5=500-30=470。从心理感觉上来看,还是第二种快些。
为啥说js说得好好的,突然算题呢。这里说这个事儿,主要是讲思维的问题。也许我们第一直觉会用第一种方式来算。然而一旦我们知道了第二种方式,速度就会快了起来。说要算94*5直觉想不起来,如果要算99*5,几乎所有人都会用第二种方式来算。
本文剩下的内容讲得东西都是类似上面的第二种方式。因为违反直觉的。可一旦我们懂了,其实没啥了不起的。就像你一旦知道,点蚊香其实不用掰开(两个绕在一起的),直接点着其中一头,第二天自动会剩下另一半(如果不好使说明那个蚊香不是好蚊香)这个道理是一样的。(题外话,以前我一直拿这句话对付问我一些刁钻问题的面试官,嘿嘿)。

废话不说了,说委托。
啥是委托呢?从字面意思上讲,就是本来交给你办的事,你不去自己办,而是让别人办。生活中比较简单例子就是,我们网购,快递送到公司传达室,然后我们再去传达室去取一个道理。
js委托是啥呢,因为冒泡机制,既然点击子元素时,也会触发父元素的点击事件。那么我们为啥不把点击子元素的事件要做的事情,写到父元素的事件里呢?是不是有点类似500-30的道理呢?谁第一想出来的,真叫人佩服。
先看例子(百度一大把的几乎都拿这个例子)
示例3:w3c下使用





    
  








    
  

示例4 li的mouseover和mouseout事件委托于父节点







    
  








    
  

示例5 这里再举个比较常见例子,使用场景是这样的,我们经常要使用一些表格,来展示数据,一般最后一列都是数据相关操作,比如查看、修改和删除等操作。这时也可以使用委托。







     
     showDetails 
     update 
     delete 
      
     
    






     
     showDetails 
     update 
     delete 
      
     
    

ext的gridPanel做的比较好,行点击事件或表格点击事件,其event都传进来了。easy-ui就不咋地了,坑。

这里先总结一下js委托相关。
1.好处大大的,因为把事件绑定到父节点上了,因此事件少了很多。就算新增的子节点自然也有了相关事件。删除部分子节点不用你销毁对应节点上绑定的事件。
2.父节点是通过event.target来找到对应的子节点的。

我们再来看看jquery怎么使用委托?
jquery那可真方便了,委托的关键是怎么通过父节点来找到对应的子节点的。可以通过event.target,如能拿到this,那就是想做啥就做啥。委托也有专门的api。如delegate和on了。
target方式,不演示了,这里大致说一下on,请看例子。注意回调函数里this是currentTarget的(原文此处写的是target,且看下面的“注意”)(这个跟jquery没关,绑定到dom上的回调函数里的this都是currentTarget的)。
on函数还是蛮吊的请点这里看详细说明
实例6:大致写个表单验证,估计也没什么人这么写,这里只是演示jquery委托。




    
      your name
     
     
your age
your sex male female shemale
submit cancel



    
      your name
     
     
your age
your sex male female shemale
submit cancel

注意 : 上面说的回调函数里this是target,说法是不正确的(差点误人子弟),其实是currentTarget。二者有什么区别呢?target是触发事件最开始的那个子节点。而currentTarget不言而喻,就是当前节点了,你绑定事件执行事件的那个节点。(之前例子一直是两层,我没仔细看)请看下面三层div的例子,注意currentTarget是中间层center(不是outer),而taget是最里层inner。说明匿名函数最后在center上执行的。







     
      
       
      
    



四、自定义事件

ok,先来看看jquery是怎么实现自定义事件的

jquery中用on或bind绑定自定义事件,用trigger来触发

示例


$(function(){
    $('button').on('mycustomEvent',function(){
        alert("trigger customEvent");
    });
    $('button').trigger('mycustomEvent');
});











jquery是怎么用的,先看到这,这时要来说一个关键的事情。必须得解释一下什么叫自定义事件。

一旦你理解其精髓,你的js世界观就会变了,会感觉到整个天空都亮了(且听我胡说)。

先来看一下什么叫事件呢?不用去找什么定义,自己闭眼睛想想就知道。用户要跟浏览器交互,浏览器要做出反应。这个反应就是事件。

以button绑定onclick为例,我们来看看事件的组成要素

第一,谁的事件,button的

第二,谁来触发,用户点击按钮时触发事件,可以说是用户来触发的

第三,谁来执行,button来执行,所以对应函数执行环境(执行上下文)this是指向button的。

第四,要做什么事情,要做的事情就是函数的执行。

第五,既然是函数,那么参数是什么,是event。那event是什么?event是事件的状态对象。

细心的我们,有没有发现一件事情,函数是整个事件中最重要的组成部分。下面来打通任督二脉。

事件==函数

来看一下函数的组成部分
为了方便说,举个例子

var obj ={
    fn : function(){
        console.log(this);
        console.log(arguments);
    }
}
obj.fn();

第一,谁的函数,obj的

第二,谁来触发,函数调用来触发的,或者说是代码obj.fn()来触发的

第三,谁来执行,obj来执行,因此对应函数的this是指向obj的。

第四,要做什么事情,不必说,

第五,既然是函数,那么参数是什么,也不必说。

其核心的地方就是触发等价于调用。再换个角度想想,事件不就是要做一些事情吗,而函数正是做一些事情的封装,二者肯定脱不了干系的。
那么什么叫自定义事件,狭义的来说,给dom元素绑定一些非浏览器默认支持的事件。广义的来说呢?要反过来来看上面的等式
函数==事件
函数其实就是自定义的事件
所以大家看那个jquery自定义事件例子,初看没啥用,其实就相当于

var obj = $('button')
obj.customEvent = function(){
    alert("trigger customEvent");
}
obj.customEvent();

但是,事实关键不在于,代码写的如何,代码谁多谁少的问题。而是思维的转变,如果你把函数当成事件来看,看事情的角度就发生变化了。

为啥说js是事件驱动的一门语言。现在能更好的理解这句话了。

扯远了,回到jquery自定义事件。
先看一下全选的例子,可以直接看运行效果。下面是直觉思维的实现方式。










 add

     
      
      
      
      
    

其核心代码是

实现1:

$(function(){
    //绑定按钮点击事件
    $('#add').on('click',function(){
        //看看是不是全选
        if($('#selectAll').is(':checked')){
            $('#container').append(' ');
        }else{
            $('#container').append(' ');
        }
    });
    //全选逻辑
    $('#selectAll').on('click',function(){
        if(this.checked){
            $('#container').find('input:checkbox').prop('checked','checked');
        }else{
            $('#container').find('input:checkbox').removeProp('checked');
        }
    });
})

从上面可以看出来

add要和container以及selectAll打交道

selectAll要和container打交道

如果把这三者,看做是三个模块,那么可以说这三者强耦合在一起了。

下面换一种方式来做
此时自定义事件出马,只让add、selectAll分别和container打交道
实现2

$(function(){
    var $container =$('#container');
    
    //绑定两个自定义事件
    $container.on('selectCheckbox',function(e,checked){
        //缓存状态
        var $this = $(this).data('state',checked);
        if(checked){
            $this.find('input:checkbox').prop('checked','checked');
        }else{
            $this.find('input:checkbox').removeProp('checked');
        }
    }).on('addCheckbox',function(){
        var $this = $(this);
        if($this.data('state')){
            $this.append(' ');
        }else{
            $this.append(' ');
        }
    });
    
    //全选按钮触发自定义事件
    $('#selectAll').on('click',function(){
        $container.trigger('selectCheckbox',[this.checked])
    });
    
    //新增按钮触发自定义事件
    $('#add').on('click',function(){
        $container.trigger('addCheckbox');
    });
})

情况好了一些,还不够彻底,能不能这三个模块一点也不发生耦合呢?

貌似很难,其实再多出一个模块就ok了(就这个例子而言,代码确实比最开始的实现多了很多,这里为了把道理说明白,真实大才小用了)

实现3

$(function(){
    var $common =$({});
    
    //绑定两个自定义事件
    $common.on('selectCheckbox',function(e,checked){
        //缓存状态(原先是缓存到container中的)
        var $this = $(this).data('state',checked);
        if(checked){
            $('#container').find('input:checkbox').prop('checked','checked');
        }else{
            $('#container').find('input:checkbox').removeProp('checked');
        }
    }).on('addCheckbox',function(){
        if($(this).data('state')){
            $('#container').append(' ');
        }else{
            $('#container').append(' ');
        }
    });
    
    //全选按钮触发自定义事件
    $('#selectAll').on('click',function(){
        $common.trigger('selectCheckbox',[this.checked])
    });
    
    //新增按钮触发自定义事件
    $('#add').on('click',function(){
        $common.trigger('addCheckbox');
    });
})









 add

     
      
      
      
      
    

如果,需求变更了,还要实现反选,即只要一个不选,selectall要变成不选的,以及container中所有都挑上,selectall也要选上,在第三种逻辑上改应该是最轻松,为啥?解耦了呗。

现在来尝试打通这个任督二脉
组件交互==回调函数==绑定事件==事件监听==调用方法==传递消息

之前全选那个例子一直说三者如果是三个模块,如果真是三个模块怎么办?且看我用回调函数来模拟,再来看看是否真的可以把函数当做事件来理解
实现4



























原生js实现自定义事件代码待续(要么你百度)
本节也待续
五、发布订阅模式
在说发布订阅模式之前,让我们再重新看看委托。
这次再说委托可不是仅仅说由于冒泡机制的那个委托。
我们要站在一个更高层次上来研究研究这个东西(正所谓站得高尿得远,嘿嘿)。
委托本质是啥呢?还是你句话,你要办的事情,不是自己直接去办,而是拜托别人去办。
那好,由于主宾不同,这就引出了两种思路。
我委托别人,那相当于我是借鸡下蛋。(直觉思维)
别人委托我,那相当于我为其他人做嫁衣。
那么我们就来看看怎么做的嫁衣、怎么下的蛋

var sum = function(a, b){
    return a + b;
}
var mysum = function(a, b){
    return sum(a, b);
}
alert(mysum(2,3));

mysum借用了sum下了蛋。

这时,你恐怕说,老姚,我读书少,你别忽悠我。这不就是简单函数调用嘛,跟委托有毛关系。

没错,这是函数调用,但我们要用委托的眼光来看我们的代码,由于角度不同,想法也许就会不同。

为了讲下去,把例子复杂一下

var operation = function(a,b,oper){
    var r = 0
    switch(oper){
        case '+': r = a+b;break;
        case '-': r = a-b;break;
        case '*': r = a*b;break;
        case '/': r = a/b;break;
        case '%': r = a%b;break;
        default : r = NAN;
    }
    return r;
}
var minus = function(a,b){
    return operation(a,b,'-');
}
alert(minus(4,2));

可以说写operation就是为minus做嫁衣的。代码虽简单其实这也是一种模式:门面模式。

你说直接要用operation来做减法,当然可以的,但是minus是不是更简单一些,更有语意化呢。

jquery中那可是大量用了这种方法,on是bind的门面,$.get是$.ajax的门面等等。

我们在做项目中,开始写代码时,对功能没有完全认识之前,思路基本上都是借鸡下蛋的,
总想着怎么使用之前的函数。这是可以理解的,功能都完成后,这时千万别认为活干完了。还有一个事情要做,那就是重构。
重构的思路是反过来的,是想办法来做各种嫁衣。
还有一种情况下蛋和嫁衣是同一个东西,你猜到是什么了吗?
递归。递归说简单也很简单,说难也很难,当初本人可是没少在上面耗时间,以后可能会出一篇文章专门讲这个的。
写文章其实也是为他人做嫁衣。我们学习嘛,当然就是借鸡下蛋了。你看看,委托的概念随随便便就还可以上升到哲学层次上哈。

扯远了,继续下蛋。

var P = function(name){
    this.name = name;
}

var Man = function(name,sex){
    P.call(this,name);
    this.sex = sex;
}

var m = new Man('laoyao','male');
alert(m.name);

这个是类式继承的简单实现。Man也要写this.name = name的,结果一看P都写过了,那好我借你的鸡下我的蛋.

事件委托是通过冒泡来的,函数的委托呢?没错,就是调用,如果想把你的this变成我的,那得通过call和apply的。

arguments是伪数组,要变成真正的数组的话,我们得写一些逻辑。结果写完了,会发现跟数组的slice内部基本逻辑差不多。

那好你写完了,我就不写了。直接委托给你得了。[].slice.call(arguments,0);(当然,我这是马后炮,没看到别人这么用时自己哪里想得到)

组件间通信,怎么个交互法,有没有什么委托呢?没错,你猜对了,就是本节主题发布订阅模式。

还记得那个全选的例子吧,上面有一种实现(实现3)就是让各个模块充分解耦,填了一个新的模块$common,对的,它就是那个托,所有人之间的通信都是通过它来的。

而发布订阅模式做得更绝秒了,让我们甚至都觉察不到它的存在。

让我们来看看它是怎么实现的。

(function($){
    var o = $({});
    $.sub = function(){
        o.on.apply(o,arguments);
    }
    $.unsub = function(){
        o.off.apply(o,arguments);
    }
    $.pub = function(){
        o.trigger.apply(o,arguments);
    }
})(jQuery);

使用如下:

$.sub('customEvent',function(e,text){
    alert(text);
})
$.pub('customEvent',['oh yeah']);
$.pub('customEvent',['haha']);
$.unsub('customEvent');
$.pub('customEvent',['can not see']);

运行如下:









先来简单看看那几行代码

给jQuery函数添加了三个静态函数,订阅、取消订阅、发布。函数内部通过on和trigger来实现的。是委托到$({})这个对象上的。

内部虽然是通过绑定自定义事件和触发自定义事件来实现的。自定义事件只不过就是个名字而已。我们可以按照官方来理解,把它看做是一个通道或者是主题,订阅我这个主题,我发消息你自然就能看到。

下面我们来改改全选的实现3那个例子。

实现5

$(function(){  
    var currentChecked = false;
    //订阅主题
    $.sub('selectCheckbox',function(e,checked){
        currentChecked = checked;
        if(checked){
            $('#container').find('input:checkbox').prop('checked','checked');
        }else{
            $('#container').find('input:checkbox').removeProp('checked');
        }
    })
    //订阅主题
    $.sub('addCheckbox',function(){
        if(currentChecked){
            $('#container').append(' ');
        }else{
            $('#container').append(' ');
        }
    });
    
    //全选按钮发布主题
    $('#selectAll').on('click',function(){
        $.pub('selectCheckbox',[this.checked])
    });
    
    //新增按钮发布主题
    $('#add').on('click',function(){
        $.pub('addCheckbox');
    });
})









 add

     
      
      
      
      
    

同样的,我们也可以来改写一下全选的实现4的controller层

//controller逻辑层(可以写成类)
$(function(){
    var currentChecked = false;
    $.sub('selectAll',function(e,flag){
        currentChecked = flag;
        container.selectOrCacelAll(flag);
    });
    $.sub('addBox',function(e){
        container.add(currentChecked);
    });
    
    //selectAll实例
    var selectAll = new SelectAll({
        listeners : {
            onSelectAll :function(flag){
                $.pub('selectAll',[flag]);
            }
        }
    });
    //add实例
    var add = new Add({
        listeners : {
            onAdd : function(){
                $.pub('addBox');
            }
        }
    });
    //container实例
    var container = new Container({});
});

原生js实现,有很多种实现方法,这里我简单写了一个,匿名函数还是删不掉的(如果要实现的话,得弄个uid才行)。

var obj = (function(){
    return {
        topics : {},
        sub : function(topic,fn){
            if(!this.topics[topic]){
                this.topics[topic] = [];
            }
            this.topics[topic].push(fn);
        },
        pub : function(topic){
            var arr = this.topics[topic];
            var args = [].slice.call(arguments,1);
            if(!arr){
                return;
            }
            for(var i = 0, len = arr.length;i < len;i++){
                arr[i](args);
            }
        },
        unsub : function(topic,fn){
            var arr = this.topics[topic];
            if(!arr){
                return;
            }
            if(typeof fn !== 'function'){
                delete this.topics[topic];
                return;
            }
            for(var i = 0, len = arr.length;i < len;i++){
                if(arr[i] == fn){
                    arr.splice(i,1);
                }
            }
        }
    };
})();











六、观察者模式

提到observer,你能想到什么?

我最开始想到的美剧《危机边缘》里面那些大光头,说是每次重大历史事件里面都有个大光头,在观察着一切的发生。

很少有人会直接想到浏览器。其实我们的浏览器整整就是一个观察者模式。

先说几个概念:
你说是观察者模式,肯定有观察者吧,
那观察谁呢,即被观察者,我们姑且称其为目标吧。
观察你的什么呢?相貌吗?观察的是状态。
我观察你干啥呢,看看你有啥变化?根据你的状态来做我的事情。

这里还是拿那个全选的例子来说说:
目标 : 全选复选框
观察者:container里的复选框,和add按钮
状态:全选按钮是否选上
行为:你选,container我就选上,点击add时,添加选上的按钮,反之,反然。

那怎么实现呢?
这里有两种思路,我观察你,想看看你的状态,我可以到你那去看,还有一种就是你的状态变化时,你来告诉我。
说白了就是“取”和“送”的概念。
观察者模式是采用“送”的概念。
那也涉及到一个问题,既然是“送”,你怎么来找到我呢?那你得保存我给你的地址。

现在可以来看看定义了。
Observer(观察者模式):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
ok,我们先按照这种思路来实现一下
实现6

$(function(){

    //缓存三个元素
    var $selectAll = $('#selectAll');
    var $add = $('#add');
    var $container = $('#container');
    
    //目标保存观察者
    $selectAll.cbList = [];
    $selectAll.addBtn = $add;
    
    //目标通知函数,通知观察者更新
    $selectAll.notify = function(value){
        $.each(this.cbList,function(index,checkbox){
            checkbox.update(value);
        });
        this.addBtn.update(value);
    };
    
    
    $selectAll.click(function(){
        $selectAll.notify(this.checked);
    })
    $add.click(function(){
        addCheckbox($add.data('checked'));
    });
    
    //观察者add的更新方法
    $add.update = function(value){
        this.data('checked',value);
    }
    function addCheckbox(flag){
        var $cb;
        if(flag){
            $cb = $('');
        }else{
            $cb = $('');
        }
        //观察者复选框更新方法
        $cb.update = function(value){
            this[0].checked = value; 
        }
        
        //目标存入观察者
        $selectAll.cbList.push($cb);
        $('#container').append($cb);
    }
})








 add

     
    

看到这里,难免会想这里哪有观察者模式的“模式”影子啊。

下来就要抽出目标和观察者的抽象接口,然后才是真正观察模式,才能得以复用。

var ObserverPatten = (function(){
    var Subject = function(){
        this.observerlist = [];
    }; 
    Subject.prototype = {
        add : function(observer){
            this.observerlist.push(observer);
        },
        notify : function(state){
            var arr = this.observerlist;
            for(var i = 0, len = arr.length;i
    

因此全选的例子可以改成如下

实现7

$(function(){

    //缓存三个元素
    var $selectAll = $('#selectAll');
    var $add = $('#add');
    var $container = $('#container');
    
    //具体目标实现目标接口
    $selectAll.extend(new ObserverPatten.Subject);
    $selectAll.add($add);
    $selectAll.click(function(){
        $selectAll.notify(this.checked);
    })
    
    $add.click(function(){
        addCheckbox($add.data('checked'));
    });
    
    //观察者add实现观察者接口
    $add.extend(new ObserverPatten.Observer);
    
    //复写update方法
    $add.update = function(value){
        this.data('checked',value);
    }
    function addCheckbox(flag){
        var $cb;
        if(flag){
            $cb = $('');
        }else{
            $cb = $('');
        }
        
        //观察者复选框实现观察者接口
        $cb.extend(new ObserverPatten.Observer);
        
        //复写update方法
        $cb.update = function(value){
            this[0].checked = value; 
        }
        
        //目标存入观察者
        $selectAll.add($cb);
        $('#container').append($cb);
    }
})









 add

     
    

七、后记

写到这里,写了一周也没写完,真的好长。

回顾本文,主要就讲了一件事情,特意去模糊事件和函数在我们头脑中的概念区别。目的也只是让我们理解代码更轻松些。

至于后面说的发布订阅模式和观察者模式,也只是大概地说说,发订模式在全选这个例子实现,其实是其一种特殊情况,就是中介者模式。

订阅者也可以作为另一个订阅者的发布者。也就是说发布者和订阅者,可以多对多的。

至于发订是观察者的一个分支,也好理解的。全选那个例子,三个模块就是观察者,目标就是发布订阅实现里面的$({})。

这个东西,本来想展开的,想写多对多的例子。一看本文太长了,还是算了。

写文章确实不容易,不仅要考虑说出去的话,有没有歧义。还要为自己的话,负责的。尽量不误人子弟。

还有一个事情,比较麻烦。就是案例的设计,还得尽量不去copy网上类似的文章,免得别人说你抄的。真头疼的。

其实还有几个点,原本也想展开说说的。念在眉毛胡子一把抓,没有重点,留给以后吧。

本文完。

    原文作者:算法小白
    原文地址: https://juejin.im/entry/57fb0544128fe100546c26dd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞