Javascript事件机制兼容性解决方案
本文的解决方案可以用于Javascript native对象和宿主对象(dom元素),通过以下的方式来绑定和触发事件:

或者
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函数
为native对象添加事件的过程主要在after函数中完成,这个函数主要做了以下几件事:
- 如果obj中已有响应函数,将其替换成dispatcher函数
- 使用链式结构,保证多次绑定事件函数的顺序执行
- 返回一个handle对象,调用remove方法可以去除本次事件绑定
下图为after函数调用前后onlog函数的引用

(调用前)

(调用后)
详细解释请看注释,希望读者能够跟着运行一遍
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生成的正序链式结构来解决这个问题。
//1、统一事件触发顺序
function fixAttach(target, type, listener) {
debugger;
var listener = fixListener(listener);
var method = 'on' + type;
return after(target, method, listener, true);
};
对于事件处理函数中的this关键字指向,通过闭包即可解决(出处),如:

本文也是通过这种方式解决此问题
//1、统一事件触发顺序
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;
} //1、统一事件触发顺序
//2、标准化事件对象
//3、模拟冒泡 emit时保持冒泡行为,注意input.onclick这种方式是不冒泡的
//4、保持冒泡过程中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
//1、统一事件触发顺序
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>
脑图:
欢迎各位有志之士前来交流探讨!
参考文章:
javascript线程解释(setTimeout,setInterval你不知道的事)
Javascript事件机制兼容性解决方案的更多相关文章
- 【移动端兼容问题研究】javascript事件机制详解(涉及移动兼容)
前言 这篇博客有点长,如果你是高手请您读一读,能对其中的一些误点提出来,以免我误人子弟,并且帮助我提高 如果你是javascript菜鸟,建议您好好读一读,真的理解下来会有不一样的收获 在下才疏学浅, ...
- 重温javascript事件机制
以前用过一段时间的jquery感觉太方便,太强大了,各种动画效果,dom事件.创建节点.遍历.控件及UI库,应有尽有:开发文档也很多,网上讨论的问题更是甚多,种种迹象表明jquery是一个出色的jav ...
- 【探讨】javascript事件机制底层实现原理
前言 又到了扯淡时间了,我最近在思考javascript事件机制底层的实现,但是暂时没有勇气去看chrome源码,所以今天我来猜测一把 我们今天来猜一猜,探讨探讨,javascript底层事件机制是如 ...
- JavaScript事件机制——细思极恐
JavaScript事件机制,也有让人深思的东西.在一开始未深入了解,我头脑里有几个问题发出: 1. 自下而上(冒泡)事件怎么写,自上而下(捕获)又是怎么写? 2. 捕获型和冒泡型同时设置,谁生效? ...
- [解惑]JavaScript事件机制
群里童鞋问到关于事件传播的一个问题:“事件捕获的时候,阻止冒泡,事件到达目标之后,还会冒泡吗?”. 初学 JS 的童鞋经常会有诸多疑问,我在很多 QQ 群也混了好几年了,耳濡目染也也收获了不少,以后会 ...
- 总结JavaScript事件机制
JavaScript事件模型 在各种浏览器中存在三种事件模型: 原始事件模型 , DOM2事件模型 , IE事件模型. 其中原始的事件模型被所有浏览器所支持,而DOM2中所定义的事件模型目前被除了IE ...
- javascript事件机制
① javascript绑定事件的方式 http://blog.iderzheng.com/dom-javascript-event-binding-comparison/ ② javascript事 ...
- 对JavaScript事件机制的一点理解
JavaScript通过事件机制实现了异步操作,这种异步操作可以使CPU可以在IO任务的等待中被释放出来处理其他任务,等待IO结束再去处理这个任务.这个是一个基本的事件机制. 那么是不是说事件从监听到 ...
- JavaScript——事件机制
事件是将JavaScript脚本与网页联系在一起的主要方式,是JavaScript中最重要的主题之一,深入理解事件的工作机制以及它们对性能的影响至关重要.本文将详细介绍JavaScript的事件机制, ...
随机推荐
- Oracle数据库迁移到AWS云的方案
当前云已经成为常态,越来越多的企业希望使用云来增加基础设施的弹性.减轻基础设施的维护压力,运维的成本等.很多企业使用云碰到的难题之一是如何将现有的应用迁移到云上,将现有应用的中间件系统.Web系统及其 ...
- Android Webview 调用JS跳转到指定activity
JAVA: WebView wv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(save ...
- HTML5中canvas大小调整
今天用到canvas元素,发现它的大小不是像普通dom元素一样,直接设置css样式可以改变的,它会由自己原本的大小伸缩. 例如, <canvas id='canvas'></canv ...
- 【Mail】邮件的基础知识和原理
电子邮件概念 电子邮件是-种用电子手段提供信息交换的通信方式,是互联网应用最广的服务.通过网络的电子邮件系统,用户可以以非常低廉的价格(不管发送到哪里,都只需负担网费).非常快速的方式(几秒钟之内可以 ...
- 没有QQ的日子
说来,也怪电脑不好,一开QQ就卡,年级也不小了,QQ上真的没啥话好说的,所以就想着关闭QQ. 其实做软件的知道,很多事情不是订下规则就可以做的到的,不过我还是给自己定个规则: 过完农历年后就不用QQ了 ...
- Swift基础语法(三)
Array数组在swift中Array相当于oc中的NSMutableArray(变长数组) //1.定义数组 var numarr:Int[] = [,,,,]; var strarr:String ...
- innodb的存储结构
如下所示,innodb的存储结构包含:表空间,段,区,页(块),行 innodb存储结构优化的标准是:一个页里面存放的行数越多,其性能越高 表空间:零散页+段 独立表空间存放的是:数据.索引.插入缓冲 ...
- Win7 64位 VS2015环境使用qt-msvc2015-5.6.0
QT下载 http://www.qt.io/download-open-source/#section-2 我用的是 qt-opensource-windows-x86-msvc2015-5.6.0. ...
- js压缩
1:用cmd命名 1.1:cmd下执行命令:"copy dialog.js+menu.js abc.js/b",则会合并dialog合menu两个js到新生成的abc.js; 1. ...
- WPF 图片显示中的保留字符问题
在WPF中显示一张图片,本是一件再简单不过的事情.一张图片,一行XAML代码即可. 但是前段时间遇到了一件奇怪的事: 开发机上运行正常的程序,在某些客户机器上却显示不了图片,而且除了这个问题,其它运行 ...