jQuery 实战读书笔记之第六章:事件本质

理解浏览器事件模型

understandEventModel.html 代码:

<!DOCTYPE HTML>
<html>
    <head>
        <title>Understand Event Model</title>
        <meta charset="UTF-8" />
        <link rel="stylesheet" href="../css/main.css"/>
        <style>
            img
            {
                display: block;
                margin: auto;
            }
        </style>
    </head>
    <body>
        <img id="example" src="images/example.jpg" alt="A bolt of lightning"
            onclick="console.log('At ' + formatDate(new Date()) + ' BOOM!');" />
 
        <p>The output is printed on the console</p>

        <!-- 在 onsubmit 中调用返回 false 的方法不能阻止默认动作 -->
        <!-- <form >-->
        <form id="myForm" action="www.cnblogs.com" onsubmit="return false">
            <div>
                <label for="name">Name:</label>
                <input id="name" name="name" type="text" />
            </div>
            <div>
                <button type="submit">Click me!</button>
            </div>
        </form>
 
        <form id="myForm2" action="www.cnblogs.com" onsubmit="prevent()">
            <div>
                <label for="name2">Name:</label>
                <input id="name2" name="name2" type="text" />
            </div>
            <div>
                <button type="submit">Click me2!</button>
            </div>
        </form>

        <div id="dom2">
            <button id="multipleButton" type="button">multiple events</button>
        </div>
    
        <div id="firstLevel">
            <div id='secondLevel'>
                <input id="testButton" type="button" value="捕获和冒泡" />
            </div>
        </div>

        <script src="js/understandEventModel.js"></script>
    </body>
</html>

understandEventModel.js:

/*
DOM Level 用来表示实现 W3C DOM 规范的级别要求
DOM Level 0 Event Model,在这个模型背后,事件处理器通过把函数赋值给元素的属性来声明实现
*/
function formatDate(date) {
    return (date.getHours() < 10 ? '0' : '') + date.getHours() +
            ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes() +
            ':' + (date.getSeconds() < 10 ? '0' : '') + date.getSeconds() +
            '.' + (date.getMilliseconds() < 10 ? '00' : (date.getMilliseconds() < 100 ? '0' : '')) +
            date.getMilliseconds();
}

/*
id 为 example 的图片在 HTML 代码中直接在标签属性 onclick 中声明了一个 click 事件,这个属性值会被使用创建一个
匿名函数体,类似下面的代码,直接在标签属性中声明事件不是推荐的做法
*/
document.getElementById('example').onmouseover = function(event) {
    console.log(this);
    console.log('At ' + formatDate(new Date()) + ' Crackle!');

    //当触发事件处理器时,在所有兼容标准的浏览器里,一个名为 Event 的对象会作为第一参数传递给处理器,只有 IE9 以上,
    //Firefox,Chrome, Safari 和 Opera 支持, IE8 之前的浏览器可以通过自己的方式把 Event 对象传给全局的 event 属性
    event = event || window.event;

    //获取目标元素的引用,也就是触发事件的元素,必须通过兼容标准浏览器的 target 属性获取,但是旧的 IE 使用
    //的是 srcElement
    var target = event.target || event.srcElement;
};

 

/*
事件冒泡
目标元素有机会处理事件之后,浏览器的事件处理机制会检查元素的父元素是否包含此事件类型的处理器,如果有也会调用,再
依次检查爷爷元素,直到 DOM 树的顶部
*/
var elements = document.getElementsByTagName('*');  //得到页面上的所有元素

for(var i = 0; i < elements.length; i++){
    if(elements[i].id != 'example'){
        (function(current){
            current.onclick = function(event){
                event = event || window.event;
                var target = event.target || event.srcElement;

                console.log('At ' + formatDate(new Date()) +
                            ' For ' + current.tagName + '#' + current.id +
                            ' target is ' + target.tagName + '#' + target.id);
            }
        })(elements[i]);
    }
}


/*
要阻止事件的传播,在现代浏览器中可以调用 Event 实例的 stopPropagation() 方法
在旧的 IE 浏览器中可以设置 Event 实例的 cancelBubble 属性为 true 实现,现在很多新的浏览器也提供了该属性,尽管
这不是 W3C 标准(我觉得这个实践中还是不用的好,知道作用就好)
有些事件是默认动作,比如 form 的 submit,要取消这些语义动作,在新的浏览器调用 Event 实例的 preventDefault() 方法。旧的
IE 中没有此方法,设置 returnValue 属性的值为 false;另一种方法是处理器代码返回 false
<form name="myForm" onsubmit="return false;" ... >
*/

/*
被页面的 onsubmit 属性引用,但没有成功阻止默认的 submit 动作
*/
function doSubmit(){
    return false;
}


/*
阻止了默认的 submit 动作
*/
function prevent(){
    event = event || window.event;
    event.preventDefault();
}


/*
DOM Level 2 event model
DOM Level 0 event model 的缺点是每个元素每次只能注册一种特定类型的事件处理器,主要是因为属性用来存储
事件处理器函数的引用
DOM Level 2 event model 就是为了解决上面的问题而建立的。这里的事件处理器通过标准的元素方法而不是元素属性赋值
来实现,每个 DOM 元素都实现了一个 addEventListener() 方法。
addEventListener(eventType, listener, useCapture)
eventType: 要处理的事件类型,DOM 0 中使用的事件,比如 click
listener:表示元素事件处理器的函数
useCapture:布尔值
*/
var multipleButton = document.getElementById('multipleButton');

/*
这个 multipleButton 按钮在下面代码增加事件处理器之前,上面的代码中增加了 click 事件是处理函数,从结果可以看出,
这个处理函数有被保留,而且是作为第一个触发
event.stopPropagation() 随便写在哪个处理函数中效果都一样,不会事件冒泡到父元素
*/
multipleButton.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) + ' BOOM once!');
    //event.stopPropagation();
});

multipleButton.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) + ' BOOM twice!');
    //event.stopPropagation();
});

multipleButton.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) + ' BOOM thrice!');
    event.stopPropagation();
});

 

/*
DOM Level 2 event model 中的事件传播
事件首先从 DOM 树根结点向目标元素传播,然后从目标元素向 DOM 根节点冒泡传播。前面的阶段成为捕获阶段,后者
是冒泡阶段

addEventListener(eventType, listener, useCapture) 方法中的 useCapture 是用来标记函数作为何种处理器使用的,false 为
冒泡阶段处理器,true 为捕获阶段处理器,默认为 false
*/

/*function eventLog(curElement, event){
    event = event || window.event;
    console.log('At ' + formatDate(new Date()) +
    ' Capture for ' + curElement.tagName + '#' + curElement.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}*/

var firstLevel = document.getElementById('firstLevel');
var secondLevel = document.getElementById('secondLevel');
var testButton = document.getElementById('testButton');

firstLevel.onclick = function(){};
secondLevel.onclick = function(){};
testButton.onclick = function(){};

//捕获阶段运行
firstLevel.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) +
    ' Capture for ' + this.tagName + '#' + this.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}, true);

firstLevel.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) +
    ' Bubble for ' + this.tagName + '#' + this.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}, false);

secondLevel.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) +
    ' Capture for ' + this.tagName + '#' + this.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}, true);

secondLevel.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) +
    ' Bubble for ' + this.tagName + '#' + this.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}, false);

testButton.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) +
    ' Capture for ' + this.tagName + '#' + this.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}, true);

testButton.addEventListener('click', function(event){
    console.log('At ' + formatDate(new Date()) +
    ' Bubble for ' + this.tagName + '#' + this.id ) +
    ' target is ' + event.target.tagName + '#' +  event.target.id;
}, false);

jQuery 事件模型

jQueryEventModel.html:

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8" />
      <title>Events in jQuery Example - jQuery in Action, 3rd edition</title>
      <link rel="stylesheet" href="../css/main.css"/>
      <style>
         img
         {
            display: block;
            margin: auto;
         }
         #wrapper
         {
            border: 1px solid #3A5895;
            padding: 10px;
         }

         #address:focus
         {
            border: 3px solid #000000;
         }
         .outer
         {
            position: relative;
            width: 200px;
            height: 100px;
            border: 1px solid #000000;
            background-color: #55AA55;
         }

         .inner
         {
            width: 50%;
            height: 50%;
            margin: 0.5em auto;
            border: 1px solid #000000;
            background-color: #FFFF4F;
         }

         #outer2
         {
            margin-top: 2em;
         }
      </style>
   </head>
   <body>
      <img id="example" src="images/example.jpg" alt="A bolt of lightning"/>

      <p>
          <button id="btn1">Click me!</button>
          <button id="btn2">Don't click me!</button>
      </p>

      <ul id="myList"></ul>

      <p>
          <button id="btn">Does nothing</button>
          <button id="btn-attach">Attach handler</button>
          <button id="btn-remove">Remove handler</button>
      </p>

      <p>
          <div id="foo">
              Trigger part
          </div>
      </p>

      <div id="wrapper">
        <button id="btnTrigger">Click me!</button>
        <button id="anotherBtn">Trigger custom event</button>
        <input type="text" id="address" />
     </div>

     <div class="outer" id="outer1">
        Outer 1
        <div class="inner" id="inner1">Inner 1</div>
     </div>
     <div class="outer" id="outer2">
        Outer 2
        <div class="inner" id="inner2">Inner 2</div>
     </div>

    <p>
        <button id="btnTestNamespacing">Test Namespacing</button>
        <button id="btn-remove-by-namespacing">Remove by namespacing</button>
    </p>

      <p>The output is printed on the console</p>

      <script src="../js/jquery-3.2.1.js"></script>
      <script src="js/jQueryEventModel.js"></script>
   </body>
</html>

jQueryEventModel.js:

/*
jQuery Event Model
1. 为创建事件处理器提供统一方法
2. 允许每个元素、每个事件类型注册多个方法
3. 适用标准事件名称,如 click 或 mouseover
4. 传递 Event 实例作为第一个参数
5. 规范事件实例中最常用的属性
6. 为事件取消和阻止操作提供统一的方法

除了不支持捕获阶段,jQuery 事件模型几乎完美支持 DOM Level 2 Event Model
*/

function formatDate(date) {
    return (date.getHours() < 10 ? '0' : '') + date.getHours() +
            ':' + (date.getMinutes() < 10 ? '0' : '') + date.getMinutes() +
            ':' + (date.getSeconds() < 10 ? '0' : '') + date.getSeconds() +
            '.' + (date.getMilliseconds() < 10 ? '00' : (date.getMilliseconds() < 100 ? '0' : '')) +
            date.getMilliseconds();
 }

 /*
on(eventType[, selector][, data], handler)
on(eventHash[, selector][, data])
为选择的元素的一个或多个事件添加一个或多个事件处理器

eventType(String):事件类型的名称,多个事件类型可以用空格分隔
selector(String):过滤器用来过滤选中元素的子元素,这些子元素触发事件
data(Any):传递给 Event 实例的数据,赋值给 data 属性
handler(Function):事件处理器的函数,冒泡阶段的当前元素作为函数上下文,false 值表示函数 return false

eventHash(Object):单个调用中为多个事件类型建立处理器的对象。属性名区分事件类型,属性值提供事件处理器

返回 jQuery 集合
 */
 $('#example')
         .on('click', function (event) {
            console.log('At ' + formatDate(new Date()) + ' BOOM once!');
         })
         .on('click', function (event) {
            console.log('At ' + formatDate(new Date()) + ' BOOM twice!');
         })
         .on('click', function (event) {
            console.log('At ' + formatDate(new Date()) + ' BOOM thrice!');
         })
         .on('mouseenter mouseleave', function(event){
             console.log(event.type)
         });

$('#btn1').on('click', function(){
    console.log('The button is clicked!');
}).on('mouseenter mouseleave', myFunctionHandler);

/*
使用 eventHash 参数类型的代码
*/
$('#btn2').on({
    click: function(){
        console.log('Oh no, you clicked me!');
    },
    mouseenter: myFunctionHandler,
    mouseleave: myFunctionHandler
});

function myFunctionHandler(event){
    event.stopPropagation();
    console.log(event.type + ' ' + event.target.id);
}

/*
注意这里的第二个参数,设置了 data 参数后,可以通过 event 参数的 data 属性访问
*/
$('#btn1').on('click', {
    name: 'Martin_Fu'
}, function(event){
    console.log(event.data.name + ' clicked the button!');
});

/*
事件委托(event delegation)
这是一种向元素的父元素添加事件处理器的重要技术,可以为不存在的元素添加事件处理器
*/
$('<li>item1</li>').add($('<li>item2</li>')).appendTo('#myList');
/*
原生 JS 写法
*/
document.getElementById('myList').addEventListener('click', function(event){
    if(event.target.nodeName === 'LI'){
        console.log('List item: ' + 
            (Array.prototype.indexOf.call(document.getElementById('myList').children, event.target) + 1));
    }
});

/*
这里注意第二个参数,它会对子元素进行筛选
事件委托的优势不只局限于为不存在的元素执行事件处理器,更可以节省内存和时间。比如 myList 下有很多 <li> ,那么需要循环
添加事件处理器,如果 <li> 元素很多,那么会耗费不少时间,由于 JS 是单线程的,会导致不好的用户体验。
但也不能因为这个原因给一个元素(比如 document )添加过多处理器,同样影响性能,建议尽可能为离目标元素近的元素添加处理器
*/
$('#myList').on('click', 'li', function(event){
    console.log('List item(jQuery): ' + ($(this).index() + 1));
});


/*
one(eventType[, selector][, data], handler)
one(eventHash[, selector][, data])
为选择的元素的一个或多个事件添加一个或多个事件处理器,一旦执行,事件处理器会自动删除

返回 jQuery 集合
 */



/*
off(eventType[, selector][, data], handler)
off(eventHash[, selector][, data])
删除参数指定的 jQuery 对象中所有元素的事件处理器,如果没有提供参数,那么删除所有的元素处理器

返回 jQuery 集合
 */
/*
这里注意一点,如果多次点击 btn-attach 按钮,btn 按钮会添加两个 click 事件,但是只要点击一次
btn-remove 按钮,这两个事件都会移除
*/
var $btn = $('#btn');
var counter = 1;

function logHandler(event){
    console.log('Click ' + counter);
    counter++;
}

$('#btn-attach').on('click', function(event){
    $btn.on('click', logHandler).text('Log');
});

$('#btn-remove').on('click', function(event){
    $btn.off('click', logHandler).text('Does nothing');
});


/*
触发事件
有时候,系统使用脚本控制事件的触发和执行过程
调用作为处理器的函数不会导致语义动作或者冒泡传播发生
*/
/*
trigger(eventType[, data])
为所有匹配的元素调用传递事件类型建立任意事件处理器和行为,全力模拟事件被触发,包括 DOM 层级传播和语义动作执行

eventType(String|jQuery.Event):指定要调用的事件类型的名字,也可以包含命名空间;还可以传递 jQuery.Event
data(Any):传递给处理器的数据,如果是数组,传递的元素会作为不同的参数

返回 jQuery 集合
 */
$('#foo').on('click', function(event, par1, par2, par3){
    console.log(event.type + "_" + event.target.id);
    console.log(par1, par2, par3);
});

/*
这里要注意的是参数传递的是数组
*/
$('#foo').trigger('click', [1, 2, 3]);

/*
triggerHandler(eventType[, data])
为匹配的元素(只是集合中的第一个元素)调用建立的事件处理器,没有出现冒泡传播、语义动作或者实时事件

eventType(String|jQuery.Event):指定要调用的事件类型的名字,也可以包含命名空间;还可以传递 jQuery.Event
data(Any):传递给处理器的数据,如果是数组,传递的元素会作为不同的参数

返回最后一个处理器的返回值,如果没有返回值,返回 undefined
 */
$('#wrapper')
    .on('focus', function(event){
        console.log('Div focused!');
    })
    .on('click', function(event){
        console.log('Div clicked!');
    });

$('#address')
    .on('focus', function(event){
        console.log('Input focused!');
    })
    .on('click', function(event){
        console.log('Input clicked!');
    }).triggerHandler('focus');

$('#btnTrigger')
    .on('click', function(event){
        console.log('Button clicked!');
    })
    .trigger('click');


/*
快捷方式
编写 on() 和 off() 完整语法很多时候会变得烦人,因此 jQuery 提供了快捷方式
eventName([data, ] handler)
通过方法名为指定事件使用的函数建立事件处理器,支持的方法如下:
blur        focusout        mouseleave      resize
change      keydown         mousemove       scroll
click       keypress        mouseout        select
dbclick     keyup           mouseover       submit
focus       mousedown       mouseup
focusin     mouseenter      ready

data(Any):作为 Event 实例的 data 属性传递给处理器的数据
handler(Function):作为事件处理器的函数

返回 jQuery 集合
*/
$('#btnTrigger')
    .dblclick(function(){
        console.log('Button double clicked!');
    })
    .dblclick();


/*
hover(enterHandler, leaveHandler)
hover(handler)
为匹配元素的 mouseenter 和 mouseleave 事件建立处理器

enterHandler(Function):作为 mouseenter 处理器的函数
leaveHandler(Function):作为 mouseleave 处理器的函数
handler(Function):对于 mouseenter 和 mouseleave 事件都会调用的单个处理器
*/
function report(event) {
    event.stopPropagation();
    console.log(event.type + ' on ' + event.target.id);
 }

$('#outer1').on('mouseenter mouseleave', report);
$('#inner1').on('mouseenter mouseleave', report);
$('#outer2').hover(report);
$('#inner2').hover(report);

/*
自定义事件
*/
$('#btnTrigger').on('customEvent', function(){
    console.log('Custom event invoked!')
});

$('#anotherBtn').click(function(){
    $('#btnTrigger').trigger('customEvent');
});


/*
为事件添加命名空间
事件是后缀命名空间,用圆点分割
*/
$('#btnTestNamespacing')
    .on('click.editMode.myApp', function(){
        console.log('Click event under editMode namespacing invoked!');
    })
    .on('mouseenter.editMode', function(){
        console.log('Mouseenter event under editMode namespacing invoked!');
    })
    .on('mouseleave.myApp', function(){
        console.log('Mouseleave event under editMode namespacing invoked!');
    });

$('#btn-remove-by-namespacing').click(function(){
    $('#btnTestNamespacing').off('.editMode');
})