[26]事件綁定及深刻

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

一. 傳統事件綁定的問題

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

var box = document.getElementById('box'); //獲取元素
    box.onclick = function () { //元素點擊觸發事件
    alert('Lee');
};

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

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

window.onload = function () { //第二組程序項目或第二個 JS 文件
    alert('Mr.Lee');
};

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

解決覆蓋問題,咱們能夠這樣去解決:瀏覽器

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

if(typeofwindow.onload == 'function') { //判斷以前是否有 window.onload
    var saved = null; //建立一個保存器
    saved = window.onload; //把以前的 window.onload 保存起來
}

window.onload = function () { //最終一個要執行事件
    if(saved) saved(); //執行以前一個事件
    alert('Mr.Lee'); //執行本事件的代碼
};

問題二:事件切換器markdown

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.若是解決覆蓋問題,就必須包含同時執行,但又出新問題this

box.onclick = function () { //包含進去,但可讀性下降
toAlert(); //第一次不會被覆蓋,但第二次又被覆蓋
toBlue.call(this); //還必須把 this 傳遞到切換器裏
};

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

function addEvent(obj, type, fn) { //取代傳統事件處理函數
    var saved = null; //保存每次觸發的事件處理函數
    if(typeofobj['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:以上編寫的自定義事件處理函數,還有一個問題沒有處理,就是兩個相同函數名的函數誤註冊了兩次或屢次,那麼應該把多餘的屏蔽掉。那,咱們就須要把事件處理函數進行遍歷,若是有一樣名稱的函數名就不添加便可。(這裏就不作了)code

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 muchrecursion(太多的遞歸)。主要的緣由是,每次切換事件的時候,都保存下來,沒有把無用的移除,致使越積越多,最後卡死。

function removeEvent(obj, type) {
    if(obj['on'] + type) obj['on' + type] = null; //刪除事件處理函數
}

以上的刪除事件處理函數只不過是一刀切的刪除了,這樣雖然解決了卡死和太多遞歸的問題。但其餘的事件處理函數也一併被刪除了,致使最後得不到本身想要的結果。若是想要只刪除指定的函數中的事件處理函數,那就須要遍歷,查找。(這裏就不作了)

二. 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,則爲冒泡

三. 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.有內存泄漏的問題 。至於怎麼替代,咱們將在之後的項目課程中探討。

四. 事件對象的其餘補充

在 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。

DOMContentLoaded 事件和readystatechange 事件,有關 DOM 加載方面的事件,關於這兩個事件的內容很是多且繁雜。

相關文章
相關標籤/搜索