javascript-DOM事件机制

loading 2022年11月28日 105次浏览

1.DOM事件级别

DOM级别可以分成0、1、2、3四个级别,但是只有0、2、3级别中有事件相关的内容,没有DOM 1级事件

1.1 DOM 0级事件

1.1 行内元素

        <input type="button" id="btn" value="按钮" onclick="alert('行内事件')"></input>

1.2 element.on事件名 = function(){}

例如element.onclick = function(){}

        var btn = document.getElementById('btn');
        btn.onclick = function () {
            alert(this.innerHTML);
        }

缺点是一个处理事件只能绑定一个函数,例如想为onclick绑定几个函数是不被允许的。

元素事件行为绑定的方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的

1.2 DOM 2级事件

DOM 2级事件就是监听方法:addEventListener()和removeEventListener()

监听方法具有3个参数:
el.addEventListener(event-name, callback, useCapture)

  • event-name:事件名称
  • callback:回调函数,当事件触发时,函数会被传入一个参数为当前的事件对象event
  • useCapture:默认是false,表示元素在事件的冒泡阶段响应事件;设为true则表示元素在事件的捕获阶段响应事件。

IE9以下的IE浏览器不支持 addEventListener()和removeEventListener(),使用 attachEvent()与detachEvent() 代替,因为IE9以下是不支持事件捕获的,所以也没有第三个参数,第一个事件名称前要加on。

1.3 DOM 3级事件

在DOM 2级事件基础上添加了更多事件类型

  • UI事件,当用户与页面上的元素交互时触发,如:load、scroll
  • 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
  • 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
  • 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  • 文本事件,当在文档中输入文本时触发,如:textInput
  • 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
  • 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
  • 一些使用者自定义的事件

2. DOM事件模型和事件流

2.1 概述

DOM事件模型分为捕获和冒泡,一个事件发生后,会在子元素和父元素之间传播,这种传播分成3个阶段:

(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;

window -> document -> html -> body -> ...

(2)目标阶段:真正的目标节点正在处理事件的阶段;

(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。

2.2 具体流程

事件冒泡

    <div id="parent">
        fatherElement
        <div id="child">
            childElement
        </div>
    </div>
    <script type="text/javascript">
        var parent = document.getElementById("parent");
        var child = document.getElementById("child");

        document.body.addEventListener("click", function (e) {
            console.log("click-body");
        }, false);

        parent.addEventListener("click", function (e) {
            console.log("click-parent");
        }, false);

        child.addEventListener("click", function (e) {
            console.log("click-child");
        }, false);

        //点击child的div输出:
        //click-child
        //click-parent
        //click-body
    </script>

事件捕获,只要将上面addEventListener的第三个参数改为true,再次点击child的div输出会变成:

        //click-body
        //click-parent
        //click-child	

正如上面提到的,onclick给元素绑定方法都是在冒泡阶段(或者目标阶段)执行的。

    <div id="outer">
        <div id="inner"></div>
    </div>
    <script type="text/javascript">
        window.onclick = function () {
            console.log('window');
        };
        document.onclick = function () {
            console.log('document');
        };
        document.documentElement.onclick = function () {
            console.log('html');
        };
        document.body.onclick = function () {
            console.log('body');
        }
        outer.onclick = function (ev) {
            console.log('outer');
        };
        inner.onclick = function (ev) {
            console.log('inner');
        };
        //inner -> outer -> body -> html -> document -> window
    </script>

2.3 阻止冒泡

2.3.1 event.stopPropagation()

该方法可以阻止事件冒泡到父元素。
还是2.2中的例子,在child的监听函数里阻止冒泡:

        var parent = document.getElementById("parent");
        var child = document.getElementById("child");

        document.body.addEventListener("click", function (e) {
            console.log("click-body");
        }, false);

        parent.addEventListener("click", function (e) {
            console.log("click-parent");
        }, false);

        child.addEventListener("click", function (e) {
            event.stopPropagation();    
            console.log("click-child");
        }, false);
	
        //点击child的div输出:
        //click-child

2.3.2 event.stopImmediateProgation()

和stopPropagation()作用相同点在于:用于阻止事件冒泡到父元素。

不同点在于,该方法阻止监听同一事件的其它事件监听器被调用

如果多个事件监听器被附加到相同元素的相同事件类型上,如果在其中一个监听器内执行该方法,剩下的事件监听器都不会被调用。

比如这个例子:

    <div>
        <p>paragraph</p>
    </div>
    <script>
        const p = document.querySelector('p')
        p.addEventListener("click", (event) => {
            alert("我是 p 元素上被绑定的第一个监听函数");
        }, false);

        p.addEventListener("click", (event) => {
            alert("我是 p 元素上被绑定的第二个监听函数");
            event.stopImmediatePropagation();
            // 执行 stopImmediatePropagation 方法,阻止 click 事件冒泡,并且阻止 p 元素上绑定的其他 click 事件的事件监听函数的执行。
        }, false);

        p.addEventListener("click", (event) => {
            alert("我是 p 元素上被绑定的第三个监听函数");
            // 该监听函数排在上个函数后面,该函数不会被执行
        }, false);

        document.querySelector("div").addEventListener("click", (event) => {
            alert("我是 div 元素,我是 p 元素的上层元素");
            // p 元素的 click 事件没有向上冒泡,该函数不会被执行
        }, false);
    </script>

如果点击p元素,则只会打印前两个监听函数。不打印第三个监听函数是因为被stopImmediatePropagation()拦截了,没有向上冒泡到div是因为阻止冒泡了。

如果把上面的方法换成stopPropagation()

        p.addEventListener("click", (event) => {
            console.log("我是 p 元素上被绑定的第二个监听函数");
            event.stopPropagation();
            // event.stopImmediatePropagation();
        }, false);

那么点击p元素则会打印3个监听函数,但是因为阻止了冒泡的缘故不会打印div。

2.3.3 event.preventDefault()

该方法和阻止冒泡没什么关系,不过还是放到一起写了。

该方法的主要作用是阻止某个事件的默认动作被正常执行。比如阻止点击链接跳转,阻止复选框被点击后变成checked状态等等。

经过处理后的事件还是正常传播的,除非碰到阻止冒泡的那两个方法。

    <p>Please click on the checkbox control.</p>

    <form>
        <label for="id-checkbox">Checkbox:</label>
        <input type="checkbox" id="id-checkbox" />
    </form>

    <div id="output-box"></div>

    <script>
        let box = document.querySelector('#id-checkbox');
        box.addEventListener('click' , function(event){
            let output = document.querySelector('#output-box');
            event.preventDefault();
            output.innerHTML = "<code>preventDefault()</code> won't let you check this<br>"            
        } , false);
    </script>

比如只允许输入框输入6个字符

    <input type="text" id='tempInp'>
    <script>
        tempInp.onkeydown = function (ev) {
            ev = ev || window.event;
            let val = this.value.trim() //trim去除字符串首位空格(不兼容)
            let len = val.length
            if (len >= 6) {
                this.value = val.substr(0, 6);
                //阻止默认行为去除特殊按键(DELETE\BACK-SPACE\方向键...)
                let code = ev.which || ev.keyCode;
                if (!/^(46|8|37|38|39|40)$/.test(code)) {
                    ev.preventDefault()
                }
            }
        }
    </script>

3. 事件委托

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理子节点的事件。 这种处理方式称为事件委托。

事件委托主要有两个好处。

3.1 减少内存消耗,提高性能

比如实现当鼠标移到li上就能改变背景颜色

首先可以通过for循环遍历每个li实现

<ul id='ul'>
    <li>item1</li>
    <li>item2</li>
    <li>item3</li>
</ul>

<script>
    window.onload = function () {
        let li = document.querySelectorAll("li");
        for (var i = 0; i < li.length; i++) {
            li[i].onmouseover = function () {
                this.style.background = "red";
            }
            li[i].onmouseout = function () {
                this.style.background = "";
            }
        }
    }
</script>

通过事件委托方式实现

    <script>
        window.onload = function () {
            let ul = document.querySelector('#ul');
            ul.onmouseover = function(e){
                let target = e.target;
                if(target.nodeName.toLowerCase() === 'li'){
                    target.style.background = 'red'
                }
            }
            ul.onmouseout = function(e){
                let target = e.target;
                if(target.nodeName.toLowerCase() === 'li'){
                    target.style.background = ''
                }
            }
        }
    </script>

3.2 动态绑定事件

在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件。

使用事件代理来解决:比如要动态给ul添加li,不使用事件委托,会发现新增的li背景颜色不会被改变,因为点击添加时for循环已经执行完毕。

    <input type="button" id="btn" value="add item">
    <ul id='ul'>
        <li>item1</li>
        <li>item2</li>
        <li>item3</li>
    </ul>

    <script>
        window.onload = function () {
            var ul = document.getElementById("ul");
            var li = ul.getElementsByTagName("li");
            var btn = document.getElementById("btn");
            var iNow = 3;
            for (var i = 0; i < li.length; i++) {
                li[i].onmouseover = function () {
                    this.style.background = "red";
                }
                li[i].onmouseout = function () {
                    this.style.background = "";
                }
            }

            btn.onclick = function () {
                iNow++;
                var li = document.createElement("li");
                li.innerHTML = `item${iNow}`;
                ul.appendChild(li);
            }
        }
    </script>

使用事件委托来做,就可以实现新增子对象时,无需再对其进行事件绑定。(因为事件绑在父对象上)

    <script>
        window.onload = function () {
            let ul = document.querySelector('#ul');
            let btn = document.querySelector('#btn');
            let cnt = 3;
            ul.onmouseover = (e) =>{
                let target = e.target;
                if(target.nodeName.toLowerCase() === 'li'){
                    target.style.background = 'red';
                }
            }
            ul.onmouseout = (e) =>{
                let target = e.target;
                if(target.nodeName.toLowerCase() === 'li'){
                    target.style.background = '';
                }
            }
            btn.onclick = () =>{
                cnt++;
                let li = document.createElement('li');
                li.innerHTML = `item${cnt}`;
                ul.appendChild(li)
            }
        }
    </script>

3.3 点击ul中的li

<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
    </ul>

    <script>
        const ul = document.querySelector('ul');
        ul.addEventListener('click' , function(e) {
            console.log(e.target);
        })
    </script>
</body>

4. event.target & event.currentTarget

常见的两个阻止冒泡的方法和阻止默认事件的方法上面已经写了,这里写上剩下的两个常用方法的区别
以这个作为例子

    <div id="a">
        <div id="b">
            <div id="c"></div>
        </div>
    </div>
    <script>
        document.getElementById('a').addEventListener('click', function (e) {
            console.log(
                'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
            )
        })
        document.getElementById('b').addEventListener('click', function (e) {
            console.log(
                'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
            )
        })
        document.getElementById('c').addEventListener('click', function (e) {
            console.log(
                'target:' + e.target.id + '&currentTarget:' + e.currentTarget.id
            )
        })
    </script>

点击最内层c元素时,输出如下:

target:c & currentTarget:c
target:c & currentTarget:b
target:c & currentTarget:a

从输出中我们可以知道:

  • event.target指向引起触发事件的元素
  • event.currentTarget指向事件绑定的元素,只有在被点击的那个元素两者才会相等(this和该属性一样,都是指向被绑定的元素)
  • 也就是说前者是事件的真正发出者;后者是事件的监听者

如果把几个监听器的第三个属性改为true,则事件变成捕获机制执行,同样点击c,输出会变成:
target:c & currentTarget:a
target:c & currentTarget:b
target:c & currentTarget:c

5.addEventListener & onclick

两者主要有以下不同点:
(1) 前者可以绑定并按顺序执行多个事件;后者只能绑定一个事件,若绑定了多个,只执行最后一个。

(2) 前者可以指定是执行捕获机制还是冒泡机制;后者只能执行冒泡机制

(3) 前者对于任何DOM元素都有效;而后者仅对HTML DOM元素有效

DOM包括核心DOM,XML DOM和HTML DOM:

核心DOM提供了操作文档的公有属性和方法,就相当于鼻祖。它可以可操作一切结构化文档的API,包括HTML和XML。全面但是繁琐。

HTML DOM专门操作HTML文档的简化版DOMAPI仅对常用的复杂的API进行了简化,对核心DOM进行了在HTML方面的拓展。不是万能的,但是简单

XML DOM提供了所有XML元素的对象和属性,以及访问方法,与HTML DOM类似。

(4) 前者注册事件时不需要加on,写'click'即可

(5) 移除事件时,前者通过removeListener;后者通过document.onclick = null