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的事件机制, ...
随机推荐
- dictionaryWithContentsOfFile:方法
dictionaryWithContentsOfFile:方法的功能是创建一个字典,将字典中的内容设置为指定文件中的所有内容, 语法:(id)dictionaryWithContentsOffilE. ...
- 数据库.bak文件还原报错的处理办法
今天从网上下了个Demo,里面有个.bak文件,就试着还原了一下,结果发现报了错.是了两种方式导入,都不行. 最终找到了解决办法: 可以直接用sql语句对.bak文件进行还原. RESTORE DAT ...
- vs快捷键visual studio
网上抄的.记录下来.没全试过!强大的VS,真的喜欢! Shift+Alt+Enter: 切换全屏编辑 Ctrl+B,T / Ctrl+K,K: 切换书签开关Ctrl+B,N / Ctrl+K,N: 移 ...
- 通过反射及注解的运用获取SQL语句
import java.lang.reflect.*; public class BeanUtil { //这是拼接查询SQL语句的方法(getDelectSQL) public static Str ...
- C#中的using和yield return混合使用
最近写代码为了为了省事儿用了几个yield return,因为我不想New一个List<T>或者T[]对象再往里放元素,就直接返回IEnumerable<T>了.我的代码里还有 ...
- UML类图6种关系的总结
http://www.open-open.com/lib/view/open1328059700311.html
- 解决EasyUI动态添加标签渲染问题
以下代码用于Js脚本中: var Work_Content_Back = "<table width='99%' class='table' style='margin-bottom: ...
- js时间处理函数
Date 对象的方法简介: ·Date | 返回当日的日期和时间 ·getDate | 从 Date 对象返回一个月中的某一天 (1 ~ 31) ·getDay | 从 Date 对象返回一周中 ...
- css3 自定义动画(1)
<style> /*@-webkit-keyframes 动画名称 {} 用时:-webkit-animation:时间 动画名称; */ /* @-webkit-keyframes mo ...
- iOS App打包上架的流程
一.申请苹果开发者账号 首先需要申请苹果开发者账号才能在APP store 里发布应用. 开发者账号分为:(1)个人开发者账号 (2)企业开发者账号 主要的区别是:点击打开链接 1.个人开发者 ...