1. 程式人生 > >JavaScript:事件對象Event和冒泡

JavaScript:事件對象Event和冒泡

add 方式 p值 器) 機制 效率 學習 roi androi

本文最初發表於博客園,並在GitHub上持續更新前端的系列文章。歡迎在GitHub上關註我,一起入門和進階前端。

以下是正文。

綁定事件的兩種方式

我們在上一篇文章 DOM操作詳解 中已經講過事件的概念。這裏講一下註冊事件的兩種方式,我們以onclick事件為例。

方式一:onclick

舉例:

<body>
<button>點我</button>
<script>
    var btn = document.getElementsByTagName("button")[0];

    //這種事件綁定的方法容易被層疊。
    btn.onclick
= function () { console.log("事件1"); } btn.onclick = function () { console.log("事件2"); } </script> </body>

點擊按鈕後,上方代碼的打印結果:

事件2

我們可以看到,這種綁定事件的方式,會層疊掉之前的事件。

方式二:addEventListener

addEventListener()裏的參數:

  • 參數1:事件名(註意,沒有on)

  • 參數2:事件名(執行函數)

  • 參數3:事件名(捕獲或者冒泡)

舉例:

<body>
<button>按鈕</button> <script> var btn = document.getElementsByTagName("button")[0]; //addEventListener: 事件監聽器。 原事件被執行的時候,後面綁定的事件照樣被執行 //第二種事件綁定的方法不會出現層疊。(更適合團隊開發) btn.addEventListener("click", fn1); btn.addEventListener("click", fn2); function fn1() { console
.log("事件1"); } function fn2() { console.log("事件2"); } </script> </body>

點擊按鈕後,上方代碼的打印結果:

    事件1
    事件2

我們可以看到,這種綁定事件的方式,不會層疊掉之前的事件。

事件對象

在觸發DOM上的某個事件時,會產生一個事件對象event,這個對象中包含著所有與事件有關的信息。比如鼠標操作時候,會添加鼠標位置的相關信息到事件對象中。

所有瀏覽器都支持event對象,但支持的方式不同。如下。

(1)普通瀏覽器支持 event。比如:

技術分享圖片

(2)ie 678 支持 window.event。

於是,我們可以采取一種兼容性的寫法。如下:

    event = event || window.event; ////兼容性寫法

代碼舉例:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<script>
    //點擊頁面的任何部分
    document.onclick = function (event) {
        event = event || window.event; ////兼容性寫法

        console.log(event);
        console.log(event.timeStamp);
        console.log(event.bubbles);
        console.log(event.button);
        console.log(event.pageX);
        console.log(event.pageY);
        console.log(event.screenX);
        console.log(event.screenY);
        console.log(event.target);
        console.log(event.type);
        console.log(event.clientX);
        console.log(event.clientY);
    }
</script>
</body>
</html>

event 屬性

event 有很多屬性,比如:

技術分享圖片

由於pageX 和 pageY的兼容性不好,我們可以這樣做:

  • 鼠標在頁面的位置 = 被卷去的部分+可視區域部分。

Event舉例

舉例1:鼠標跟隨

代碼實現:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        body {
            height: 5000px;
        }

        img {
            position: absolute;
            padding: 10px 0;
            border: 1px solid #ccc;
            cursor: pointer;
            background-color: yellowgreen;
        }
    </style>
</head>
<body>
<img src="" width="100" height="100"/>

<script>
    //需求:點擊頁面的任何地方,圖片跟隨鼠標移動到點擊位置。
    //思路:獲取鼠標在頁面中的位置,然圖片緩慢運動到鼠標點擊的位置。
    //  兼容ie67做pageY和pageX;
    //  原理:     鼠標在頁面的位置 = 被卷去的部分+可視區域部分。
    //步驟:
    //1.老三步。
    //2.獲取鼠標在頁面中的位置。
    //3.利用緩動原理,慢慢的運動到指定位置。(包括左右和上下)

    //1.老三步。
    var img = document.getElementsByTagName("img")[0];
    var timer = null;
    var targetx = 0;
    var targety = 0;
    var leaderx = 0;
    var leadery = 0;
    //給整個文檔綁定點擊事件獲取鼠標的位置。
    document.onclick = function (event) {
        //新五步
        //兼容獲取事件對象
        event = event || window.event;
        //鼠標在頁面的位置 = 被卷去的部分+可視區域部分。
        var pagey = event.pageY || scroll().top + event.clientY;
        var pagex = event.pageX || scroll().left + event.clientX;

        targety = pagey - 30;
        targetx = pagex - 50;

        //要用定時器,先清定時器
        clearInterval(timer);
        timer = setInterval(function () {
            //為盒子的位置獲取值
            leaderx = img.offsetLeft;
            //獲取步長
            var stepx = (targetx - leaderx) / 10;
            //二次處理步長
            stepx = stepx > 0 ? Math.ceil(stepx) : Math.floor(stepx);
            leaderx = leaderx + stepx;
            //賦值
            img.style.left = leaderx + "px";


            //為盒子的位置獲取值
            leadery = img.offsetTop;
            //獲取步長
            var stepy = (targety - leadery) / 10;
            //二次處理步長
            stepy = stepy > 0 ? Math.ceil(stepy) : Math.floor(stepy);
            leadery = leadery + stepy;
            //賦值
            img.style.top = leadery + "px";

            //清定時器
            if (Math.abs(targety - img.offsetTop) <= Math.abs(stepy) && Math.abs(targetx - img.offsetLeft) <= Math.abs(stepx)) {
                img.style.top = targety + "px";
                img.style.left = targetx + "px";
                clearInterval(timer);
            }
        }, 30);
    }

</script>

</body>
</html>

實現效果:

技術分享圖片

event應用舉例:獲取鼠標距離所在盒子的距離

關鍵點:

    鼠標距離所在盒子的距離 = 鼠標在整個頁面的位置 - 所在盒子在整個頁面的位置

代碼演示:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        .box {
            width: 300px;
            height: 200px;
            padding-top: 100px;
            background-color: pink;
            margin: 100px;
            text-align: center;
            font: 18px/30px "simsun";
            cursor: pointer;
        }
    </style>
</head>
<body>
<div class="box">

</div>

<script src="animate.js"></script>
<script>
    //需求:鼠標進入盒子之後只要移動,哪怕1像素,隨時顯示鼠標在盒子中的坐標。
    //技術點:新事件,onmousemove:在事件源上,哪怕鼠標移動1像素也會觸動這個事件。
    //一定程度上,模擬了定時器
    //步驟:
    //1.老三步和新五步
    //2.獲取鼠標在整個頁面的位置
    //3.獲取盒子在整個頁面的位置
    //4.用鼠標的位置減去盒子的位置賦值給盒子的內容。

    //1.老三步和新五步
    var div = document.getElementsByTagName("div")[0];

    div.onmousemove = function (event) {

        event = event || window.event;
        //2.獲取鼠標在整個頁面的位置
        var pagex = event.pageX || scroll().left + event.clientX;
        var pagey = event.pageY || scroll().top + event.clientY;
        //3.獲取盒子在整個頁面的位置
        // var xx =
        // var yy =
        //4.用鼠標的位置減去盒子的位置賦值給盒子的內容。
        var targetx = pagex - div.offsetLeft;
        var targety = pagey - div.offsetTop;
        this.innerHTML = "鼠標在盒子中的X坐標為:" + targetx + "px;<br>鼠標在盒子中的Y坐標為:" + targety + "px;"
    }

</script>
</body>
</html>

實現效果:

技術分享圖片

舉例:商品放大鏡

代碼實現:

(1)index.html:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .box {
            width: 350px;
            height: 350px;
            margin: 100px;
            border: 1px solid #ccc;
            position: relative;
        }

        .big {
            width: 400px;
            height: 400px;
            position: absolute;
            top: 0;
            left: 360px;
            border: 1px solid #ccc;
            overflow: hidden;
            display: none;
        }

        /*mask的中文是:遮罩*/
        .mask {
            width: 175px;
            height: 175px;
            background: rgba(255, 255, 0, 0.4);
            position: absolute;
            top: 0;
            left: 0;
            cursor: move;
            display: none;
        }

        .small {
            position: relative;
        }

        img {
            vertical-align: top;
        }
    </style>

    <script src="tools.js"></script>
    <script>
        window.onload = function () {
            //需求:鼠標放到小盒子上,讓大盒子裏面的圖片和我們同步等比例移動。
            //技術點:onmouseenter==onmouseover 第一個不冒泡
            //技術點:onmouseleave==onmouseout  第一個不冒泡
            //步驟:
            //1.鼠標放上去顯示盒子,移開隱藏盒子。
            //2.老三步和新五步(黃盒子跟隨移動)
            //3.右側的大圖片,等比例移動。

            //0.獲取相關元素
            var box = document.getElementsByClassName("box")[0];
            var small = box.firstElementChild || box.firstChild;
            var big = box.children[1];
            var mask = small.children[1];
            var bigImg = big.children[0];

            //1.鼠標放上去顯示盒子,移開隱藏盒子。(為小盒子綁定事件)
            small.onmouseenter = function () {
                //封裝好方法調用:顯示元素
                show(mask);
                show(big);
            }
            small.onmouseleave = function () {
                //封裝好方法調用:隱藏元素
                hide(mask);
                hide(big);
            }

            //2.老三步和新五步(黃盒子跟隨移動)
            //綁定的事件是onmousemove,而事件源是small(只要在小盒子上移動1像素,黃盒子也要跟隨)
            small.onmousemove = function (event) {
                //新五步
                event = event || window.event;

                //想要移動黃盒子,必須要知道鼠標在small小圖中的位置。
                var pagex = event.pageX || scroll().left + event.clientX;
                var pagey = event.pageY || scroll().top + event.clientY;

                //x:mask的left值,y:mask的top值。
                var x = pagex - box.offsetLeft - mask.offsetWidth / 2; //除以2,可以保證鼠標mask的中間
                var y = pagey - box.offsetTop - mask.offsetHeight / 2;

                //限制換盒子的範圍
                //left取值為大於0,小盒子的寬-mask的寬。
                if (x < 0) {
                    x = 0;
                }
                if (x > small.offsetWidth - mask.offsetWidth) {
                    x = small.offsetWidth - mask.offsetWidth;
                }
                //top同理。
                if (y < 0) {
                    y = 0;
                }
                if (y > small.offsetHeight - mask.offsetHeight) {
                    y = small.offsetHeight - mask.offsetHeight;
                }

                //移動黃盒子
                console.log(small.offsetHeight);
                mask.style.left = x + "px";
                mask.style.top = y + "px";

                //3.右側的大圖片,等比例移動。
                //如何移動大圖片?等比例移動。
                //    大圖片/大盒子 = 小圖片/mask盒子
                //    大圖片走的距離/mask走的距離 = (大圖片-大盒子)/(小圖片-黃盒子)
//                var bili = (bigImg.offsetWidth-big.offsetWidth)/(small.offsetWidth-mask.offsetWidth);

                //大圖片走的距離/mask盒子都的距離 = 大圖片/小圖片
                var bili = bigImg.offsetWidth / small.offsetWidth;

                var xx = bili * x;  //知道比例,就可以移動大圖片了
                var yy = bili * y;

                bigImg.style.marginTop = -yy + "px";
                bigImg.style.marginLeft = -xx + "px";
            }
        }
    </script>
</head>
<body>
<div class="box">
    <div class="small">
        <img src="images/001.jpg" alt=""/>
        <div class="mask"></div>
    </div>
    <div class="big">
        <img src="images/0001.jpg" />
    </div>
</div>
</body>
</html>

(2)tools.js:

/**
 * Created by smyhvae on 2018/02/03.
 */

//顯示和隱藏
function show(ele) {
    ele.style.display = "block";
}

function hide(ele) {
    ele.style.display = "none";
}

function scroll() {  // 開始封裝自己的scrollTop
    if (window.pageYOffset != null) {  // ie9+ 高版本瀏覽器
        // 因為 window.pageYOffset 默認的是  0  所以這裏需要判斷
        return {
            left: window.pageXOffset,
            top: window.pageYOffset
        }
    }
    else if (document.compatMode === "CSS1Compat") {    // 標準瀏覽器   來判斷有沒有聲明DTD
        return {
            left: document.documentElement.scrollLeft,
            top: document.documentElement.scrollTop
        }
    }
    return {   // 未聲明 DTD
        left: document.body.scrollLeft,
        top: document.body.scrollTop
    }
}

效果演示:

技術分享圖片

事件冒泡

事件傳播的三個階段是:事件捕獲、事件冒泡和目標。

  • 事件捕獲階段:事件從最上一級標簽開始往下查找,直到捕獲到事件目標 target。(從祖先元素往子元素查找,DOM樹結構)。在這個過程中,事件相應的監聽函數是不會被觸發的。

  • 事件目標:當到達目標元素之後,執行目標元素該事件相應的處理函數。如果沒有綁定監聽函數,那就不執行。

  • 事件冒泡階段:事件從事件目標 target 開始,往上冒泡直到頁面的最上一級標簽。(從子元素到祖先元素冒泡)

如下圖所示:

技術分享圖片

PS:這個概念類似於 Android 裏的 touch 事件傳遞

事件冒泡的概念

事件冒泡: 當一個元素上的事件被觸發的時候(比如說鼠標點擊了一個按鈕),同樣的事件將會在那個元素的所有祖先元素中被觸發。這一過程被稱為事件冒泡;這個事件從原始元素開始一直冒泡到DOM樹的最上層。

通俗來講,冒泡指的是:子元素的事件被觸發時,父盒子的同樣的事件也會被觸發。取消冒泡就是取消這種機制。

代碼演示:

    //事件冒泡
    box3.onclick = function () {
        alert("child");
    }

    box2.onclick = function () {
        alert("father");
    }

    box1.onclick = function () {
        alert("grandfather");
    }

    document.onclick = function () {
        alert("body");
    }

技術分享圖片

上圖顯示,當我點擊兒子 box3的時候,它的父親box2、box1、body都依次被觸發了。即使我改變代碼的順序,也不會影響效果的順序。

冒泡順序

一般的瀏覽器: (除IE6.0之外的瀏覽器)

  • div -> body -> html -> document -> window

IE6.0:

  • div -> body -> html -> document

事件捕獲

addEventListener可以捕獲事件:

    box1.addEventListener("click", function () {
        alert("捕獲 box3");
    }, true);

上面的方法中,參數為true,代表捕獲;參數為false或者不寫參數,代表冒泡。

代碼演示:

    //參數為true,代表捕獲;參數為false或者不寫參數,代表冒泡
    box3.addEventListener("click", function () {
        alert("捕獲 child");
    }, true);

    box2.addEventListener("click", function () {
        alert("捕獲 father");
    }, true);

    box1.addEventListener("click", function () {
        alert("捕獲 grandfather");
    }, true);

    document.addEventListener("click", function () {
        alert("捕獲 body");
    }, true);

效果演示:

技術分享圖片

不是所有的事件都能冒泡

以下事件不冒泡:blur、focus、load、unload、onmouseenter、onmouseleave。意思是,事件不會往父元素那裏傳遞。

我們檢查一個元素是否會冒泡,可以通過事件的以下參數:

    event.bubbles

如果返回值為true,說明該事件會冒泡;反之則相反。

舉例:

    box1.onclick = function (event) {
        alert("冒泡 child");

        event = event || window.event;
        console.log(event.bubbles); //打印結果:true
    }

阻止冒泡的方法

w3c的方法:(火狐、谷歌、IE11)

    event.stopPropagation();

IE10以下則是:

event.cancelBubble = true

兼容代碼如下:

   box3.onclick = function (event) {

        alert("child");

        //阻止冒泡
        event = event || window.event;

        if (event && event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
    }

上方代碼中,我們對box3進行了阻止冒泡,產生的效果是:事件不會繼續傳遞到 father、grandfather、body了。

事件委托

事件委托,通俗地來講,就是把一個元素響應事件(click、keydown......)的函數委托到另一個元素。

比如說有一個列表 ul,列表之中有大量的列表項 li,我們需要在點擊列表項li的時候響應一個事件。如果給每個列表項一一都綁定一個函數,那對於內存消耗是非常大的,效率上需要消耗很多性能。

因此,比較好的方法就是把這個點擊事件綁定到他的父層,也就是 ul 上,然後在執行事件的時候再去匹配判斷目標元素。

所以事件委托可以減少大量的內存消耗,節約效率。

事件委托的參考鏈接:JavaScript 事件委托詳解

我的公眾號

想學習代碼之外的軟技能?不妨關註我的微信公眾號:生命團隊(id:vitateam)。

掃一掃,你將發現另一個全新的世界,而這將是一場美麗的意外:

技術分享圖片

JavaScript:事件對象Event和冒泡