task0002(二)- DOM + 事件

轉載自個人我的博客javascript

歡迎你們批評指正html

DOM

添加class、移除class、是否同級元素、獲取元素位置

先來一些簡單的,在你的util.js中完成如下任務:前端

// 爲element增長一個樣式名爲newClassName的新樣式
function addClass(element, newClassName) {
    // your implement
}

// 移除element中的樣式oldClassName
function removeClass(element, oldClassName) {
    // your implement
}

// 判斷siblingNode和element是否爲同一個父元素下的同一級的元素,返回bool值
function isSiblingNode(element, siblingNode) {
    // your implement
}

// 獲取element相對於瀏覽器窗口的位置,返回一個對象{x, y}
function getPosition(element) {
    // your implement
}
// your implement

思路:

  1. 其實這裏能夠先定義一個hasClass函數。用來判斷該節點是否含有某個className。java

    • addClass添加樣式。調用hasClass函數,判斷element是否含有待添加的新className,若沒有則添加,不然什麼都不作。git

    • removeClass刪除樣式。調用hasClass函數,判斷element是否含有該指定樣式,若含有的話刪除該className。沒有的話什麼都不作。github

  2. 判斷siblingNode和element是否爲同一個父元素下的同一級的元素。這裏直接判斷parentNode就能夠了吧數組

  3. 獲取element相對於瀏覽器窗口的位置,返回一個對象{x, y}。瀏覽器

    • 這個題應該是這幾個中比較複雜的一個了。由於不能直接使用offsetLeft/TopoffsetLeft/Top所獲取的是其相對父元素的相對位置。當多層定位嵌套時想要獲取到當前元素相對網頁的位置就會不對。緩存

    • 而且因爲在表格iframe中,offsetParent對象未必等於父容器,因此也不能直接利用該元素的parent來獲取位置,由於其對於表格iframe中的元素不適用。ruby

    • 經過查詢知道有一個Element.getBoundingClientRect()方法。它返回一個對象,其中包含了left、right、top、bottom四個屬性,分別對應了該元素的左上角和右下角相對於瀏覽器窗口(viewport)左上角的距離。

    • 可是用該方法獲取到的是元素的相對位置,在出現滾動時,距離會發生改變,要得到絕對位置時,還須要加上滾動的距離。由於Firefox或Chrome的不兼容問題須要進行兼容性處理,參考document.body.scrollTop or document.documentElement.scrollTop

    • 最終根據兩個值,獲得絕對位置。

//其實也簡單,只須要獲取到兩個值,取其中的最大值便可。
var scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
var scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);

實現:

//判斷element中是否含有className爲sClass。
function hasClass(element, sClass) {
    return element.className.match(new RegExp("(\\s|^)" + sClass + "(\\s|$)"));
}

// 爲element增長一個樣式名爲newClassName的新樣式
function addClass(element, newClassName) {
    if (!hasClass(element, newClassName)) {
        element.className += " " + newClassName;
    }
}

// 移除element中的樣式oldClassName
function removeClass(element, oldClassName) {
    if (hasClass(element, oldClassName)) {
        var reg = new RegExp("(\\s|^)" + oldClassName + "(\\s|$)");
        element.className = element.className.replace(reg, "");
    }
}

// 判斷siblingNode和element是否爲同一個父元素下的同一級的元素,返回bool值
function isSiblingNode(element, siblingNode) {
    return element.parentNode === siblingNode.parentNode
}

// 獲取element相對於瀏覽器窗口的位置,返回一個對象{x, y}
function getPosition(element) {
    var position = {};
    position.x = element.getBoundingClientRect().left + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);//獲取相對位置+滾動距離=絕對位置.
    position.y = element.getBoundingClientRect().top + Math.max(document.documentElement.scrollTop, document.body.scrollTop);
    return position;
}

參考資料:(還沒看完)

挑戰mini $

接下來挑戰一個mini $,它和以前的$是不兼容的,它應該是document.querySelector的功能子集,在不直接使用document.querySelector的狀況下,在你的util.js中完成如下任務:

// 實現一個簡單的Query
function $(selector) {
    
}

// 能夠經過id獲取DOM對象,經過#標示,例如
$("#adom"); // 返回id爲adom的DOM對象

// 能夠經過tagName獲取DOM對象,例如
$("a"); // 返回第一個<a>對象

// 能夠經過樣式名稱獲取DOM對象,例如
$(".classa"); // 返回第一個樣式定義包含classa的對象

// 能夠經過attribute匹配獲取DOM對象,例如
$("[data-log]"); // 返回第一個包含屬性data-log的對象

$("[data-time=2015]"); // 返回第一個包含屬性data-time且值爲2015的對象

// 能夠經過簡單的組合提升查詢便利性,例如
$("#adom .classa"); // 返回id爲adom的DOM所包含的全部子節點中,第一個樣式定義包含classa的對象

實現思路:
嗯,這個題思考了好久,網上找了不少資料但仍是不怎麼會,還達不到想要的效果,有點鑽牛角尖了。儘可能來寫一下吧。(我果真是個弱雞)。感謝秒味課堂的免費課程。

  1. 題目要求獲取到全部的節點中的第一個,因此不須要用數組來儲存獲取到的節點。

  2. 額。。想了半天,仍是使用函數包裝來實現後代選擇器比較好,因此VQuery函數返回是獲取到的完整節點對象數組,$函數用來達到題目要求。

  3. 因此在VQuery函數中就不須要考慮空格了,直接使用switch分支,來斷定不一樣的狀況。#.[[=]

  4. $函數中,判斷字符串中是否含有空格,有空格的話須要分割成數組,數組的前一項是爲父選擇符,後一項爲子選擇符。分不一樣的狀況來調用VQuery函數,並返回對象。

/**
 * $函數的依賴函數,選擇器函數
 * @param   {string} selector CSS方式的選擇器
 * @param   {object} root     可選參數,selector的父對象。不存在時,爲document
 * @returns {Array}  返回獲取到的節點數組,須要注意的是使用ID選擇器返的也是數組
 */
function VQuery(selector, root) {
    //用來保存選擇的元素
    var elements = []; //保存結果節點數組
    var allChildren = null; //用來保存獲取到的臨時節點數組
    root = root || document; //若沒有給root,賦值document
    switch (selector.charAt(0)) {
    case "#": //id選擇器
        elements.push(root.getElementById(selector.substring(1)));
        break;
    case ".": //class選擇器
        if (root.getElementsByClassName) { //標準
            elements = root.getElementsByClassName(selector.substring(1));
        } else { //兼容低版本瀏覽器
            var reg = new RegExp("\\b" + selector.substring(1) + "\\b");
            allChildren = root.getElementsByTagName("*");
            for (var i = 0, len = allChildren.length; i < len; i++) {
                if (reg.test(allChildren[i].className)) {
                    elements.push(allChildren[i]);
                }
            }
        }
        break;
    case "[": //屬性選擇器
    
        if (selector.indexOf("=") === -1) {
        //只有屬性沒有值的狀況
            allChildren = root.getElementsByTagName("*");
            for (var i = 0, len = allChildren.length; i < len; i++) {
                if (allChildren[i].getAttribute(selector.slice(1, -1)) !== null) {
                    elements.push(allChildren[i]);
                }
            }
        } else {
        //既有屬性又有值的狀況
            var index = selector.indexOf("="); //緩存=出現的索引位置。
            allChildren = root.getElementsByTagName("*");
            for (var i = 0, len = allChildren.length; i < len; i++) {
                if (allChildren[i].getAttribute(selector.slice(1, index)) === selector.slice(index + 1, -1)) {
                    elements.push(allChildren[i]);
                }
            }
        }
        break;
    default: //tagName
        elements = root.getElementsByTagName(selector);
    }
    return elements
}
/**
 * 模仿jQuery的迷你$選擇符。
 * @param   {string} selector CSS方式的選擇器,支持簡單的後代選擇器(只支持一級)
 * @returns {object} 返回獲取到的第一個節點對象,後代選擇器時,返回第一個對象中的第一個符合條件的對象
 */
function $(selector) {
//這裏trim處理輸入時兩端出現空格的狀況,支持ie9+。可是這個函數實現起來也特別簡單,能夠參考我task0002(-)前面有trim函數的實現。稍微修改一下,這樣就沒兼容性問題了。
    if (selector == document) {
        return document;
    }
    selector = selector.trim();
    //存在空格時,使用後代選擇器
    if (selector.indexOf(" ") !== -1) {
        var selectorArr = selector.split(/\s+/); //分割成數組,第一項爲parent,第二項爲chlid。
        //這裏沒去考慮特別多的狀況了,只是簡單的把參數傳入。
        return VQuery(selectorArr[1], VQuery(selectorArr[0])[0])[0];
    } else { //普通狀況,只返回獲取到的第一個對象
        return VQuery(selector,document)[0];
    }
}

事件

事件綁定、事件移除

咱們來繼續用封裝本身的小jQuery庫來實現咱們對於JavaScript事件的學習,仍是在你的util.js,實現如下函數

// 給一個element綁定一個針對event事件的響應,響應函數爲listener
function addEvent(element, event, listener) {
    // your implement
}

// 例如:
function clicklistener(event) {
    ...
}
addEvent($("#doma"), "click", a);

// 移除element對象對於event事件發生時執行listener的響應
function removeEvent(element, event, listener) {
    // your implement
}

這裏慕課網的視頻講的特別清楚,就不贅述了。

/**
 * 事件添加函數
 * @param {object}   element  須要綁定事件的對象
 * @param {string}   event    事件類型
 * @param {function} listener 事件觸發執行的函數
 */
function addEvent(element, event, listener) {
    if (element.addEventListener) { //標準
        element.addEventListener(event, listener, false);
    } else if (element.attachEvent) { //低版本ie
        element.attachEvent("on" + event, listener);
    } else { //都不行的狀況
        element["on" + event] = listener;
    }
}

/**
 * 事件移除函數
 * @param {object}   element  須要移除事件的對象
 * @param {string}   event    事件類型
 * @param {function} listener 須要被移除事件函數
 */
function removeEvent(element, event, listener) {
    // your implement
    if (element.removeEventListener) { //標準
        element.removeEventListener(event, listener, false);
    } else if (element.detachEvent) { //低版本ie
        element.detachEvent("on" + event, listener);
    } else { //都不行的狀況
        element["on" + event] = null;
    }
}

click事件、Enter事件

利用上面寫好的事件綁定函數就很簡單了。

  • click事件,這個簡單,直接函數封裝一層就行。

  • Enter事件,這裏主要考察的鍵盤的事件的觸發。

    1. keydown事件:在鍵盤按下時觸發.

    2. keyup事件:在按鍵釋放時觸發,也就是你按下鍵盤起來後的事件

    3. keypress事件:在敲擊按鍵時觸發,咱們能夠理解爲按下並擡起同一個按鍵

    4. keyCode屬性:在鍵盤事件觸發時,按下的鍵的值。值=13時,爲Enter鍵。(需進行兼容處理)

// 實現對click事件的綁定
function addClickEvent(element, listener) {
    addEvent(element, "click", listener);
}
// 實現對於按Enter鍵時的事件綁定
function addEnterEvent(element, listener) {
    // your implement
    addEvent(element, "keydown", function (ev) {
    //兼容性處理。
        var oEvent = ev || window.event;
        if (oEvent.keyCode === 13) {
            listener();
        }
    });
}

接下來咱們把上面幾個函數和$作一下結合,把他們變成$對象的一些方法

  • addEvent(element, event, listener) -> $.on(element, event, listener);

  • removeEvent(element, event, listener) -> $.un(element, event, listener);

  • addClickEvent(element, listener) -> $.click(element, listener);

  • addEnterEvent(element, listener) -> $.enter(element, listener);

//在js中萬物皆對象(原諒我這麼淺顯的說),因此實現就特別簡單了
$.on = function (element, type, listener) {
    return addEvent(element, type, listener);
};
$.un = function (element, type, listener) {
    return removeEvent(element, type, listener);
};
$.click = function (element, listener) {
    return addClickEvent(element, listener);
}
$.enter = function (element, listener) {
    $.enter addEnterEvent(element, listener);
};

事件代理

接下來考慮這樣一個場景,咱們須要對一個列表裏全部的<li>增長點擊事件的監聽

咱們經過本身寫的函數,取到id爲list這個ul裏面的全部li,而後經過遍歷給他們綁定事件。這樣咱們就不須要一個一個去綁定了。可是看看如下代碼:

<ul id="list">
    <li id="item1">Simon</li>
    <li id="item2">Kenner</li>
    <li id="item3">Erik</li>
</ul>
<button id="btn">Change</button>
function clickListener(event) {
    console.log(event);
}
function renderList() {
    $("#list").innerHTML = '<li>new item</li>';
}

function init() {
    each($("#list").getElementsByTagName('li'), function(item) {
        $.click(item, clickListener);
    });

    $.click($("#btn"), renderList);
}
init();

咱們增長了一個按鈕,當點擊按鈕時,改變list裏面的項目,這個時候你再點擊一下li,綁定事件再也不生效了。那是否是咱們每次改變了DOM結構或者內容後,都須要從新綁定事件呢?固然不會這麼笨,接下來學習一下事件代理,而後實現下面新的方法:

// 先簡單一些
function delegateEvent(element, tag, eventName, listener) {
    // your implement
}

$.delegate = delegateEvent;
// 使用示例
// 仍是上面那段HTML,實現對list這個ul裏面全部li的click事件進行響應
$.delegate($("#list"), "li", "click", clickHandle);

實現思路:

寫到這裏,恰好前幾天CSS魔法寫的《前端進階之路:點擊事件綁定》有提到「事件代理/委託」,不過是直接使用jQuery來實現的。因此地址有興趣的本身搜索吧-_-。

  • 「事件代理」 的本質是利用了事件冒泡的特性。當一個元素上的事件被觸發的時候,好比說鼠標點擊了一個按鈕,一樣的事件將會在那個元素的全部祖先元素中被觸發。這一過程被稱爲事件冒泡;

  • 這個事件從原始元素開始一直冒泡到DOM樹的最上層。任何一個事件的目標元素都是最開始的那個元素,在咱們的這個例子中也就是按鈕,而且它在咱們的元素對象中以屬性的形式出現。使用事件代理,咱們能夠把事件處理器添加到一個元素上,等待一個事件從它的子級元素裏冒泡上來,而且能夠得知這個事件是從哪一個元素開始的。

  • 這裏就不細說事件冒泡與事件捕獲了(阻止默認行爲也會用到,有興趣去網上找找看),可是要理解事件代理就必須先知道它們。下面這張圖能夠先看看。(圖片來自網絡,侵刪)

事件捕獲與事件冒泡原型圖

實現以下:

/**
 * 事件代理
 * @param   {HTMLElement}   element   須要進行事件代理的父元素。
 * @param   {string}   tag       須要觸發事件的標籤名
 * @param   {string}   eventName 觸發的事件類型
 * @param   {function} listener  事件執行的函數
 * @returns {[[Type]]} [[Description]]
 */
function delegateEvent(element, tag, eventName, listener) {
    // your implement
    return addEvent(element, eventName, function (ev) {
        var oEvent = ev || event; //兼容處理
        var target = oEvent.target || oEvent.srcElement; //兼容處理
        if (target.tagName.toLocaleLowerCase() === tag) {
            listener.call(target, oEvent); //使用call方法修改執行函數中的this指向,如今this指向觸發了事件的HTML節點(可直接使用this.innerHTML返回該節點內容)
        }
    })
}

$.delegate = function (element, tag, eventName, listener) {
    return delegateEvent(element, tag, eventName, listener);
};

封裝改變

估計有同窗已經開始吐槽了,函數裏面一堆$看着暈啊,那麼接下來把咱們的事件函數作以下:(這裏應該是把前面的$.on$.click$.un$.delegate都改寫一下。比較簡單,就拿一個出來做例子吧。)

//和上面的函數同樣,原來第一個參數是傳入獲取到的父HTMLElement對象,如今直接傳入選擇器名稱就行
$.delegate = function (selector, tag, event, listener) {
//這裏的`$(selector)`,是用的本身封裝的選擇器函數,願意的話能夠換成標準支持的`document.querySelector()`
    return delegateEvent($(selector), tag, event, listener);
};
// 使用示例:
$.delegate('#list', "li", "click", liClicker);
相關文章
相關標籤/搜索