如何優雅的封裝一個DOM事件庫

来源:https://www.cnblogs.com/yuliangbin/archive/2018/08/11/9460917.html
-Advertisement-
Play Games

1、DOM0級事件和DOM2級事件 DOM 0級事件是元素內的一個私有屬性:div.onclick = function () {},對一個私有屬性賦值(在該事件上綁定一個方法)。由此可知DOM 0級事件只能給元素的某一個行為綁定一次方法,第二次綁定會把前面的覆蓋掉。 DOM 2級事件是讓DOM元素 ...


1、DOM0級事件和DOM2級事件

DOM 0級事件是元素內的一個私有屬性:div.onclick = function () {},對一個私有屬性賦值(在該事件上綁定一個方法)。由此可知DOM 0級事件只能給元素的某一個行為綁定一次方法,第二次綁定會把前面的覆蓋掉。

DOM 2級事件是讓DOM元素通過原型鏈一直找到EventTarget這個內置類原型上的addEventListener方法來實現的。

DOM 2可以給某一個元素的同一個行為綁定多個不同的方法

//實例 1
obj.addEventListener(事件類型 , 處理函數 , false)
//IE9以下不相容,可以為一個事件綁定多個處理程式,並且按照綁定時的順序去執行
//實例2
div.addEventListener('click' , function f() {} , false); //1
div.addEventListener('click' , function f() {} , false); //2
//事件1和事件2雖然執行的函數一樣,但是函數f()的地址不一樣,所以是2個處理函數,執行2次
//實例3
function f() {};
div.addEventListener('click' , f , false); //1
div.addEventListener('click' , f , false); //2
//事件1和事件2執行的函數都是f(),但因為地址一樣,所以只執行一次。即某一個元素的同一個行為只能綁定一次相同的方法

1.1、DOMContentLoaded和loaded

DOM 2還提供了DOM 0中沒有的行為類型 -> DOMContentLoaded:當頁面中的DOM結構(HTML結構)載入完成觸發的行為。而onload事件則是當頁面中的所有資源全部載入完成(圖片、html結構、音視頻...)才會被執行。

jQuery中的$(document).ready(function () {}),等價於$(function () {}),事件原理就是DOM2中新增的DOMContentLoaded事件。

1.2、事件的移除

DOM 0級事件的移除:div.onclick = null;

DOM 2級事件的移除:

function fn() {};
div.addEventListener('click',fn,false);
div.removeEventListener('click',fn,false);

1.3、DOM2事件機制

  • 只能給某個元素的同一個行為綁定多個"不同"的方法
  • 當行為觸發,會按照綁定的先後順序把綁定的方法執行
  • 執行的方法中的this是當前綁定事件的元素本身

1.4、IE6-8下的事件機制

在IE6-8瀏覽器中,不支持addEventListener/removeEventLiatener,如果想實現DOM 2事件綁定,只能用attachEvent(),移除用detachEvent()。

 obj.attachEvent('on'+type , func);只能在冒泡階段發生,一個事件同樣可以綁定多個處理函數。與obj.addEventListener('type' , func , false)不一樣的是,即使函數的地址是一樣的,綁定多少次就執行多少次。即同一個函數可以綁定多次

與標準瀏覽器的事件池機制對比:

  • this問題:IE6-8中當方法執行的時候,方法中的this不是當前元素,而指的是window
  • 重覆問題:可以給同一個元素的同一個行為綁定多個相同的方法
  • 順序問題:執行的時候順序是混亂的,標準瀏覽器是按照綁定順序依次執行

2、處理this問題

/*
 *    bind: 處理DOM2級事件綁定的相容性問題
 *    @parameter:
 *    curEle: 要綁定事件的元素
 *    eventType: 要綁定的事件類型('click','mouseover'...)
 *    eventFn: 要綁定的方法
 */
 var tempFn = {};
 function bind(curEle,eventType,eventFn){
    if ("addEventListener" in document) {//標準瀏覽器
        curEle.addEventListener(eventType,eventFn,false);
        return ;
    }
    var tempFn[eventFn] = function () {
        eventFn.call(curEle);
    };
    curEle.attachEvent("on" + eventType,tempFn);
 }
 
 function unbind(curEle,eventType,eventFn) {
    if ("removeEventListener" in document) {
        curEle.removeEventListener(eventType,eventFn,false);
        return ;
    }
    curEle.detachEvent("on" + eventType,tempFn[eventFn]);
 }

//分析
 
// 1、知若想改變IE下事件執行函數的this的指向,可以在函數執行的時候改變this,即用函數eventFn.call('curEle'),這樣雖然解決了this指向的問題,
   但又拋出了一個新的問題:即不知道該如何移除該事件函數,因為綁定的是一個匿名函數,而匿名函數的地址我們是無法知道的。
  所以要先把匿名函數定義時的地址賦值給一個變數temp var tempFn = function () { eventFn.call('curEle'); }; //2、為什麼要把tempFn設置成一個全局變數 若tempFn不是一個全局變數,而是寫在函數內部的私有變數,而私有變數只能在函數內部進行訪問,
所以我們在bind()函數里的tempFn在unbind()函數里是不能訪問的,因此也就不能移除該事件函數。所以若想移除該事件函數,tempFn就必須是全局變數

拓展:我們知道:寫在函數內部的變數是私有變數,一個函數的私有變數只能在函數的內部進行訪問。

  • 若在一個函數里要用到另一個函數里的變數,可以把該變數設置成全局的變數,這樣兩個函數都可以訪問到。(這可能會造成全局污染)
  • 若幾個函數的作用是為同一個/同一類元素提供方法去使用,且不同方法中要用到其它方法里的變數等,那麼可以用該元素的自定義變數來存儲這些變數,就可以實現在不同方法中的訪問。(不會造成全局污染)

上面的代碼除了全局變數可能造成污染外。還有一種不得不考慮的情況就是:當為不同的事件綁定方法時,不同的事件可能執行相同的方法(如mouseover 和 click 都執行fn1方法時),如果仍然將這些方法存儲在一起,那麼移除某一類事件的方法時就可能出錯,因此我們需要為不同的事件創建不同的數組來存儲綁定在其上的方法。

所以我們需要對代碼進行進一步的優化。

function bind(curEle,eventType,eventFn){
        ...
        var tempFn = function () {
            eventFn.call('curEle');
        };
        tempFn.photo = eventFn;//給傳入的每一個函數做一個唯一標識
        //首先判斷該自定義屬性之前是否存在,不存在的話創建一個,由於要存儲多個方法,所以我們讓其值是一個數組
        //為什麼要對不同的事件類型創建不同的數組呢,因為不同的事件可能執行相同的方法。如mouseover 和 click 都執行fn1方法時,移除的時候就可能出錯
        if (!curEle['bindFn' + eventType]) {
            curEle['bindFn' + eventType] = [];
        }
        curEle['bindFn' + eventType].push(tempFn);
        curEle.attachEvent("on" + eventType,tempFn);
    }
 
    function unbind(curEle,eventType,eventFn) {
        ...
        var arr = curEle['bindFn' + eventType];
        for (var i = 0; i < arr.length; i ++) {
            if (arr[i].photo === eventFn) {
                arr.splice(i,1);//找到後,把自己存儲容器中對應的移除掉,與事件池中保持一致
                curEle.detachEvent("on" + eventType,arr[i]);//把事件池中對應的方法移除掉
                break;
            }
        }
    }

3、處理重覆問題

function bind(curEle,eventType,eventFn){
        if ("addEventListener" in document) {
            //省略代碼
        }
        //省略代碼
        //處理重覆問題:如果每一次往自定義屬性添加方法前,看一下是否已經有了,有的話就不用重覆添加,同理,也就不用往事件池裡存儲了
        var arr = curEle['bindFn' + eventType];
        for (var i = 0; i < arr.length ;i ++) {
            if (arr[i].photo === eventFn) {
                return ;
            }
        }
        arr.push(tempFn);
        curEle.attachEvent("on" + eventType,tempFn);
    }

4、處理順序問題

我們知道在IE6-8下,事件的執行順序是無序的,這是由瀏覽器的事件池機制所決定的。所以要改善這個問題,我們模仿標準瀏覽器的事件執行順序,可以自己寫一個事件池來使方法的執行順序有序執行。聽起來有點繞,我們來看一下具體的實現就清楚了。

  //創建自己的事件池,並把需要給當前元素綁定的方法依次增加到事件池中
    function on(curEle,eventType,eventFn) {
        if (!curEle['myEvent' + eventType]) {
            curEle['myEvent' + eventType] = [];
        }
        var arr = curEle['myEvent' + eventType];
        for (var i = 0; i < arr.length; i ++) {
            if (arr[i] === eventFn) return ;
        }
        arr.push(eventFn)
        bind(curEle,eventType,run);//把run方法綁定到自定義的bind()函數中,這個bind函數解決了this指向和重覆問題。因此綁定後run方法的this指向當前點擊元素
    }
 
   //在自己的事件池中把某一個方法移除
    function off(curEle,eventType,eventFn) {
        var arr = curEle['myEvent' + eventType];
        for (var i = 0; i < arr.length; i ++) {
            if (arr[i] === eventFn) {
                arr.splice(i,1);
            }
        }
    }
 
    //由於IE6-8瀏覽器DOM2級事件執行多個綁定方法時會出現順序混亂,我們就只給它綁定一個run方法,然後在run方法里執行事件池on里綁定的方法。
    function run(event) {
        event = event || window.event;
        var flag = event.target ? true :false ;//IE6-8下不相容event.target
        if (!flag) {//做非相容處理
            event.target = window.srcElement;
            event.pageX = event.clientX + document.documentElement.scrollLeft;
            event.pageY = event.clentY +document.documentElement.scrollTop;
            event.preventDefault = function () {
                event.returnValue = false ;
            }
            event.stopPropagation = function () {
                event.cancleBubble = true ;
            }
        }
        //獲取事件池中綁定的方法,並且讓這些方法依次執行
        var arr = event.target['myEvent' + event.type];
        for (var i = 0; i < arr.length; i ++) {
            arr[i].call(event.target,event);//把事件對象傳遞給當前執行的函數
        }    
    }

5、一個完整的DOM庫

以上就是對封裝整個DOM庫的思考,可見分析的整個過程是多麼的煎熬。然而整個的DOM庫封裝後,代碼卻少的可憐。我們一起來看一下。

//綁定事件
function
on(ele,type,fn) { if(ele.addEventListener) { ele.addEventListener(type,fn,false); } else{ if (!ele['myEvent' + type]) { ele['myEvent' + type] = []; ele.attachEvent('on' + type,function(){//在這裡綁定run方法 run.call(ele); }) } let arr = ele['myEvent' + type]; for(let i = 0; i < arr.length; i++) { if (arr[i] == fn) { return; } } arr.push(fn); } }
//解決IE下事件執行順序的run方法
function run() { let e = window.event;//在IE6-8下,事件對象是存儲在全局的event屬性上的 e.target = e.srcElement; e.preventDefault = function () { e.returnValue = false ; } e.stopPropagation = function () { e.cancleBubble = true ; } let arr = this['myEvent' + event.type]; for(let i = 0; i < arr.length; i++) { if(arr[i] == null) {//在這裡刪除被解綁的方法 arr.splice(i,1); i--; } arr[i].call(this,event); } }
//解除事件
function off(ele,type,fn) { if (ele.removeEventListener) { ele.removeEventListener(type,fn,false); } else { let arr = ele["myEvent" + type]; for(let i = 0; i < arr.length; i++) { if (arr[i] == fn) { arr[i] = null; //arr.splice(i,1);這裡為什麼不能直接刪除掉,而是要用null來占位。答案是:為了不改變arr的長度。使run能正確執行。 return; } } } }

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Preface I've implemented ProxySQL on PXC yesterday but got some errors when configured query rules.I'm gonna do it again in my master-slave environmen ...
  • Android Keyguard自Android L以來一直沒有多大變化。在Android L之前, Keyguard有自己獨立的進程,KeyguardService自開機時啟動並常駐。Android L之後到Android P,Keyguard和SystemUI共同享有一個進程。process i ...
  • 註:由於我是物聯網專業的大二學生,平時學習較多的是嵌入式編程方面的知識,對於網頁,手機Android客戶端從來沒有接觸過,因此所有東西都要從頭來過,慢慢學習。 每次學習新的東西都很激動,仿佛自己又離改變世界近了一點(純屬自己意淫),好了廢話不多說,開始我的微信小程式之旅! 1.API: (Appli ...
  • 一.前言 眾所周知:沒有對象怎麼辦?那就new一個! 那麼在JS中,當我們new一個對象的時候,這個new關鍵字內部都幹了什麼呢?現在我們就來剖析一下原生JS中new關鍵字內部的工作原理。 二.原始的new 首先,我們先new一個對象看看: 列印結果: 從列印結果中可以看到: 用new關鍵字實例化對 ...
  • 1.js的數據類型: Number、Boolean、String、Undefined、Null、Symbol(es6新定義的)和Object(ps:Array是特殊的Object) typeof返回6種類型:number boolean string object undefined functio ...
  • 眾所周知,可以通過設置background-repeat的值來改變背景圖片的重覆次數。但有一個問題,background-repeat的值不是讓圖片只有1個,就是讓圖片鋪滿。如果只想設置給定數量的圖片該怎麼辦?以2個為例,請看代碼: 這樣,就實現了<h1>元素的最左邊有一個logo.jpg、最右邊也 ...
  • 一.不可改變的原始值(棧數據)(五個) 數字(number),字元串(string),布爾值(boolean),undefined,null 其中;undefined是未定義的意思,而null是空的意思,他們倆的區別在於,null有值,不過這個值是空值,而undefined是未定義,完全沒有值的意思 ...
  • 遞歸演算法: 優點:代碼簡潔、清晰,並且容易驗證正確性。 缺點: 1、它的運行需要較多次數的函數調用,如果調用層數比較深,每次都要創建新的變數,需要增加額外的堆棧處理,會對執行效率有一定影響,占用過多的記憶體資源。 2、遞歸演算法解題的運行效率較低。在遞歸調用的過程中系統為每一層的返回點、局部變數等開闢了 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...