本文的解决方案能够用于Javascript native对象和宿主对象(dom元素),经过如下的方式来绑定和触发事件:javascript
或者php
var input = document.getElementsByTagName('input')[0]; var form = document.getElementsByTagName('form')[0]; Evt.on(input, 'click', function(evt){ console.log('input click1'); console.log(evt.target === input); console.log(evt.modified); //evt.stopPropagation(); console.log(evt.modified); }); var handle2 = Evt.on(input, 'click', function(evt){ console.log('input click2'); console.log(evt.target === input); console.log(evt.modified); }); Evt.on(form, 'click', function(evt){ console.log('form click'); console.log(evt.currentTarget === input); console.log(evt.target === input); console.log(evt.currentTarget === form); console.log(evt.modified); }); Evt.emit(input, 'click'); Evt.emit(input, 'click', {bubbles: true}); handle2.remove(); Evt.emit(input, 'click');
After函数css
为native对象添加事件的过程主要在after函数中完成,这个函数主要作了如下几件事:html
若是obj中已有响应函数,将其替换成dispatcher函数前端
使用链式结构,保证屡次绑定事件函数的顺序执行java
返回一个handle对象,调用remove方法能够去除本次事件绑定程序员
下图为after函数调用先后onlog函数的引用chrome
(调用前)编程
(调用后)浏览器
详细解释请看注释,但愿读者可以跟着运行一遍
var after = function(target, method, cb, originalArgs){ var existing = target[method]; var dispatcher = existing; if (!existing || existing.target !== target) { //若是target中没有method方法,则为他添加一个方法method方法 //若是target已经拥有method方法,但target[method]中target不符合要求则将method方法他替换 dispatcher = target[method] = function(){ //因为js是此法做用域:经过阅读包括变量定义在内的数行源码就能知道变量的做用域。 //局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的 //因此在这个函数中能够访问到dispatcher变量 var results = null; var args = arguments; if (dispatcher.around) {//若是原先拥有method方法,先调用原始method方法 //此时this关键字指向target因此不用target results = dispatcher.around.advice.apply(this, args); } if (dispatcher.after) {//若是存在after链则依次访问其中的advice方法 var _after = dispatcher.after; while(_after && _after.advice) { //若是须要原始参数则传入arguments不然使用上次执行结果做为参数 args = _after.originalArgs ? arguments : results; results = _after.advice.apply(this, args); _after = _after.next; } } } if (existing) { //函数也是对象,也能够拥有属性跟方法 //这里将原有的method方法放到dispatcher中 dispatcher.around = { advice: function(){ return existing.apply(target, arguments); } } } dispatcher.target = target; } var signal = { originalArgs: originalArgs,//对于每一个cb的参数是否使用最初的arguments advice: cb, remove: function() { if (!signal.advice) { return; } //remove的本质是将cb从函数链中移除,删除全部指向他的连接 var previous = signal.previous; var next = signal.next; if (!previous && !next) { dispatcher.after = signal.advice = null; dispatcher.target = null; delete dispatcher.after; } else if (!next){ signal.advice = null; previous.next = null; signal.previous = null; } else if (!previous){ signal.advice = null; dispatcher.after = next; next.previous = null; signal.next = null; } else { signal.advice = null; previous.next = next; next.previous = previous; signal.previous = null; signal.next = null; } } } var previous = dispatcher.after; if (previous) {//将signal加入到链式结构中,处理指针关系 while(previous && previous.next && (previous = previous.next)){}; previous.next = signal; signal.previous = previous; } else {//若是是第一次使用调用after方法,则dispatcher的after属性指向signal dispatcher.after = signal; } cb = null;//防止内存泄露 return signal; }
解决兼容性
IE浏览器从IE9开始已经支持DOM2事件处理程序,可是对于老版本的ie浏览器,任然使用attachEvent方式来为dom元素添加事件。值得庆幸的是微软已宣布2016年将再也不对ie8进行维护,对于广大前端开发者无疑是一个福音。然而在曙光来临以前,仍然须要对那些不支持DOM2级事件处理程序的浏览器进行兼容性处理,一般须要处理如下几点:
屡次绑定一个事件,事件处理函数的调用顺序问题
事件处理函数中的this关键字指向问题
标准化event事件对象,支持经常使用的事件属性
因为使用attachEvent方法添加事件处理函数没法保证事件处理函数的调用顺序,因此咱们弃用attachEvent,转而用上文中的after生成的正序链式结构来解决这个问题。
//一、统一事件触发顺序 function fixAttach(target, type, listener) { debugger; var listener = fixListener(listener); var method = 'on' + type; return after(target, method, listener, true); };
对于事件处理函数中的this关键字指向,经过闭包便可解决(出处),如:
本文也是经过这种方式解决此问题
//一、统一事件触发顺序 function fixAttach(target, type, listener) { debugger; var listener = fixListener(listener); var method = 'on' + type; return after(target, method, listener, true); }; function fixListener(listener) { return function(evt){ //每次调用listenser以前都会调用fixEvent debugger; var e = _fixEvent(evt, this);//this做为currentTarget if (e && e.cancelBubble && (e.currentTarget !== e.target)){ return; } var results = listener.call(this, e); if (e && e.modified) { // 在整个函数链执行完成后将lastEvent回归到原始状态, //利用异步队列,在主程序执行完后再执行事件队列中的程序代码 //常规的作法是在emit中判断lastEvent并设为null //这充分体现了js异步编程的优点,把变量赋值跟清除代码放在一块儿,避免逻辑分散,缺点是不符合程序员正常思惟方式 if(!lastEvent){ setTimeout(function(){ lastEvent = null; }); } lastEvent = e; } return results; } }
对于事件对象的标准化,咱们须要将ie提供给咱们的现有属性转化为标准的事件属性。
function _fixEvent(evt, sender){ if (!evt) { evt = window.event; } if (!evt) { // emit没有传递事件参数,或者经过input.onclick方式调用 return evt; } if(lastEvent && lastEvent.type && evt.type == lastEvent.type){ //使用一个全局对象来保证在冒泡过程当中访问的是同一个event对象 //chrome中整个事件处理过程event是惟一的 evt = lastEvent; } var fixEvent = evt; // bubbles 和cancelable根据每次emit时手动传入参数设置 fixEvent.bubbles = typeof evt.bubbles !== 'undefined' ? evt.bubbles : false; fixEvent.cancelable = typeof evt.cancelable !== 'undefined' ? evt.cancelable : true; fixEvent.currentTarget = sender; if (!fixEvent.target){ // 屡次绑定统一事件,只fix一次 fixEvent.target = fixEvent.srcElement || sender; fixEvent.eventPhase = fixEvent.target === sender ? 2 : 3; if (!fixEvent.preventDefault) { fixEvent.preventDefault = _preventDefault; fixEvent.stopPropagation = _stopPropagation; fixEvent.stopImmediatePropagation = _stopImmediatePropagation; } //参考:http://www.nowamagic.net/javascript/js_EventMechanismInDetail.php if( fixEvent.pageX == null && fixEvent.clientX != null ) { var doc = document.documentElement, body = document.body; fixEvent.pageX = fixEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); fixEvent.pageY = fixEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } if (!fixEvent.relatedTarget && fixEvent.fromEvent) { fixEvent.relatedTarget = fixEvent.fromEvent === fixEvent.target ? fixEvent.toElement : fixEvent.fromElement; } // 参考: http://www.cnblogs.com/hsapphire/archive/2009/12/18/1627047.html if (!fixEvent.which && fixEvent.keyCode) { fixEvent.which = fixEvent.keyCode; } } return fixEvent; } function _preventDefault(){ this.defaultPrevented = true; this.returnValue = false; this.modified = true; } function _stopPropagation(){ this.cancelBubble = true; this.modified = true; } function _stopImmediatePropagation(){ this.isStopImmediatePropagation = true; this.modified = true; }
在_preventDefault、_stopPropagation、_stopImmediatePropagation三个函数中咱们,若是被调用则listener执行完后使用一个变量保存event对象(见fixListener),以便后序事件处理程序根据event对象属性进行下一步处理。stopImmediatePropagation函数,对于这个函数的模拟,咱们一样经过闭包来解决。
注意这里不能直接写成这种形式,上文中fixListener也是一样道理。
须要注意一点,咱们将event标准化目的还有一点,能够在emit方法中设置参数来控制事件过程,好比:
Evt.emit(input, ’click’);//不冒泡
Evt.emit(input, ’click’, {bubbles: true});//冒泡
根据个人测试使用fireEvent方式触发事件,没法设置{bubbles:false}来阻止冒泡,因此这里咱们用Javascript来模拟冒泡过程。同时在这个过程当中也要保证event对象的惟一性。
// 模拟冒泡事件 var sythenticBubble = function(target, type, evt){ var method = 'on' + type; var args = Array.prototype.slice.call(arguments, 2); // 保证使用emit触发dom事件时,event的有效性 if ('parentNode' in target) { var newEvent = args[0] = {}; for (var p in evt) { newEvent[p] = evt[p]; } newEvent.preventDefault = _preventDefault; newEvent.stopPropagation = _stopPropagation; newEvent.stopImmediatePropagation = _stopImmediatePropagation; newEvent.target = target; newEvent.type = type; } do{ if (target && target[method]) { target[method].apply(target, args); } }while(target && (target = target.parentNode) && target[method] && newEvent && newEvent.bubbles); } var emit = function(target, type, evt){ if (target.dispatchEvent && document.createEvent){ var newEvent = document.createEvent('HTMLEvents'); newEvent.initEvent(type, evt && !!evt.bubbles, evt && !!evt.cancelable); if (evt) { for (var p in evt){ if (!(p in newEvent)){ newEvent[p] = evt[p]; } } } target.dispatchEvent(newEvent); } /*else if (target.fireEvent) { target.fireEvent('on' + type);// 使用fireEvent在evt参数中设置bubbles:false无效,因此弃用 } */else { return sythenticBubble.apply(on, arguments); } }
附上完整代码:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/> <meta http-equiv="window-target" content="_top"> <title>Writing to Same Doc</title> <script language="JavaScript"> var after = function(target, method, cb, originalArgs){ var existing = target[method]; var dispatcher = existing; if (!existing || existing.target !== target) { //若是target中没有method方法,则为他添加一个方法method方法 //若是target已经拥有method方法,但target[method]中target不符合要求则将method方法他替换 dispatcher = target[method] = function(){ //因为js是此法做用域:经过阅读包括变量定义在内的数行源码就能知道变量的做用域。 //局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的 //因此在这个函数中能够访问到dispatcher变量 var results = null; var args = arguments; if (dispatcher.around) {//若是原先拥有method方法,先调用原始method方法 //此时this关键字指向target因此不用target results = dispatcher.around.advice.apply(this, args); } if (dispatcher.after) {//若是存在after链则依次访问其中的advice方法 var _after = dispatcher.after; while(_after && _after.advice) { //若是须要原始参数则传入arguments不然使用上次执行结果做为参数 args = _after.originalArgs ? arguments : results; results = _after.advice.apply(this, args); _after = _after.next; } } } if (existing) { //函数也是对象,也能够拥有属性跟方法 //这里将原有的method方法放到dispatcher中 dispatcher.around = { advice: function(){ return existing.apply(target, arguments); } } } dispatcher.target = target; } var signal = { originalArgs: originalArgs,//对于每一个cb的参数是否使用最初的arguments advice: cb, remove: function() { if (!signal.advice) { return; } //remove的本质是将cb从函数链中移除,删除全部指向他的连接 var previous = signal.previous; var next = signal.next; if (!previous && !next) { dispatcher.after = signal.advice = null; dispatcher.target = null; delete dispatcher.after; } else if (!next){ signal.advice = null; previous.next = null; signal.previous = null; } else if (!previous){ signal.advice = null; dispatcher.after = next; next.previous = null; signal.next = null; } else { signal.advice = null; previous.next = next; next.previous = previous; signal.previous = null; signal.next = null; } } } var previous = dispatcher.after; if (previous) {//将signal加入到链式结构中,处理指针关系 while(previous && previous.next && (previous = previous.next)){}; previous.next = signal; signal.previous = previous; } else {//若是是第一次使用调用after方法,则dispatcher的after属性指向signal dispatcher.after = signal; } cb = null;//防止内存泄露 return signal; } //一、统一事件触发顺序 //二、标准化事件对象 //三、模拟冒泡 emit时保持冒泡行为,注意input.onclick这种方式是不冒泡的 //四、保持冒泡过程当中event的惟一性 window.Evt = (function(){ var on = function(target, type, listener){ debugger; if (!listener){ return; } // 处理stopImmediatePropagation,经过包装listener来支持stopImmediatePropagation if (!(window.Event && window.Event.prototype && window.Event.prototype.stopImmediatePropagation)) { listener = _addStopImmediate(listener); } if (target.addEventListener) { target.addEventListener(type, listener, false); return { remove: function(){ target.removeEventListener(type, listener); } } } else { return fixAttach(target, type, listener); } }; var lastEvent; // 使用全局变量来保证一个元素的多个listenser中事件对象的一致性,冒泡过程当中事件对象的一致性;在chrome这些过程当中使用的是同一个event //一、统一事件触发顺序 function fixAttach(target, type, listener) { debugger; var listener = fixListener(listener); var method = 'on' + type; return after(target, method, listener, true); }; function fixListener(listener) { return function(evt){ //每次调用listenser以前都会调用fixEvent debugger; var e = _fixEvent(evt, this);//this做为currentTarget if (e && e.cancelBubble && (e.currentTarget !== e.target)){ return; } var results = listener.call(this, e); if (e && e.modified) { // 在整个函数链执行完成后将lastEvent回归到原始状态, //利用异步队列,在主程序执行完后再执行事件队列中的程序代码 //常规的作法是在emit中判断lastEvent并设为null //这充分体现了js异步编程的优点,把变量赋值跟清除代码放在一块儿,避免逻辑分散,缺点是不符合程序员正常思惟方式 if(!lastEvent){ setTimeout(function(){ lastEvent = null; }); } lastEvent = e; } return results; } } function _fixEvent(evt, sender){ if (!evt) { evt = window.event; } if (!evt) { // emit没有传递事件参数,或者经过input.onclick方式调用 return evt; } if(lastEvent && lastEvent.type && evt.type == lastEvent.type){ //使用一个全局对象来保证在冒泡过程当中访问的是同一个event对象 //chrome中整个事件处理过程event是惟一的 evt = lastEvent; } var fixEvent = evt; // bubbles 和cancelable根据每次emit时手动传入参数设置 fixEvent.bubbles = typeof evt.bubbles !== 'undefined' ? evt.bubbles : false; fixEvent.cancelable = typeof evt.cancelable !== 'undefined' ? evt.cancelable : true; fixEvent.currentTarget = sender; if (!fixEvent.target){ // 屡次绑定统一事件,只fix一次 fixEvent.target = fixEvent.srcElement || sender; fixEvent.eventPhase = fixEvent.target === sender ? 2 : 3; if (!fixEvent.preventDefault) { fixEvent.preventDefault = _preventDefault; fixEvent.stopPropagation = _stopPropagation; fixEvent.stopImmediatePropagation = _stopImmediatePropagation; } //参考:http://www.nowamagic.net/javascript/js_EventMechanismInDetail.php if( fixEvent.pageX == null && fixEvent.clientX != null ) { var doc = document.documentElement, body = document.body; fixEvent.pageX = fixEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); fixEvent.pageY = fixEvent.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } if (!fixEvent.relatedTarget && fixEvent.fromEvent) { fixEvent.relatedTarget = fixEvent.fromEvent === fixEvent.target ? fixEvent.toElement : fixEvent.fromElement; } // 参考: http://www.cnblogs.com/hsapphire/archive/2009/12/18/1627047.html if (!fixEvent.which && fixEvent.keyCode) { fixEvent.which = fixEvent.keyCode; } } return fixEvent; } function _preventDefault(){ this.defaultPrevented = true; this.returnValue = false; this.modified = true; } function _stopPropagation(){ this.cancelBubble = true; this.modified = true; } function _stopImmediatePropagation(){ this.isStopImmediatePropagation = true; this.modified = true; } function _addStopImmediate(listener) { return function(evt) { // 除了包装listener外,还要保证全部的事件函数共用一个evt对象 if (!evt.isStopImmediatePropagation) { //evt.stopImmediatePropagation = _stopImmediateProgation; return listener.apply(this, arguments); } } } // 模拟冒泡事件 var sythenticBubble = function(target, type, evt){ var method = 'on' + type; var args = Array.prototype.slice.call(arguments, 2); // 保证使用emit触发dom事件时,event的有效性 if ('parentNode' in target) { var newEvent = args[0] = {}; for (var p in evt) { newEvent[p] = evt[p]; } newEvent.preventDefault = _preventDefault; newEvent.stopPropagation = _stopPropagation; newEvent.stopImmediatePropagation = _stopImmediatePropagation; newEvent.target = target; newEvent.type = type; } do{ if (target && target[method]) { target[method].apply(target, args); } }while(target && (target = target.parentNode) && target[method] && newEvent && newEvent.bubbles); } var emit = function(target, type, evt){ if (target.dispatchEvent && document.createEvent){ var newEvent = document.createEvent('HTMLEvents'); newEvent.initEvent(type, evt && !!evt.bubbles, evt && !!evt.cancelable); if (evt) { for (var p in evt){ if (!(p in newEvent)){ newEvent[p] = evt[p]; } } } target.dispatchEvent(newEvent); } /*else if (target.fireEvent) { target.fireEvent('on' + type);// 使用fireEvent在evt参数中设置bubbles:false无效,因此弃用 } */else { return sythenticBubble.apply(on, arguments); } } return { on: on, emit: emit }; })() </script> <style type="text/css"></style> </head> <body> <form> <input type="button" value="Replace Content" > </form> </body> </html>
脑图:
欢迎各位有志之士前来交流探讨!