Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解Javascript中的事件绑定与事件委托 #4

Open
linqinghao opened this issue Mar 31, 2018 · 0 comments
Open

理解Javascript中的事件绑定与事件委托 #4

linqinghao opened this issue Mar 31, 2018 · 0 comments

Comments

@linqinghao
Copy link
Owner

linqinghao commented Mar 31, 2018

最近在深入实践 js 中,遇到了一些问题,比如我需要为动态创建的 DOM 元素绑定事件,那么普通的事件绑定就不行了,于是通过上网查资料了解到事件委托,因此想总结一下 js 中的事件绑定与事件委托。

事件绑定

最直接的事件绑定:HTML 事件处理程序

如下示例代码,通过节点属性显式声明,直接在 HTML 中,显式地为按钮绑定了 click 事件,当该按钮有用户点击行为时,便会触发 myClickFunc 方法。

/* html */
<button id='btn' onclick='myClickFunc()'>
  ClickMe
</button>;

/* js */
// 事件处理程序
var myClickFunc = function(evt) {
  // TODO..
};

// 移除事件处理程序
myClickFunc = function() {};

显而易见,这种绑定方式非常不友好,HTML 代码和 JS 代码严重耦合在一起,比如当要修改一个函数名时候,就要修改两次,

DOM 0 级事件处理程序

通过 DOM 操作动态绑定事件,是一种比较传统的方式,把一个函数赋值给事件处理程序。这种方式也是应用较多的方式,比较简单。看下面例子:

/* html */
<button id='btn'>ClickMe</button>;

/* js */
// 事件处理程序
var myClickFunc = function(evt) {
  // TODO ...
};

// 直接给DOM节点的 onclick 方法赋值,注意这里接收的是一个function
document.getElementById('btn').onclick = myClickFunc;

// 移除事件处理程序
document.getElementById('btn').onclick = null;

DOM 2 级事件处理程序

通过事件监听的方式绑定事件,DOM2 级事件定义了两个方法,用于处理指定和删除事件处理程序的操作。

// event: 事件名称
// function: 事件函数
// boolean: false | true, true 为事件捕获, false 为事件冒泡(默认);
Ele.addEventListener(event,function[,boolean]); // 添加句柄
ELe.removeEventListener(event,function[,boolean]); // 移除句柄

看个例子:

/* html */
<button id="btn">ClickMe</button>

/* js */
// 通过DOM操作进行动态绑定:
// 获取btnHello节点
var oBtn = document.getElementById('btn');

// 增加第一个 click 事件监听处理程序
oBtn.addEventListener('click',function(evt){
    // TODO sth 1...
});

// 增加第二个 click 事件监听处理程序
oBtn.addEventListener('click',function(evt){
    // TODO sth 2...
});

// ps:通过这种形式,可以给btn按钮绑定任意多个click监听;注意,执行顺序与添加顺序相关。

// 移除事件处理程序
oBtn.removeEventListener('click',function(evt){..});

IE 事件处理程序

DOM 2 级事件处理程序在 IE 是行不通的,IE 有自己的事件处理程序方法:attachEvent()detachEvent()。这两个方法的用法与addEventListener()是一样的,但是只接收两个参数,一个是事件名称,另一个是事件处理程序的函数。为什么不使用第三个参数的原因呢?因为 IE8 以及更早的浏览器版本只支持事件冒泡。看个例子:

/* html */
<button id='btn'>ClickMe</button>;

/* js */
var oBtn = document.getElementById('btn');
// 事件处理函数
function evtFn() {
  console.log(this);
}
// 添加句柄
oBtn.attachEvent('onclick', evtFn);

// 移除句柄
oBtn.detachEvent('onclick', evtFn);

简易的跨浏览器解决方法

如果我们既要支持 IE 的事件处理方法,又要支持 DOM 2 级事件,那么就要封装一个跨浏览器的事件处理函数,如果支持 DOM 2 级事件,就用addEventListener,否则就用attachEvent。例子如下:

//跨浏览器事件处理程序
var eventUtil = {
  // 添加句柄
  addHandler: function(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent('on' + type, handler);
    } else {
      element['on' + type] = handler;
    }
  },
  // 删除句柄
  removeHandler: function(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent('on' + type, handler);
    } else {
      element['on' + type] = null;
    }
  },
};

var oBtn = document.getElementById('btn');
function evtFn() {
  alert('hello world');
}
eventUtil.addHandler(oBtn, 'click', evtFn);
eventUtil.removeHandler(oBtn, 'click', evtFn);

事件冒泡和事件捕获

在了解事件委托之前,要先了解下事件冒泡和事件捕获。

早期的 web 开发,浏览器厂商很难回答一个哲学上的问题:当你在页面上的一个区域点击时,你真正感兴趣的是哪个元素。这个问题带来了交互的定义。在一个元素的界限内点击,显得有点含糊。毕竟,在一个元素上的点击同时也发生在另一个元素的界限内。例如单击一个按钮。你实际上点击了按钮区域、body 元素的区域以及 html 元素的区域。

伴随着这个问题,两种主流的浏览器 Netscape 和 IE 有不同的解决方案。Netscape 定义了一种叫做事件捕获的处理方法,事件首先发生在 DOM 树的最高层对象(document)然后往最深层的元素传播。在图例中,事件捕获首先发生在 document 上,然后是 html 元素,body 元素,最后是 button 元素。

IE 的处理方法正好相反。他们定义了一种叫事件冒泡的方法。事件冒泡认为事件促发的最深层元素首先接收事件。然后是它的父元素,依次向上,知道 document 对象最终接收到事件。尽管相对于 html 元素来说,document 没有独立的视觉表现,他仍然是 html 元素的父元素并且事件能冒泡到 document 元素。所以图例中噢噢那个 button 元素先接收事件,然后是 body、html 最后是 document。如下图:

事件冒泡与事件捕获-2019-12-7.png

事件冒泡

简单点说,事件冒泡就是事件触发时,会从目标 DOM 元素向上传播,直到文档根节点,一般情况下,会是如下形式传播:

targetDOM → parentNode → ... → body → document → window

如果希望一次事件触发能在整个 DOM 树上都得到响应,那么就需要用到事件冒泡的机制。看下面示例:

/* html */
<button id='btn'>ClickMe</button>;

/* js */
// 给按钮增加click监听
document.getElementById('btn').addEventListener(
  'click',
  function(evt) {
    alert('button clicked');
  },
  false
);

// 给body增加click监听
document.body.addEventListener(
  'click',
  function(evt) {
    alert('body clicked');
  },
  false
);

在这种情况下,点击按钮“ClickMe”后,其自身的 click 事件会被触发,同时,该事件将会继续向上传播, 所有的祖先节点都将得到事件的触发命令,并立即触发自己的 click 事件;所以如上代码,将会连续弹出两个 alert.

在有些时候,我们想让事件独立触发,所以我们必须阻止冒泡,用 event 的stopPropagation()方法。

<button id='btn'>ClickMe</button>;

/* js */
// 给按钮增加click监听
document.getElementById('btn').addEventListener(
  'click',
  function(evt) {
    alert('button clicked');
    evt.stopPropagation(); //阻止事件冒泡
  },
  false
);

// 给body增加click监听
document.body.addEventListener(
  'click',
  function(evt) {
    alert('body clicked');
  },
  false
);

此时,点击按钮后,只会触发按钮本身的 click 事件,得到一个 alert 效果;该按钮的点击事件,不会向上传播,body 节点就接收不到此次事件命令。

需要注意的是:

  1. 不是所有的事件都能冒泡,如:blurfocusloadunload 事件都不能
  2. 不同的浏览器,阻止冒泡的方式也不一样,在 w3c 标准中,通过event.stopPropagation()完成, 在 IE 中则是通过自身的event.cancelBubble=true来完成。

事件委托

事件委托看起来挺难理解,但是举个生活的例子。比如,有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台 MM 代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台 MM 收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台 MM 也会在收到寄给新员工的快递后核实并代为签收。举个例子

HTML 结构:

<ul id="ul-item">
  <li>item1</li>
  <li>item2</li>
  <li>item3</li>
  <li>item4</li>
</ul>

如果我们要点击 li 标签,弹出里面的内容,我们就需要为每个 li 标签绑定事件。

(function() {
  var oUlItem = document.getElementById('ul-item');
  var oLi = oUlItem.getElementsByTagName('li');
  for (var i = 0, l = oLi.length; i < l; i++) {
    oLi[i].addEventListener('click', show);
  }
  function show(e) {
    e = e || window.event;
    alert(e.target.innerHTML);
  }
})();

虽然这样子能够实现我们想要的功能,但是如果这个 ul 中的 li 子元素频繁的添加或删除,我们就需要在每次添加 li 的时候为它绑定事件。这就添加了复杂度,并且造成内存开销较大。

更简单的方法是利用事件委托,当事件被抛到更上层的父节点的时候,通过检查事件的目标对象(target)来判断并获取事件源 li。

(function() {
  var oUlItem = document.getElementById('ul-item');
  oUlItem.addEventListener('click', show);
  function show(e) {
    e = e || window.event;
    var src = e.target;
    if (src && src.nodeName.toLowerCase() === 'li') {
      alert(src.innerHTML);
    }
  }
})();

这里我们为父节点 UL 添加了点击事件,当点击子节点 li 标签的时候,点击事件会冒泡到父节点。父节点捕获到事件之后,通过判断e.target.nodeName来判断是否为我们需要处理的节点,并且通过e.target拿到了被点击的 li 节点。从而可以获取到相应的信息,并做处理。

优点:

通过上面的介绍,大家应该能够体会到使用事件委托对于 web 应用程序带来的几个优点:

  1. 管理的函数变少了。不需要为每个元素都添加监听函数。对于同一个父节点下面类似的子元素,可以通过委托给父元素的监听函数来处理事件。

  2. 可以方便地动态添加和修改元素,不需要因为元素的改动而修改事件绑定。

  3. JavaScript 和 DOM 节点之间的关联变少了,这样也就减少了因循环引用而带来的内存泄漏发生的概率。

参考资料

http://www.diguage.com/archives/71.html

http://owenchen.net/?p=15

http://div.io/topic/1357

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant