事件绑定与传递
事件绑定与传递
事件监听与响应
上面提及的绑定方式主要是如下三种:
element.addEventListener('type', obj.method.bind(obj))
element.addEventListener('type', function (event) {})
element.addEventListener('type', (event) => {})
不过这种方式会创建大量新的匿名事件监听器,并且即使已经不需要再使用的时候也无法移除。这可能会导致大量的性能损耗以及不可预知的逻辑错误,在未知的用户交互或者冒泡的时候出现问题,相对而言安全一点的绑定方式是:
const handler = function() {
console.log("Tada!");
};
element.addEventListener("click", handler);
// Later on
element.removeEventListener("click", handler);
命名函数,可以保证之后能够移除:
element.addEventListener("click", function click(e) {
if (someCondition) {
return e.currentTarget.removeEventListener("click", click);
}
});
一个更好地方式:
function handleEvent(
eventName,
{ onElement, withCallback, useCapture = false } = {},
thisArg
) {
const element = onElement || document.documentElement;
function handler(event) {
if (typeof withCallback === "function") {
withCallback.call(thisArg, event);
}
}
handler.destroy = function() {
return element.removeEventListener(eventName, handler, useCapture);
};
element.addEventListener(eventName, handler, useCapture);
return handler;
}
// Anytime you need
const handleClick = handleEvent("click", {
onElement: element,
withCallback: event => {
console.log("Tada!");
}
});
// And anytime you want to remove it
handleClick.destroy();
export const addEventListener = (node, event, listener) =>
node.addEventListener
? node.addEventListener(event, listener, false)
: node.attachEvent("on" + event, listener);
export const removeEventListener = (node, event, listener) =>
node.removeEventListener
? node.removeEventListener(event, listener, false)
: node.detachEvent("on" + event, listener);
参数传递
无论何种绑定方式,都会存在着一个参数传递问题。
- 使用闭包方式进行参数传递
function callback(a, b) {
return function() {
console.log("sum = ", a + b);
};
}
var x = 1,
y = 2;
document.getElementById("someelem").addEventListener("click", callback(x, y));
- 使用 bind 函数进行参数传递
const alertText = function(text) {
alert(text);
};
document
.getElementById("someelem")
.addEventListener("click", alertText.bind(this, "hello"));
- Auto Injected Event
有时候需要在一个函数中判断点击的源,这个时候可以使用默认的$event$对象:
function switchResources(event) {
switch (event.target.id) {
case "button1":
content = "Button 1 clicked.";
displayLog();
break;
case "button2":
content = "Button 2 clicked.";
displayLog();
break;
case "button3":
content = "Button 3 clicked.";
displayLog();
break;
}
}
button1.addEventListener("click", switchResources, false);
button2.addEventListener("click", switchResources, false);
button3.addEventListener("click", switchResources, false);
Event 对象
事件流,冒泡与捕获
事件流描述的是从页面接收事件的顺序,IE 的事件流是事件冒泡流,而 Netscape 提出的事件流是事件捕获流。事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点(文档);现代的所有浏览器都支持事件冒泡,但还是有些细微差别。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。
“DOM2 级事件”规定事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。还是上面的代码作为例子,单击 div 元素会按照如下顺序触发事件:
document->html->body->div->body->html->document
在 DOM 事件流中,实际的目标(div)在捕获阶段不会接收到事件。这意味着在捕获阶段,事件到 body 就停止了,下一个阶段是“处于目标”阶段,于是事件在 div 上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回文档。但是多数支持 DOM 事件流的浏览器都实现了一种特定的行为:即使“DOM2 级事件”规范明确要求捕获阶段不会涉及目标事件,但 IE9、safari、chrome、ff 和 opera9.5 及更高版本都会在捕获阶段触发事件对象上的事件,结果就是有两个机会在目标对象上面操作。
addEventListener 最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用。
Event Delegation | 事件委托
DOM 事件委托即指一种以单一通用父节点上绑定响应函数而不是在每个子元素上绑定响应函数的机制,它主要是依赖于上文提及的事件冒泡(Bubbling)机制。当某个元素上触发了某个事件之后,所有注册到该 Target 上的监听器都会被触发。并且该事件会按照 DOM 树的层级逐步冒泡传递到绑定在父层节点的监听器上,每个监听器都会检查是否是关联的 EventTarget,这一步骤会重复进行直到到达 Document 根节点。一个最简单的 Event Delegation 即是:
<ul onclick="alert(event.type + '!')">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
如果你点击了任何的 li 子节点,你都会看到一个警告窗。虽然没有在每个 li 节点上绑定事件处理函数,然后该事件会冒泡传递到父节点中。事件委托机制有着很大的性能优势,譬如如果你需要动态地增减列表中的 li 元素,那么你会进行如下 DOM 创建机制:
var newLi = document.createElement("li");
newLi.innerHTML = "Four";
myUL.appendChild(newLi);
var newLi = document.createElement("li");
newLi.innerHTML = "Four";
myUL.appendChild(newLi);
如果没有用事件委托的话,那么你需要重新绑定 onclick
函数到每一个新增的li
节点上,而如果用事件委托的话你完全不需要关心 li 节点上是否已经绑定了事件响应函数。这个特性在一个大型 Web 项目中效果非凡,如果你为大量的元素绑定了事件响应函数,一旦某个元素被插入或者移出 DOM 树,在事件委托的情况下你可以减少很多的绑定耗时。另外,事件委托还能带来的一个好处就是减少了整体的需要为事件监听与响应函数分配的内存消耗,这一点在小型的页面上可能不会出现。但是在大型,特别是单页应用中屡见不鲜,不用事件委托的话我们常常会发现很多 DOM 元素即使被移出了 DOM 树,依赖在占用内存,也就是所谓的内存泄露,而且往往泄露的内存都是关联到事件绑定上。如果用了事件委托,你就不需要显性地进行 unbind
操作来避免内存泄露。
YAHOO.example.toggleEventListen = {
hide: function() {
// add a CSS class to the main UL to hide all nested list items
YAHOO.util.Dom.addClass(this, "dynamic"); // get all ULs in this one and loop over them
var uls = this.getElementsByTagName("ul");
for (var i = 0; i < uls.length; i++) {
// get the link above this UL and add an event listener pointing to the toggle method
var parentLink = uls[i].parentNode.getElementsByTagName("a")[0];
YAHOO.util.Event.addListener(
parentLink,
"click",
YAHOO.example.toggleEventListen.toggle
);
}
},
toggle: function(e) {
// get the first nested UL and toggle its display property
var ul = this.parentNode.getElementsByTagName("ul")[0];
if (ul.style.display == "none" || ul.style.display == "") {
ul.style.display = "block";
} else {
ul.style.display = "none";
} // stop the link from being followed
YAHOO.util.Event.preventDefault(e);
}
};
// 基于事件委托的事件处理
YAHOO.example.toggleEventDelegation = {
hide: function() {
// add a CSS class to the main UL to hide all nested list items
YAHOO.util.Dom.addClass(this, "dynamic"); // Add one event listener to the element pointing to the toggle method
YAHOO.util.Event.addListener(
this,
"click",
YAHOO.example.toggleEventDelegation.toggle
);
},
toggle: function(e) {
// Check where the event came from
var origin = YAHOO.util.Event.getTarget(e); // Compare with the right node name and test if the parent LI contains a list
if (
origin.nodeName.toLowerCase() === "a" &&
origin.parentNode.getElementsByTagName("ul").length > 0
) {
// get the first nested UL and toggle its display property
var ul = origin.parentNode.getElementsByTagName("ul")[0];
if (ul.style.display == "none" || ul.style.display == "") {
ul.style.display = "block";
} else {
ul.style.display = "none";
} // stop the link from being followed
YAHOO.util.Event.preventDefault(e);
}
}
};