上篇文章我们提到,注册事件的方法 addEventListener()内有三个参数,分别是「事件名称」、「事件的处理程序」,以及「捕获」或「冒泡」的机制切换。

那么,今天我们要来看的第一个部分,就是隐藏在事件处理程序中的event

隐藏在监听函数中的”event”

当监听的事件发生时,浏览器会去执行我们通过addEventListener()注册的事件处理程序函数。

这个时候,EventListener 会去创建一个「事件对象」 (Event Object),里面包含了所有与这个事件相关的属性,并且以「参数」的形式传给我们的处理程序函数:

<button id="btn">Click</button>
......
var btn = document.getElementById('btn');

// 参数 e 就是上面所说的事件对象
// 因为是参数,当然可以自己定名称
btn.addEventListener('click', function(e){
  console.log(e);
}, false);

当点击<button>后,可以从console看到event对象中提供了这么多东西:

像是

  • type : 表示事件的名称
  • target : 表示触发事件的元素
  • bubbles:表示这事件是否是在「冒泡」阶段触发( true/ false)
  • pageX/ pageY:表示事件触发时,鼠标座标在网页的相对位置

其余的属性这里就不一一介绍,不过要注意的是,每个「事件对象」所提供的属性都会根据触发的事件而稍微不同。

event. preventDefault()

HTML 中部分元素会有默认行为,像是<a>标签默认页面跳转或是锚点定位,或是表单的submit等等…

如果我们需要在这些元素上绑定事件,那么适当地取消它们的默认行为就是很重要的一件事。

比如,有一个通往 baidu 的链接<a>:

<a id="link" href="https://www.baidu.com">百度</a>

假设今天点击这个 link 时,我希望浏览器执行console.log('米淇淋你好帅!');那么根据先前所说,我可以先注册click事件:

var link = document.querySelector("#link");

link.addEventListener(
  "click",
  function (e) {
    console.log("米淇淋你好coll!");
  },
  false
);

结果你却发现,即便我们在<a>中去注册了click事件,但是当我点击这个 link 的时候,浏览器开始会 console.log 出”米淇淋你好帅!”,但最后 baidu 的网页依旧会覆盖我想要的内容。

可是我希望执行的是console.log('米淇淋你好帅!');而不是直接把我带偏了去到 baidu 的网站,那么我们该怎么做,才能避免呢?

这时候如果调用event.preventDefault()方法,默认事件行为将不再触发

var link = document.querySelector("#link");

// 在 事件处理函数中 加上 e.preventDefault();
link.addEventListener(
  "click",
  function (e) {
    e.preventDefault();
    console.log("米淇淋你好帅!");
  },
  false
);

这个时候,再试着点击 link 一次,你会发现浏览器默认的跳转页面的行为不见了,console.log('米淇淋你好帅!');也可顺利执行啦哈哈。

但要注意的是,event.preventDefault()并不会阻止事件向上传递(即事件冒泡) 。

另外,值得一提的是,下面这样设置也可以让 a 标签仅仅当做一个普通的按钮,点击实现一个功能,不想页面跳转,也不想锚点定位:

<a href="javascript:;">链接</a>

此外,在事件处理函数的最后加上return false;也会有event.preventDefault()的效果,但切记不可以加在前面,若是加在前面事件处理函数就直接 gg 了。

event.stopPropagation() & event.stopImmediatePropagation()

1.event.stopPropagation() 方法阻止事件向上冒泡传递,阻止任何父事件处理程序被执行。

接下来我们看个例子:

<div>
  <div id="parent">
    父元素
    <div id="child">子元素</div>
  </div>
</div>
......
var parent = document.getElementById('parent');
var child = document.getElementById('child');

child.addEventListener('click', function () {
  console.log('child bubbling');
}, false);

parent.addEventListener('click', function () {
  console.log('parent bubbling');
}, false);

document.body.addEventListener('click', function () {
  console.log('body bubbling');
}, false);

document.documentElement.addEventListener('click', function () {
  console.log('html bubbling');
}, false);

document.addEventListener('click', function () {
  console.log('document bubbling');
}, false);

window.addEventListener('click', function () {
  console.log('window bubbling');
}, false);

当我点击的是「子元素」的时候,通过console.log可以观察到事件触发的顺序为:

child bubbling
parent bubbling
body bubbling
html bubbling
document bubbling
window bubbling

而如果在「子元素」中加入event.stopPropagation() 方法,其余保持原样的话:

child.addEventListener(
  "click",
  function (e) {
    console.log("child bubbling");
    e.stopPropagation();
  },
  false
);

再次点击「子元素」,则只出现:

child bubbling

其余父事件不会触发,即event.stopPropagation() 方法阻止了事件向上冒泡传递,阻止任何父事件处理程序被执行。

2.stopImmediatePropagation()方法 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。

我们来看个例子:

<body>
  <button id="btn">click me to stop propagation</button>
</body>
......
var btn = document.querySelector('#btn');

btn.addEventListener('click', function(e) {
  console.log('btn click 1');
  //e.stopImmediatePropagation();
});

btn.addEventListener('click', function() {
  console.log('btn click 2');
});

document.body.addEventListener('click', function() {
  console.log('body click');
});

document.documentElement.addEventListener('click', function() {
  console.log('html click');
});

document.addEventListener('click', function() {
  console.log('document click');
});

window.addEventListener('click', function() {
  console.log('window click');
});

当我点击 button 的时候,通过console.log可以观察到事件触发的顺序为:

btn click 1
btn click 2
body click
html click
document click
window click

而如果在「btn 的第一个监听函数」中加入event.stopImmediatePropagation() 方法,其余保持原样的话:

btn.addEventListener("click", function (e) {
  console.log("btn click 1");
  e.stopImmediatePropagation();
});

再次点击 button,则只出现:

btn click 1

所以说,使用 stopImmediatePropagation() 方法后,点击按钮时,仅触发设置了stopImmediatePropagation() 方法的监听器,与此同时按钮的其余同类型点击事件不触发。

event.target & event.currentTarget

老实说并不能好好用文字描述这两者的区别,我们直接看个例子:

<style>
  #a{
    width: 200px;
    height: 200px;
    background: yellow       ;
  }
  #b{
    width: 150px;
    height: 150px;
    background: green;
  }
  #c{
    width: 100px;
    height: 100px;
    background: grey;
  }
  #d{
    width: 50px;
    height: 50px;
    background: black;
  }
</style>
......
<div id="a">
  <div id="b">
    <div id="c">
      <div id="d"></div>
    </div>
  </div>
</div>
......
document.getElementById('a').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
})
  document.getElementById('b').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
})
  document.getElementById('c').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
})
  document.getElementById('d').addEventListener('click', function(e) {
    console.log(
      'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
    )
})

jsbin 点这里。

当我们点击最里层黑色区域的元素 d 的时候,会依次输出:

target:d&currentTarget:d
target:d&currentTarget:c
target:d&currentTarget:b
target:d&currentTarget:a

从输出中我们可以看到,event.target指向引起触发事件的元素,而event.currentTarget则是事件绑定的元素,只有被点击的那个目标元素的event.target才会等于event.currentTarget也就是说,event.currentTarget始终是监听事件者,而event.target是事件的真正发出者

另外,值得一提的是,function 内部的this指的也就是event.currentTarget

事件代理

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件代理或叫事件委托(Event Delegation)。

1.优点

  • 减少内存消耗,提高性能

假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件:

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  ......
  <li>Item n</li>
</ul>

如果给每个列表项都绑定一个函数,假如此时列表项很多,那无疑对内存的消耗是非常大的,并且效率上需要消耗很多性能。借助事件代理,我们只需要给父容器 ul 绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的 click 行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

  • 动态绑定事件

在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。

2.如何实现

接下来我们来实现上例中父层元素 #myList 下的 li 元素的事件委托到它的父层元素上:

// 取得容器
var myList = document.getElementById("myList");

// 让父层 myList 来监听 click 事件
myList.addEventListener(
  "click",
  function (e) {
    // 判断目标元素若是 li 则执行 console.log
    if (e.target.tagName.toLowerCase() === "li") {
      console.log(e.target.textContent);
    }
  },
  false
);

// 建立新的 <li> 元素
var newList = document.createElement("li");

// 建立 textNode 文字节点
var textNode = document.createTextNode("Hello world!");

// 通过 appendChild 将 textNode 加入至 newList
newList.appendChild(textNode);

// 通过 appendChild 将 newList 加入至 myList
myList.appendChild(newList);

我们把click事件改由父层的myList来监听,利用事件传递的原理,判断e.target是我们想要的目标节点时,才去执行后续的动作。

这样的好处是你的事件管理会非常轻松,而且后续加上的newList也会有click的效果,无需另外再去绑定click事件。

如果觉得文章对你有些许帮助,欢迎在我的 GitHub 博客点赞和关注,感激不尽!


JavaScript      JavaScript 事件 event

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!