本文内容

  • 事件基础
  • 事件监听方式
  • 事件默认行为
  • 事件冒泡与事件捕获
  • 事件绑定与事件委托

事件基础

注意:本文不会深入探究Javascript的事件循环。

提到事件,相信每位Javascript开发者都不会陌生,由于Javascript是先有实现,后有规范,因此,对于大部分人来说,事件模块可以说是比较模糊的,本文将从不同角度帮助你理清楚事件模块。

事件的本质可以说是一个回调函数,当事件触发时会调用你的监听函数。

事件是一定会触发的,如果没有对应的监听函数,就不会执行回调。

比如下面就是用户点击指定元素打印日志的例子:

document.querySelector('#button').onclick = function() {
console.log('clicked');
};

事件基础相信大家都没什么问题,重点在后面的内容。

事件监听方式

由于历史原因,Javascript目前存在三种事件监听方式:

  1. HTML代码中监听
  2. DOM0级监听
  3. DOM2级监听

Q: 为啥从DOM0级开始?

1998年,W3C综合各浏览器厂商的现有API,指定了DOM1标准。在DOM1标准出现之前浏览器已有的事件监听方式叫做DOM0级。

Q:DOM1级监听到哪里去了?

由于DOM1标准只是对DOM0标准的整理+规范化,并没有增加新的内容,因此DOM0级可以看做DOM1级。

HTML代码监听

<button onclick="alert('Hello World!')">点我</button>

直接将事件处理函数或事件处理代码写到HTML元素对应的属性上的方式就是HTML代码监听方式

该方式有一个明显的缺点,如果事件逻辑比较复杂时,将大段代码直接写在HTML元素上不利于维护。因此一般会提取到一个专一的函数进行处理。

<button onclick="callback()">点我</button>

该方式也有一个问题,那就是如果callback()函数还未加载好时点击按钮将报错。而且直接将事件耦合到HTML元素上也不符合单一职责,HTML元素应该只负责展示,不负责事件。

不建议在开发中使用该方式处理事件。

DOM0级事件监听

在DOM1级规范出来之前,各浏览器厂商已经提供了一套事件API,也就是DOM0级API,它的写法如下:

<button id="click">点我</button>
<script>
document.querySelector('#click').onclick = function() {
console.log('clicked');
};
</script>

这个相信大家在刚开始入行时写的比较多,比如我们的ajax相关API就是DOM0级的。

var xhr = new XMLHttpRequest();
xhr.onload = function() {};
xhr.onerror = function() {};

DOM0级事件基本上都是以"on"开头的

DOM0级事件也存在一个问题,那就是不支持添加多个事件处理函数,因此只有在不支持DOM2级事件的情况下才会使用DOM0级来绑定事件。

DOM2级事件监听

DOM2级事件是最新的事件处理程序规范(有许多年未更新了)。DOM2级事件通过addEventListener方式给元素添加事件处理程序。

<button id="click">点我</button>
<script>
document.addEventListener('click', function(){
console.log('clicked');
});
</script>

多次调用addEventListener可以绑定多个事件处理程序,但是需要注意:

同样的事件名、同样的事件处理函数和同样的事件流机制(冒泡和捕获,下面会讲到),只会触发一次

// 下面的代码只会触发一次
<button id="request">登录</button>
<script>
function onClick() {
console.log('clicked');
}
document.querySelector('#request').addEventListener('click', onClick, false);
document.querySelector('#request').addEventListener('click', onClick, false);
</script>

onClick是同一个事件处理程序,所以只触发一次

// 下面的代码只会触发两次
<button id="request">登录</button>
<script>
document.querySelector('#request').addEventListener('click', function() {
console.log('clicked');
}, false);
document.querySelector('#request').addEventListener('click', function() {
console.log('clicked');
}, false);
</script>

两个匿名函数,所以会触发两次

事件默认行为

很多网页元素会有默认行为,比如下面这些:

  • 点击a标签的时候,会有跳转行为
  • 点击右键时会弹出菜单
  • 在表单中点击提交按钮会提交表单

如果我们需要阻止默认行为,比如我们在阻止表单的默认提交事件,进行数据校验,通过校验后再调用表单submit方法提交。

不同的监听方式阻止默认行为的方式也不同。

HTML代码方式

HTML代码方式支持return false和event.preventDefault()

return false方式

<form action="" onsubmit="return handleSubmit()">
<button type="submit">Submit</button>
</form>
<script>
function handleSubmit() {
return false;
}
</script>

上例中我们监听了表单的onsubmit事件,当点击按钮或者按下回车时,将会触发handleSubmit方法,同时会阻止表单的提交。

表单内如果有type="submit"的按钮存在,按下回车时就会自动提交。

HTML监听方式阻止默认事件需要满足以下两点:

  1. HTML事件监听代码return handler()return不能少,少了就无法阻止默认行为
  2. handler()函数需要返回false

event.preventDefault()

<a href="https://www.ddhigh.com" onclick="handleClick(event)" id="click">Href</a>
<script>
function handleClick(e) {
e.preventDefault();
}
</script>

DOM0级事件方式

DOM0级事件支持return false和event.preventDefault()两种方式。

event.preventDefault()

// event.preventDefault()
<a href="https://www.ddhigh.com" id="click">Href</a>
<script>
document.querySelector('#click').onclick= function (event) {
event.preventDefault();
};
</script>

return false

// return false
<a href="https://www.ddhigh.com" id="click">Href</a>
<script>
document.querySelector('#click').onclick= function (event) {
return false;
};
</script>

两种方式都能工作,不过建议使用event.preventDefault(),原因在下面DOM2级会讲到

DOM2级事件

DOM2级事件事件只支持event.preventDefault()方式,这也是事件的标准处理方法。

<a href="https://www.ddhigh.com" id="click">Href</a>
<script>
document.querySelector('#click').addEventListener('click', function (e) {
e.preventDefault();
});
</script>

事件冒泡与事件捕获

先来看一个HTML结构

<div id="father">
<div id="child">
<div id="son">Click</div>
</div>
</div>

我们知道,一旦绑定了事件处理程序,在事件触发时,事件处理函数都会触发。

如果我们给father/child/son都绑定了事件处理函数,点击了son时,谁被触发呢?

事实上,三个函数都会被触发,因为son时child的子元素,child又是father的子元素,点击son,同时也点击了father和child。

由此带来一个问题,三个函数谁先触发,谁后触发呢?这就是我们常说的事件流,father->child->son这种路径是可以的,但是son->child->father这种路径也是可以的。

针对这两种方式,W3C给了我们一个答案,两种方式都支持,即可以从父元素到子元素,又可以从子元素到父元素,前者叫事件捕获,后者叫事件冒泡

事件捕获

事件发生时采取自上而下的方式进行触发,最先触发的是window,其次是document,然后根据DOM层级依次触发,最终进入到真正的事件元素。

addEventListener第三个参数传入true就是捕获方式的标志。

<div id="father">
<div id="child">
<div id="son">Click</div>
</div>
</div> <script>
document.querySelector('#father').addEventListener('click', function () {
console.log('father');
}, true);
document.querySelector('#child').addEventListener('click', function () {
console.log('child');
}, true);
document.querySelector('#son').addEventListener('click', function () {
console.log('son');
}, true);
</script>

点击son之后的输出顺序为

father
child
son

事件冒泡

事件发生时采取自下而上的方式进行触发,最先触发的是发生事件的元素,其次是父元素,依次向上,最终触发到documentwindow

addEventListener第三个参数传入false就是捕获冒泡的标志。

<div id="father">
<div id="child">
<div id="son">Click</div>
</div>
</div> <script>
document.querySelector('#father').addEventListener('click', function () {
console.log('father');
}, false);
document.querySelector('#child').addEventListener('click', function () {
console.log('child');
}, false);
document.querySelector('#son').addEventListener('click', function () {
console.log('son');
}, false);
</script>

点击son之后的输出顺序为

son
child
father

由于事件捕获和事件冒泡机制,我们需要一个标记来标识真正触发事件的元素,这个元素就是event.target,而另外一个相似的属性叫event.currentTarget,这是当前元素。

事件捕获和时间冒泡的顺序

根据浏览器规范,事件捕获会先于事件冒泡发生。因此,总的事件顺序如下

  1. window 捕获阶段
  2. document 捕获阶段
  3. ... 依次到真正触发事件的元素 捕获阶段
  4. 真正触发事件的元素 冒泡阶段
  5. 依次向上的父元素 冒泡阶段
  6. document 冒泡阶段
  7. window 冒泡阶段
<div id="father">
<div id="child">
<div id="son">Click</div>
</div>
</div> <script>
document.querySelector('#father').addEventListener('click', function () {
console.log('father捕获');
}, true);
document.querySelector('#child').addEventListener('click', function () {
console.log('child捕获');
}, true);
document.querySelector('#son').addEventListener('click', function () {
console.log('son捕获');
}, true);
document.querySelector('#father').addEventListener('click', function () {
console.log('father冒泡');
}, false);
document.querySelector('#child').addEventListener('click', function () {
console.log('child冒泡');
}, false);
document.querySelector('#son').addEventListener('click', function () {
console.log('son冒泡');
}, false);
</script>

点击son之后的输出为

father捕获
child捕获
son捕获
son冒泡
child冒泡
father冒泡

事件绑定和事件委托

弄明白浏览器的事件流机制之后,来讨论事件绑定和事件委托其实是很简单的事情。

事件绑定

就是在事件监听方式中直接对具体元素进行事件监听的方式。有个明显的缺点,对于新增加的DOM节点是无法监听到事件的。

    <div class="a">click1</div>
<div class="a">click2</div>
<script>
document.querySelectorAll('.a').forEach(ele => ele.onclick = function () {
console.log('clicked ' + this.innerHTML);
});
setTimeout(function () {
const div3 = document.createElement('div')
div3.className = "a";
div3.innerHTML = "click3"
document.body.appendChild(div3)
}, 500);
</script>

上面的click3点击是没有任何反应的,因为在创建该元素时没有绑定事件处理函数。

事件委托

我们利用事件流机制来实现上面的需求。

事件委托就是利用事件流机制,在父元素进行监听,由于事件冒泡机制,父元素可以接受新添加元素的事件。

    <div class="a">click1</div>
<div class="a">click2</div>
<script>
document.body.addEventListener('click', function (e) {
console.log(e.target.innerHTML)
}, false);
setTimeout(function () {
const div3 = document.createElement('div')
div3.className = "a";
div3.innerHTML = "click3"
document.body.appendChild(div3)
}, 500);
</script>

由于事件冒泡机制,click3元素点击之后会将事件冒泡给父元素,也就是我们的document.body,通过event.target可以拿到真正触发事件的元素。

(完)

Javascript事件系统的更多相关文章

  1. javaScript事件系统详解

    一个有情怀的猴子

  2. JavaScript中事件处理

    先看看下面一道题目,请评价以下代码并给出改进意见: if (window.addEventListener) {//标准浏览器 var addListener = function(el, type, ...

  3. 关于编写性能高效的javascript事件的技术

    如何能做出高效的web前端程序是我每次做前端开发都会不自觉去考虑的问题.几年前雅虎里牛逼的前端工程师们出了一本关于提升web前端性能的书籍,轰动了整个web开发技术界,让神秘的web前端优化问题成为了 ...

  4. 【转】关于编写性能高效的javascript事件的技术

    原文转自:http://blog.jobbole.com/80170/ 如何能做出高效的web前端程序是我每次做前端开发都会不自觉去考虑的问题.几年前雅虎里牛逼的前端工程师们出了一本关于提升web前端 ...

  5. 偶的《javascript框架设计》终于出版

    #cnblogs_post_body p{ text-indent:2em!important; } 历时两年多,我的书终于付梓出版了.应各方面的要求,写软文一篇,隆重介绍一下此书对各位程序员的钱途有 ...

  6. 大型 JavaScript 应用架构中的模式

    原文:Patterns For Large-Scale JavaScript Application Architecture by @Addy Osmani 今天我们要讨论大型 JavaScript ...

  7. JS读书心得:《JavaScript框架设计》——第12章 异步处理

    一.何为异步   执行任务的过程可以被分为发起和执行两个部分. 同步执行模式:任务发起后必须等待直到任务执行完成并返回结果后,才会执行下一个任务. 异步执行模式:任务发起后不等待任务执行完成,而是马上 ...

  8. [转]大型 JavaScript 应用架构中的模式

    目录 1.我是谁,以及我为什么写这个主题 2.可以用140个字概述这篇文章吗? 3.究竟什么是“大型”JavaScript应用程序? 4.让我们回顾一下当前的架构 5.想得长远一些 6.头脑风暴 7. ...

  9. 【温故而知新-Javascript】使用 Document 对象

    Document 对象时通往DOM功能的入口,它向你提供了当前文档的信息,以及一组可供探索.导航.搜索或操作结构与内容的功能. 我们通过全局变量document访问Document对象,它是浏览器为我 ...

随机推荐

  1. React Native错误汇总(持续更新)

    错误1 Element type is invalid-: 错误描述: Element type is invalid: expected a String(for built-in componen ...

  2. Python基础:04映射类型

    字典是Python语言中唯一的映射类型.一个字典对象是可变的,它是一个容器类型,能存储任意个数的Python对象.字典中的数据是无序排列的. 映射类型也可被称做哈希表,哈希表的算法是获取键,对键执行一 ...

  3. linux之docker

    简介 Docker是一个开源的应用容器引擎,基于go语言,遵循从Apache2.0协议开源 Docker可以让开发者打包他们的应用以及依赖包到一个轻量级,可移植的容器中,然后发布到任何流行的linux ...

  4. python的if循环和嵌套

    1.    if 条件:    if语句块   执行流程:判断条件是否为真. 如果真. 执行if语句块 money = int(input('请输入你兜里的钱:')) if money >500 ...

  5. 基于TableStore的海量气象格点数据解决方案实战

    前言 气象数据是一类典型的大数据,具有数据量大.时效性高.数据种类丰富等特点.气象数据中大量的数据是时空数据,记录了时间和空间范围内各个点的各个物理量的观测量或者模拟量,每天产生的数据量常在几十TB到 ...

  6. Java中Map/List/Set .

    很实用,分享一下. 简单版本 复杂版本 参考: http://initbinder.com/articles/cheat-sheet-for-selecting-maplistset-in-java. ...

  7. CSS属性Display(显示)和Visibility(可见性)

    隐藏一个元素可以通过把display属性设置为“none”,或把visibility属性设置为“hidden”.但是请注意,这两种方法会产生不同的效果. Visibility:hidden可以隐藏某个 ...

  8. 学习框架/对象onbeforeunload事件

    1.有没有发现在一些表单提交页面,当我们点击页面某个链接要离开表单提交页面时会弹出一个提示信息,如下: 那么,这是提示是怎么产生的呢?代码如下: window.onbeforeunload=funct ...

  9. echarts细节的修改(2):矩形数图,柱状图,折线图,雷达图等

    1.矩形数图的配置,是直接拿饼图的配置 然后将type换成treemap. 修改类型 option.series.type = 'treemap'; 关闭面包屑导航 option.series.bre ...

  10. js切割字符串

    var time_str= '2019-9-10 13:18:20'; var t = time_str.substr(2,8);   console.log(t);   输出  19-9-10