大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: Javascript入门 修改讲义

复习:事件:当(某个)按钮点击 时,弹出提示框……

再次演示时注意强调JavaScript代码的位置。因为浏览器对HTML文档的解析是从上往下依次执行的,所以JavaScript代码运行的时候一定要确保相应的DOM元素已经被浏览器解析。

事件组成

深入学习事件,首先要了解一些基本概念:

  • 类型(type):什么类型的事件,比如:
    • 鼠标点击(click)
    • 键盘按键被按下(keydown)
    • 元素(被浏览器)渲染加载(load)
    • ……
  • 处理程序(handler/listener):,在事件被触发后调用的回调函数
  • 注册(register)/ 绑定(bind):即把事件“处理程序”和“源”联系起来的过程
  • 源(source):事件是源自于由哪一个DOM元素,或者哪一个DOM元素(不是“用户”)触发(trigger/target)了事件,或者事件被绑定到的目标DOM元素(target

事件本身不是JavaScript语言的一部分,而是由浏览器提供的接口和底层支持。只是JavaScript可以调用这些事件而已,但是因为事件的应用如此广泛,我们有时候又说“JavaScript(编程)是事件驱动的”……


绑定方式

HTML行内

如前所示。

如果代码非常简单且不要求重用的话,可以直接使用JavaScript的源代码:

<h1 onclick="alert(`先学习后付费`)">源栈欢迎您!</h1>

一般认为(但不绝对)这种行内的写法不好,因为这样就(同HTML与CSS的混杂一样)把HTML内容(呈现)和JavaScript(实现)混在一起了,没有满足“隔离原则”所以应该在:

JavaScript代码块

中,完成事件绑定。具体又分为两种:

on<EventType>

document.getElementsByTagName('h1')[0].onclick = welcome;  //注意这里不能带圆括号和参数

看起来和在HTML元素中添加属性差不多,一样可以直接使用匿名函数。

注意

  1. greet后面没有圆括号,即:是把(回调)函数本身,而不是函数运行结果传递给onclick事件——这个逻辑是OK的。 (那为什么前面HTML中就可以呢?咳咳……不要问,问就是JavaScript“真有趣”)
  2. 确保绑定事件的JavaScript代码运行之前,DOM已加载,否则找不到元素就会报错

addEventListener()方法

按W3C推荐,应首先使用addEventListener(type, listener[, options])

document.getElementsByTagName('h1')[0].addEventListener('click', greet);

这里的事件类型又没有on的前缀了,(有的IDE)也没有智能提示,而且是大小写敏感的……折腾啊,o(╥﹏╥)o

查看事件

用JavaScript代码绑定的事件,可以在浏览器上F12查看。

演示:先找到DOM元素,然后


其他

模拟事件执行

绝大多数浏览器都支持在元素对象上直接调用事件方法来模拟触发相应的事件,比如:
 document.getElementsByTagName('h1')[0].click();

这和用户用鼠标点击h1元素产生相同的效果,即h1上绑定的onclick事件回调函数会被触发。

解除绑定

如果说“注册”,对应的是“退订”。

有两种方式:

  • 使用addEventListener绑定的,用removeEventListener():
    document.getElementsByTagName('h1')[0].removeEventListener("click", welcome)
    必须是两个参数,即需要指定解除某事件的哪一个handler。
    这说明使用addEventListener(),是可以为同一个DOM元素,添加多个handler的!(演示)
  • 其他方式(行内和Javascript块中)绑定的,“属性”设置为null
    document.getElementsByTagName('h1')[0].onclick = null; 

选择适用

主要还是看老大的喜好。

  • 行内:可读性高,没有隔离
  • on<EventType>和addEventListener():反之

另:addEventListener()在事件冒泡中还有一些特殊性。


this和event

当我们在

JavaScript块

中绑定事件时:

在事件调用函数中可使用 this 指向事件源,即:触发事件的DOM/BOM对象。

使用this,可以简化代码。比如,我们要点击切换颜色:

document.getElementsByTagName('h1')[0].addEventListener("click", function(){
    this.style.color = "blue";
});
同时,事件调用函数可以带一个event参数(但IE中有全局变量window.event):
document.getElementsByTagName('a')[0].addEventListener("click", function(event){
  • 属性type、target 等(断点演示
    event.target === this    //true,触发事件的DOM元素 event.type    // "click"
  • 方法:event.preventDefault() 和event.stopPropagation()  vs return false;
    event.preventDefault();   //组织DOM元素默认的行为

行内绑定

不一样:

  1. 无法直接使用this(演示)
  2. 可以传递event
    <a onclick="welcome(event)" > 源栈欢迎您!</a>
    //对应接收
    function welcome(e){
    此时可以使用event.target做this用
    event.target.style.color = "red";
  3. 也可以直接传递this,不传递event:
    <a onclick="welcome(this)" > 源栈欢迎您!</a>
    function welcome(element){
        element.style.color = "red";
    但在(现代浏览器中的)回调函数中仍能使用event,这时候的event是window的属性。


冒泡和捕获

一般的事件处理不需要考虑这种情况。但是,这不仅是一个常见面试题,而且有其实际使用场景。

演示准备

  1. 有父子两个元素
        <div id="propagate">
            <p>
                源栈欢迎您
            </p>
        </div>
    为了便于演示,加上一点CSS效果:
        <style>
            #propagate {
                padding: 30px;
                border: 1px solid;
            }
    
                #propagate > p {
                    background-color: blue;
                    border: 1px dashed;
                }
        </style>
  2. 都注册了事件
    let father = document.getElementById('propagate'),
        child = father.children[0];
    father.addEventListener('click', function () {
        console.log('event handler on father');
    });
    child.addEventListener('click', function () {
        console.log('event handler on child');
    });
  3. 用户点击在子元素上

@想一想@

  1. 这是不是也同时点在了父元素上?
  2. 所以,是不是一次事件就会调用两个handler?
  3. 那么,先触发的是子元素,还是父元素呢?
演示:略

历史和规范

浏览器大战(复习)的年代,不同的浏览器使用了不同的机制来处理这种场景

  • 冒泡(bubble):事件从内向外传递,先触发子元素事件,再触发父元素的
  • 捕获(capture):事件从外向内传递,先触发父元素事件,再触发子元素的

现代浏览器都按W3C标准进行了统一。

当事件行为发生时,有两个“传播”阶段:首先由外向内“捕获”,然后再从内向外“冒泡”,如下图所示。

addEventListener()控制

那为什么上述演示的结果感觉是只有“冒泡”呢?

因为我们在绑定事件时(默认)指明了:该事件处理程序只适用/响应于冒泡阶段。

addEventListener()还有第三个参数useCapture,当其为

  • false:事件绑定在冒泡阶段(默认)
  • true:事件绑定在捕获阶段(注:唯一引入捕获的方式,其他都是冒泡)

演示:

father.addEventListener('click', function () {
    console.log('event handler on father');
}, true);
child.addEventListener('click', function () {
    console.log('event handler on child');
}, true);
以及:父true子false,父false子true

#理解#:上述(由浏览器控制的)①②③④四个阶段始终是存在的,我们能控制的是:事件处理程序(handler)在哪一个阶段(phase)予以响应。

stopPropagation()

可以想象,事件冒泡/捕获机制极有可能会带来混乱,所以有时候我们需要:阻止事件(行为)继续传递,这就可以调用:

event.stopPropagation();

注意:propagation包含冒泡和捕获

演示:采用冒泡/捕获,在父子元素事件handler中添加stopPropagation()

#常见面试题#

问:stopPropagation()和preventDefault()的区别?

  • 联系:都是event方法,都能阻止事件发生
  • 区别:
    1. preventDefault()阻止的DOM元素默认的(不是我们JavaScript开发人员绑定的)事件,如提交按钮的提交,超链接的跳转……
    2. stopPropagation()阻止的不是事件本身,是事件的传播,通过阻止事件传播阻止JavaScript开发人员的绑定事件

两个Target

现在我们来看一个真实的适用场景:给一个父元素(div)里面的所有子元素(p)绑定click事件。

    <div id="propagate">
        <p>
            源栈欢迎您
        </p>
        <p>
            大神小班
        </p>
        <p>
            灵活学制
        </p>
    </div>

利用冒泡机制,我们就不需要在每一个p标签上添加事件了:

for (let i = 0; i < father.length; i++) {
    father[i].onmouseover = function(){
        console.log(this.innerText);
    };
}

而是在div上绑定事件。

但是,click事件处理程序中要求显示用户所点击DOM元素的文本,怎么办?@想一想@:能不能用this?

实际上,这里面可以使用的有三个对象:

  • event.currentTarget(等同于this):JavaScript代码中绑定事件的DOM元素
  • event.target:(用户)点击的DOM元素
father.addEventListener('click', function (event) {
    console.log('this === event.target:' + (this === event.target));
    console.log('event.target:' + event.target.innerHTML);  //符合题意需求
    console.log('this === event.currentTarget:' + (this === event.currentTarget));
    console.log('event.currentTarget:' + event.currentTarget);
});
@试一试@:当在父子元素上都绑定了事件之后,又各自使用冒泡/捕获,会是一种什么情形?


作业

本章作业名为:event.html 

  1. 用代码和PPT演示:事件的冒泡和排序传播
  2. 完成DOM第2题
  3. 利用一个文本输入框和一个按钮,完成猜数字的游戏:
    1. 进入页面会弹出游戏玩法说明,等待用户点击“确认”,开始游戏;
    2. 浏览器生成一个不大于1000的随机正整数;
    3. 用户在文本框输入猜测的值:如果用户输入的不是正整数,弹出提示框,并要求用户重新输入
    4. 如果用户没有猜对,浏览器比较后告知结果:“大了”或者“小了”。如果用户:
      • 只用了不到6次就猜到,弹出:碉堡了!
      • 只用了不到8次就猜到,弹出:666!
      • 用了8-10次猜到,弹出:猜到了。
      • 用了10次都还没猜对,弹出:^(* ̄(oo) ̄)^
学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 Javascript入门 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码