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>腦圖:
歡迎各位有志之士前來交流探討!
來源:我的小樹林