1. 程式人生 > >二十二、事件綁定及深入

二十二、事件綁定及深入

dom節點 bili rec tex lin 問題: type onload fin

二十二、事件綁定及深入

事件綁定分為兩種:一種是傳統事件綁定(內聯模型,腳本模型),一種是現代事件綁定(DOM2級模型)。現代事件綁定在傳統綁定上提供了更強大更方便的功能。

1.傳統事件綁定的問題

傳統事件綁定有內聯模型和腳本模型,內聯模型我們不做討論,基本很少去用。先來看一下腳本模型,腳本模型將一個函數賦值給一個事件處理函數。

var box = document.getElementById(‘box‘); //獲取元素

box.onclick = function () { //元素點擊觸發事件

alert(‘Lee‘);

};

問題一:一個事件處理函數觸發兩次事件

  window.onload = function () { //第一組程序項目或第一個JS文件

  alert(‘Lee‘);

  };

  window.onload = function () { //第二組程序項目或第二個JS文件

   alert(‘Mr.Lee‘);

  };

當兩組程序或兩個JS文件同時執行的時候,後面一個會把前面一個完全覆蓋掉。導致前面的window.onload完全失效了。

解決覆蓋問題,我們可以這樣去解決:

  window.onload = function () { //第一個要執行的事件,會被覆蓋

   alert(‘Lee‘);

  };

  if (typeof window.onload == ‘function‘) { //判斷之前是否有window.onload

  var saved = null; //創建一個保存器

   saved = window.onload; //把之前的window.onload保存起來

  }

  window.onload = function () { //最終一個要執行事件

   if (saved) saved(); //執行之前一個事件

  alert(‘Mr.Lee‘); //執行本事件的代碼

  };

問題二:事件切換器

  box.onclick = toBlue; //第一次執行boBlue()

  function toRed() {

  this.className = ‘red‘;

  this.onclick = toBlue; //第三次執行toBlue(),然後來回切換

  }

  function toBlue() {

  this.className = ‘blue‘;

   this.onclick = toRed; //第二次執行toRed()

  }

這個切換器在擴展的時候,會出現一些問題:

1.如果增加一個執行函數,那麽會被覆蓋

box.onclick = toAlert; //被增加的函數

box.onclick = toBlue; //toAlert被覆蓋了

2.如果解決覆蓋問題,就必須包含同時執行,但又出新問題

box.onclick = function () { //包含進去,但可讀性降低

toAlert(); //第一次不會被覆蓋,但第二次又被覆蓋

toBlue.call(this); //還必須把this傳遞到切換器裏

};

綜上的三個問題:覆蓋問題、可讀性問題、this傳遞問題。我們來創建一個自定義的事件處理函數,來解決以上三個問題。

function addEvent(obj, type, fn) { //取代傳統事件處理函數

var saved = null; //保存每次觸發的事件處理函數

if (typeof obj[‘on‘ + type] == ‘function‘) { //判斷是不是事件

saved = obj[‘on‘ + type]; //如果有,保存起來

}

obj[‘on‘ + type] = function () { //然後執行

  if (saved) saved(); //執行上一個

fn.call(this); //執行函數,把this傳遞過去

};

}

addEvent(window, ‘load‘, function () { //執行到了

alert(‘Lee‘);

});

addEvent(window, ‘load‘, function () { //執行到了

alert(‘Mr.Lee‘);

});

PS:以上編寫的自定義事件處理函數,還有一個問題沒有處理,就是兩個相同函數名的函數誤註冊了兩次或多次,那麽應該把多余的屏蔽掉。那,我們就需要把事件處理函數進行遍歷,如果有同樣名稱的函數名就不添加即可。(這裏就不做了)

  addEvent(window, ‘load‘, init); //註冊第一次

  addEvent(window, ‘load‘, init); //註冊第二次,應該忽略

  function init() {

   alert(‘Lee‘);

  }

用自定義事件函數註冊到切換器上查看效果:

  addEvent(window, ‘load‘, function () {

   var box = document.getElementById(‘box‘);

  addEvent(box, ‘click‘, toBlue);

  });

  function toRed() {

   this.className = ‘red‘;

   addEvent(this, ‘click‘, toBlue);

  }

  function toBlue() {

   this.className = ‘blue‘;

   addEvent(this, ‘click‘, toRed);

  }

PS:當你單擊很多很多次切換後,瀏覽器直接卡死,或者彈出一個錯誤:too much recursion(太多的遞歸)。主要的原因是,每次切換事件的時候,都保存下來,沒有把無用的移除,導致越積越多,最後卡死。

  function removeEvent(obj, type) {

   if (obj[‘on‘] + type) obj[‘on‘ + type] = null; //刪除事件處理函數

  }

以上的刪除事件處理函數只不過是一刀切的刪除了,這樣雖然解決了卡死和太多遞歸的問題。但其他的事件處理函數也一並被刪除了,導致最後得不到自己想要的結果。如果想要只刪除指定的函數中的事件處理函數,那就需要遍歷,查找。(這裏就不做了)

2.W3C事件處理函數

“DOM2級事件”定義了兩個方法,用於添加事件和刪除事件處理程序的操作:addEventListener()和removeEventListener()。所有DOM節點中都包含這兩個方法,並且它們都接受3個參數;事件名、函數、冒泡或捕獲的布爾值(true表示捕獲,false表示冒泡)。

  window.addEventListener(‘load‘, function () {

   alert(‘Lee‘);

  }, false);

  window.addEventListener(‘load‘, function () {

   alert(‘Mr.Lee‘);

  }, false);

PS:W3C的現代事件綁定比我們自定義的好處就是:1.不需要自定義了;2.可以屏蔽相同的函數;3.可以設置冒泡和捕獲。

  window.addEventListener(‘load‘, init, false); //第一次執行了

  window.addEventListener(‘load‘, init, false); //第二次被屏蔽了

  function init() {

  alert(‘Lee‘);

  }

事件切換器

  window.addEventListener(‘load‘, function () {

   var box = document.getElementById(‘box‘);

    box.addEventListener(‘click‘, function () { //不會被誤刪

      alert(‘Lee‘);

    }, false);

   box.addEventListener(‘click‘, toBlue, false); //引入切換也不會太多遞歸卡死

  }, false);

  function toRed() {

   this.className = ‘red‘;

   this.removeEventListener(‘click‘, toRed, false);

   this.addEventListener(‘click‘, toBlue, false);

  }

  function toBlue() {

   this.className = ‘blue‘;

   this.removeEventListener(‘click‘, toBlue, false);

   this.addEventListener(‘click‘, toRed, false);

  }

設置冒泡和捕獲階段

之前我們上一章了解了事件冒泡,即從裏到外觸發。我們也可以通過event對象來阻止某一階段的冒泡。那麽W3C現代事件綁定可以設置冒泡和捕獲。

document.addEventListener(‘click‘, function () {

alert(‘document‘);

}, true); //把布爾值設置成true,則為捕獲

box.addEventListener(‘click‘, function () {

alert(‘Lee‘);

}, true); //把布爾值設置成false,則為冒泡

3.IE事件處理函數

IE實現了與DOM中類似的兩個方法:attachEvent()和detachEvent()。這兩個方法接受相同的參數:事件名稱和函數。

在使用這兩組函數的時候,先把區別說一下:1.IE不支持捕獲,只支持冒泡;2.IE添加事件不能屏蔽重復的函數;3.IE中的this指向的是window而不是DOM對象。4.在傳統事件上,IE是無法接受到event對象的,但使用了attchEvent()卻可以,但有些區別。

  window.attachEvent(‘onload‘, function () {

  var box = document.getElementById(‘box‘);

   box.attachEvent(‘onclick‘, toBlue);

  });

  function toRed() {

   var that = window.event.srcElement;

   that.className = ‘red‘;

  that.detachEvent(‘onclick‘, toRed);

  that.attachEvent(‘onclick‘, toBlue);

  }

  function toBlue() {

   var that = window.event.srcElement;

   that.className = ‘blue‘;

   that.detachEvent(‘onclick‘, toBlue);

   that.attachEvent(‘onclick‘, toRed);

  }

PS:IE不支持捕獲,無解。IE不能屏蔽,需要單獨擴展或者自定義事件處理。IE不能傳遞this,可以call過去。

  window.attachEvent(‘onload‘, function () {

   var box = document.getElementById(‘box‘);

   box.attachEvent(‘onclick‘, function () {

   alert(this === window); //this指向的window

   });

  });

  window.attachEvent(‘onload‘, function () {

  var box = document.getElementById(‘box‘);

  box.attachEvent(‘onclick‘, function () {

   toBlue.call(box); //把this直接call過去

   });

  });

  function toThis() {

  alert(this.tagName);

  }

在傳統綁定上,IE是無法像W3C那樣通過傳參接受event對象,但如果使用了attachEvent()卻可以。

box.onclick = function (evt) {

alert(evt); //undefined

}

box.attachEvent(‘onclick‘, function (evt) {

alert(evt); //object

    alert(evt.type); //click

});

  box.attachEvent(‘onclick‘, function (evt) {

    alert(evt.srcElement === box); //true

alert(window.event.srcElement === box); //true

});

最後,為了讓IE和W3C可以兼容這個事件切換器,我們可以寫成如下方式:

function addEvent(obj, type, fn) { //添加事件兼容

if (obj.addEventListener) {

obj.addEventListener(type, fn);

} else if (obj.attachEvent) {

obj.attachEvent(‘on‘ + type, fn);

}

}

function removeEvent(obj, type, fn) { //移除事件兼容

if (obj.removeEventListener) {

obj.removeEventListener(type, fn);

} else if (obj.detachEvent) {

obj.detachEvent(‘on‘ + type, fn);

}

}

function getTarget(evt) { //得到事件目標

if (evt.target) {

return evt.target;

} else if (window.event.srcElement) {

return window.event.srcElement;

}

}

PS:調用忽略,IE兼容的事件,如果要傳遞this,改成call即可。

PS:IE中的事件綁定函數attachEvent()和detachEvent()可能在實踐中不去使用,有幾個原因:1.IE9就將全面支持W3C中的事件綁定函數;2.IE的事件綁定函數無法傳遞this;3.IE的事件綁定函數不支持捕獲;4.同一個函數註冊綁定後,沒有屏蔽掉;5.有內存泄漏的問題。至於怎麽替代,我們將在以後的項目課程中探討。

4.事件對象的其他補充

在W3C提供了一個屬性:relatedTarget;這個屬性可以在mouseover和mouseout事件中獲取從哪裏移入和從哪裏移出的DOM對象。

box.onmouseover = function (evt) { //鼠標移入box

alert(evt.relatedTarget); //獲取移入box最近的那個元素對象

} //span

  box.onmouseout = function (evt) { //鼠標移出box

alert(evt.relatedTarget); //獲取移出box最近的那個元素對象

} //span

IE提供了兩組分別用於移入移出的屬性:fromElement和toElement,分別對應mouseover和mouseout。

box.onmouseover = function (evt) { //鼠標移入box

alert(window.event.fromElement.tagName); //獲取移入box最近的那個元素對象span

}

box.onmouseout = function (evt) { //鼠標移入box

alert(window.event.toElement.tagName); //獲取移入box最近的那個元素對象span

}

PS:fromElement和toElement如果分別對應相反的鼠標事件,沒有任何意義。

剩下要做的就是跨瀏覽器兼容操作:

function getTarget(evt) {

var e = evt || window.event; //得到事件對象

if (e.srcElement) { //如果支持srcElement,表示IE

if (e.type == ‘mouseover‘) { //如果是over

return e.fromElement; //就使用from

} else if (e.type == ‘mouseout‘) { //如果是out

return e.toElement; //就使用to

}

} else if (e.relatedTarget) { //如果支持relatedTarget,表示W3C

return e.relatedTarget;

}

}

有時我們需要阻止事件的默認行為,比如:一個超鏈接的默認行為就點擊然後跳轉到指定的頁面。那麽阻止默認行為就可以屏蔽跳轉的這種操作,而實現自定義操作。

取消事件默認行為還有一種不規範的做法,就是返回false。

link.onclick = function () {

alert(‘Lee‘);

    return false; //直接給個假,就不會跳轉了。

};

PS:雖然return false;可以實現這個功能,但有漏洞;第一:必須寫到最後,這樣導致中間的代碼執行後,有可能執行不到return false;第二:return false寫到最前那麽之後的自定義操作就失效了。所以,最好的方法應該是在最前面就阻止默認行為,並且後面還能執行代碼。

link.onclick = function (evt) {

evt.preventDefault(); //W3C,阻止默認行為,放哪裏都可以

alert(‘Lee‘);

};

link.onclick = function (evt) { //IE,阻止默認行為

window.event.returnValue = false;

alert(‘Lee‘);

};

跨瀏覽器兼容

function preDef(evt) {

var e = evt || window.event;

if (e.preventDefault) {

e.preventDefault();

} else {

e.returnValue = false;

}

}

上下文菜單事件:contextmenu,當我們右擊網頁的時候,會自動出現windows自帶的菜單。那麽我們可以使用contextmenu事件來修改我們指定的菜單,但前提是把右擊的默認行為取消掉。

addEvent(window, ‘load‘, function () {

var text = document.getElementById(‘text‘);

addEvent(text, ‘contextmenu‘, function (evt) {

var e = evt || window.event;

preDef(e);

var menu = document.getElementById(‘menu‘);

menu.style.left = e.clientX + ‘px‘;

menu.style.top = e.clientY + ‘px‘;

menu.style.visibility = ‘visible‘;

addEvent(document, ‘click‘, function () {

document.getElementById(‘myMenu‘).style.visibility = ‘hidden‘;

});

});

});

PS:contextmenu事件很常用,這直接導致瀏覽器兼容性較為穩定。

卸載前事件:beforeunload,這個事件可以幫助在離開本頁的時候給出相應的提示,“離開”或者“返回”操作。

addEvent(window, ‘beforeunload‘, function (evt) {

preDef(evt);

});

鼠標滾輪(mousewheel)和DOMMouseScroll,用於獲取鼠標上下滾輪的距離。

addEvent(document, ‘mousewheel‘, function (evt) { //非火狐

alert(getWD(evt));

});

addEvent(document, ‘DOMMouseScroll‘, function (evt) { //火狐

alert(getWD(evt));

});

function getWD(evt) {

var e = evt || window.event;

if (e.wheelDelta) {

return e.wheelDelta;

} else if (e.detail) {

return -evt.detail * 30; //保持計算的統一

}

}

PS:通過瀏覽器檢測可以確定火狐只執行DOMMouseScroll。

二十二、事件綁定及深入