在所有JavaScript应用中事件处理都是非常重要的. 所有的JavaScript均通过事件绑定到UI上, 所以大多数前端工程师需要花费很多时间来编写和修改事件处理程序. 遗憾的是, 在JavaScript诞生之初, 这部分内容并未受太多重视. 甚至当开发者们开始热衷于将传统的软件架构概念融入到JavaScript里时, 事件绑定仍然没有收到多大重视. 大多数事件处理相关的代码和时间环境(对于开发者来说, 每次时间出发时才可能会用)紧紧耦合在一起, 导致可维护性很糟糕.
7.1 典型用法
多数开发者都很了解, 当事件出发时, 事件对象(event 对象) 会作为回调参数传入事件处理程序中. event对象包含所有和事件相关的信息, 包括事件的宿主(target) 以及其他和事件类型相关的数据. 鼠标事件会将其位置信息暴露在event对象上, 键盘事件会将按键信息暴露在event对象上, 触屏事件会将触摸位置和持续事件(duration) 暴露在event对象上, 只有提供了所有这些信息, UI才会正确地执行交互.
在很多场景中, 你只是用到了event所提供信息的一小部分, 看下面这段代码.
// 不好的写法
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);
这段代码只用到了event对象的两个属性: clientX和 clientY. 在将元素显示在页面里之前先用这两个属性给它作定位. 尽管这段代码看起来非常简单且没有什么问题, 但实际上是不好的写法, 因为这种做法有其局限性.
7.2 规则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(evnet) {
MyApplication.handleClick(event);
})
之前在事件处理程序中包含了所有应用逻辑现在转移到了MyApplication.showPopup()方法中. 现在MyApplication.handleClick()方法制作一件事情, 即调用MyApplication.showPopup(). 若应用逻辑被剥离出去, 对同一段功能代码的调用可以在多点发生, 则不需要一定依赖于某个特定时间的触发, 这显然更加方便. 单着只是拆解时间处理程序代码的第一步.
7.3 规则2: 不要分发事件对象
在剥离出应用逻辑之后, 上段实例代码和存在一个问题, 即event对象被无节制地分发. 它从匿名的时间处理函数传入了MyApplication.handleClick(), 然后又传入了MyApplication.showPopup(). 正如上文提到的, event对象上包含很多和事件相关的额外信息, 而这段代码只用到了其中的两个而已.
应用逻辑不应当依赖于event对象来正确完成功能, 原因如下.
- 方法接口并没有表明哪些数据是必要的. 好的API一定是对于期望和依赖都是透明的. 将event对象作为参数并不能告诉你event的哪些属性时有用的, 用来干什么?
- 因此, 如果你想测试这个方法, 你必须重新创建一个event对象并将它作为参数传入. 所以, 你需要确切的知道张哥方法使用了哪些信息, 这样才能正切地写出测试代码.
这些问题在大型Web应用中都是不可取的. 代码不够明晰就会导致bug.
最佳的办法是让事件处理程序使用event对象来处理事件, 然后拿到所有需要的数据传给应用逻辑. 例如, MyApplication.showPopup()方法只需要两个数据, x坐标和y坐标. 这样我们将方法重写一下, 让它来接受这两个参数.
// 好的写法
var MyApplication = {
handleClick: function(event) {
this.showPopup(event.clientX, event.clientY);
}
showPopup: function(x, y) {
var popup = document.getElementById('popup');
popup.style.left = x + 'px';
popup.style.top = y + 'px';
popup.className = 'reveal';
}
};
addListener(element, 'click', function(evnet) {
MyApplication.handleClick(event);
})
在这段重写大代码中, MyApplication.handleClick()将x坐标和y坐标传入了MyApplication .showPopup(), 代替了之前传入的事件对象. 可以很清晰的看到MyApplication.showPopup()所期望传入的参数, 并且在测试或代码的任意位置都可以很轻易的直接调用这段逻辑, 比如:
MyApplication.showPopup(10, 10);
当处理事件时, 最好让事件处理程序成为接触到event对象的唯一的函数. 事件处理程序应当在进入应用逻辑之前针对event对象执行任何必要的操作, 包括阻止默认事件或阻止冒泡, 都应当直接包含在事件处理程序中, 比如:
// 好的做法
var MyApplication = {
handleClick: function(event) {
// 假设事件支持DOM Level2
event.preventDefault();
event.stopPropagation();
// 传入应用逻辑
this.showPopup(event.clientX, event.clientY);
}
showPopup: function(x, y) {
var popup = document.getElementById('popup');
popup.style.left = x + 'px';
popup.style.top = y + 'px';
popup.className = 'reveal';
}
};
addListener(element, 'click', function(evnet) {
MyApplication.handleClick(event);
})
在这段代码中, MyApplication.handleClick()是事件处理程序, 因此它是在将数据传入应用逻辑之前调用了event.preventDefault()和event.stopPropagation(), 这清楚的展示了事件处理程序和应用逻辑之间的分工. 因为应用逻辑不需要对event产生依赖, 进而在很多地方都可以轻松的使用相同的业务逻辑, 包括写测试代码.