事件处理
在所有 JavaScript 应用中事件处理都是非常重要的。所有的 JavaScript 均通过事件绑定到 UI 上,所以大多数前端工程师需要花费很多时间来编写和修改事件处理程序。由于事件绑定没有受到多大重视,大多数事件处理相关的代码和事件环境(对于开发者来说,每次事件触发时才会可用)紧紧耦合在一起,导致可维护性很糟糕。
典型用法
【事件对象】:当事件触发时,事件对象(event 对象)会作为回调参数传入事件处理程序中。event 对象包含所有和事件相关的信息,包括宿主(target)以及其他和事件类型相关的数据。例如,鼠标事件会将其位置信息暴露在 event 对象上,键盘事件会将按键的信息暴露在 event 对象上,触屏事件会将触摸位置和持续时间(duration)暴露在 event 对象上。只有提供了所有这些信息,UI 才会正确地执行交互。
【情境】:
function handleClick(event) {
var popup = document.getElementById('popup');
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
addListener(element, "click", handleClick);
规则1:隔离应用逻辑
上段情境代码的第一个问题是:事件处理程序包含了应用逻辑(application logic)。应用逻辑是和应用相关的功能性代码,而不是和用户行为相关的。
【最佳实践】:将应用逻辑从所有事件处理程序中抽离出来。
- 或许在其他地方也会触发同一段相同的应用逻辑:比如,有时你需要在用户将鼠标移到某个元素上时判断是否显示弹出框,或者当按下键盘上的某个键时也作同样的逻辑判断。
- 方便测试:测试时需要直接触发功能代码,而不必通过模拟对元素的点击来触发。如果将应用逻辑放置于事件处理程序中,唯一的测试方法是制造事件的触发。尽管某些测试框架可以模拟触发事件,但实际上这不是测试的最佳方法。调用功能性代码最好的做法就是单个的函数调用。
【重构】:
var MyApplication = {
handleClick: function(event) {
this.showPopup(event);
},
showPopup: function(event) {
var popup = document.getElementById("popup");
popup.style.left = event.clientX + "px";
popup.style.top = event.clientY + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event);
});
【说明】:将应用逻辑剥离出去,对同一段功能代码的调用可以在多点发生,则不需要一定依赖于某个特定事件的触发,这显然更加方便。
规则2:不要分发事件对象
上例情境代码还存在一个问题:event 对象被无节制地分发。从匿名的事件处理函数传入了 MyApplication.handleClick(),然后又传入了 MyApplication.showPopup()。event 对象上包含很多和事件相关的额外信息,而这段代码只用到了其中的两个而已。
【原则】:应用逻辑不应当依赖于 event 对象来正确完成功能。
- 方法接口并没有表明哪些数是必要的。好的 API一定是对于期望和依赖都是透明的。将 event 对象作为参数并不能告诉你 event 的哪些属性是有用的,用来干什么?(译者:作者的意思是说好的 API 要更明确清楚,但这个观点我们需要辩证地对待,如果明确知道回调传值的用处以及需要传哪些值,当然更好。但更多的时候我们并不知道应用逻辑做何种事情,因此需要为应用逻辑提供尽可能多的信息,如何利用这些信息,效率如何统统交由应用逻辑负责,以达到某种层次的解耦。)
- 如果你想测试这个方法,必须重新创建一个 event 对象并将它作为参数传入。所以,需要确切地知道这个方法使用了哪些信息,这样才能正确地写出测试代码。
【注意】:接口格式不清晰和自行构造 event 对象来用于测试在大型 Web 应用中都是不可取的。代码不够清晰就会导致 bug。
【最佳实践】:让事件处理程序使用 event 对象来处理事件,然后拿到所有需要的数据传给应用逻辑。
【重构】:
var MyApplication = {
handleClick: function(event) {
this.showwPopup(event.clientX, event.clientY);
},
showPopup: funciton(clientX, clientY) {
var popup = document.getElementById('popup');
popup.style.left = clientX + "px";
popup.style.top = clientY + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event);
});
// 测试应用逻辑代码时,不需要自行构造 event 对象。
MyApplication.showPopup(10, 10);
【注意】:当处理事件时,最好让事件处理程序成为接触到 event 对象的唯一的函数。事件处理程序应当在进入应用逻辑之前针对 event 对象执行任何必要的操作,包括阻止默认事件或阻止事件冒泡,都应当直接包含在事件处理程序中。
var MyApplication = {
handleClick: function(event) {
// 假设事件支持 DOM Level2
event.preventDefault();
event.stopPropagation();
// 传入应用逻辑
this.showwPopup(event.clientX, event.clientY);
},
showPopup: funciton(clientX, clientY) {
var popup = document.getElementById('popup');
popup.style.left = clientX + "px";
popup.style.top = clientY + "px";
popup.className = "reveal";
}
};
addListener(element, "click", function(event) {
MyApplication.handleClick(event);
});