DOM事件模型

328 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在学习JavaScript过程中,肯定会学到页面交互,在交互过程中我会给HTML元素节点绑定一些事件来完成交互,那么这是如何实现的呢?

事件

什么叫事件?事件如何产生?

事件就是可以背JavaScript侦听到的用户行为。比如鼠标点击、图片加载完成、鼠标进入某个元素等事件,这些事件发生后与事件处理函数相配合,来实现交互功能。DOM事件一般分为HTML事件、DOM0级事件和DOM2级事件。

事件流

事件流指从页面中接收事件的顺序,也可理解为事件在页面中传播的顺序。

在HTML页面中,DOM元素呈树级结构分布,也可以理解为同心圆,点击某个元素后,也相当于点击了他的祖先及元素,那么事件是如何先后触发传播的呢。在IE中,它认为是从下到上进行传播,从目标元素开始触发,到根结点结束,即冒泡事件流;而Netscape认为是从根结点开始触发到目标节点结束,即捕获事件流,两个公司有着完全相反的的事件流。

在DOM2级事件流中一般分为事件捕获阶段、目标阶段和事件冒泡阶段。

事件对象

Event事件对象是用来记录一些事件发生时的相关信息的对象,但事件对象只有事件发生时才会产生,并且只能是事件处理函数内部访问,在所有事件处理函数运行结束后,事件对象就被销毁!

常见属性和方法有:

  • target 事件触发的节点
  • currentTarget 事件绑定的节点
  • stopPropagation() 冒泡机制下,阻止事件的进一步往上冒泡
  • preventDefault() 取消事件的默认操作,比如链接的跳转或者表单的提交,主要是用来阻止标签的默认行为

在IE8及以前本版之中,通过设置属性注册事件处理程序时,调用的时候并未传递事件对象,需要通过全局对象window.event来获取。

function getEvent(event) {
 event = event || window.event;
}

在IE浏览器上面是event事件是没有preventDefault()这个属性的,所以在IE上,我们需要设置的属性是returnValue

window.event.returnValue=false

stopPropagation()也是,所以需要设置cancelBubble,cancelBubble是IE事件对象的一个属性,设置这个属性为true能阻止事件进一步传播。

event.cancelBubble=true

事件处理程序

事件处理程序就是响应某个事件的函数,简单地来说,就是函数。我们又把事件处理程序称为事件侦听器。事件处理程序是以"on"开头的,比如点击事件的处理程序是"onclick",事件处理程序大概有以下5种。

HTML事件处理函数

HTML事件处理函数就是直接写在HTML元素上的,例如:、下面例子,点击按钮后,事件触发,浏览器弹出提示。

<button onclick="alert('html事件处理函数')">html事件处理函数</button>

当我们要实现一个复杂功能时,我们可以定义函数,在函数里处理,就变成这样了

<script>
    function alertHtml() {
        alert('html事件处理函数')
    }
</script>
<button onclick="alertHtml()">html事件处理函数</button>

这样写的话大家有没有觉得很耦合,修改函数后可能html和js都要修改,行为和表现没有相分离,不够好,所以我们有了DOM0级事件。

DOM0级事件处理程序

先来看看DOM0级事件怎么使用

<button id="btn">DOM0级事件处理程序</button>

<script>
    var btn = document.getElementById('btn');
    btn.onclick = function () {
        alert('DOM0级事件处理程序')
    }
    btn.onclick = function () {
        alert('DOM0级事件处理程序2')
    }
</script>

可以看到,我们是先获取了 button 这个对象,然后给他的onclick属性赋值了一个方法,事件触发后直接该方法,避免与HTML耦合。但缺点也很明显,就是一个元素的一种事件,只能绑定最后一个事件处理函数,前面的会被覆盖掉。

在DOM0级事件中的事件流是冒泡事件流,在button触发后,他的腹肌div的事件也会被触发,大家可以试一试。

DOM2级事件处理程序

针对DOM0级事件存在的问题,DOM2级事件进行了改进,新增了addEventListenerremoveEventListener来监听事件和取消事件。这两个方法都接受三个参数:

  • type 事件类型的字符串
  • listener 事件处理函数
  • useCapture true/false,说明该事件是在捕获阶段还是冒泡阶段触发,默认false冒泡阶段
  • options
var btn = document.getElementById('btn');
var parent = document.getElementById('parent')

function parentFunc() {
    console.log('parent ===> DOM2级事件处理程序')
}

function btnFunc() {
    console.log('DOM2级事件处理程序')
}

parent.addEventListener('click', parentFunc)

btn.addEventListener('click', btnFunc)
// DOM2级事件处理程序
// parent ===> DOM2级事件处理程序

如果我们将addEventListener第三个参数指定为true的时候会在捕获阶段触发

<script>
    parent.addEventListener('click', parentFunc, true)
    btn.addEventListener('click', btnFunc, true)
    // parent ===> DOM2级事件处理程序
    // DOM2级事件处理程序
</script>

IE事件处理程序

IE是个大难题,在IE9以前是不支持addEventListener方法的,而是使用attachEventdetachEvent这两个函数的,他们接受两个参数

  • type 事件类型的字符串
  • listener 事件处理函数

addEventListener不同的是,这个的type需要加上on,并且只支持冒泡事件流,其他和DOM2级事件一样使用。

<button id="btn">点击</button>
 
<script>
  var btn=document.getElementById("btn");
  btn.attachEvent('onclick',hello);
  btn.detachEvent('onclick',hello);
  function hello(){
    alert("hello");
  }
</script>

这里事件触发的顺序不是添加的顺序而是添加顺序的相反顺序。
使用 attachEvent 方法有个缺点,this 的值会变成 window 对象的引用而不是触发事件的元素。

跨浏览器的事件处理程序

再看到上面几种事件触发后,是不是觉得很麻烦呢,不同的浏览器要用不同的方法,难道我们写代码的时候要写这么多冗余代码嘛,存不存在一种跨浏览器的事件处理程序呢,答案是没有,但是我们可以自己实现一个。

const EventUtil = {
    addEvent(ele, type, handler) {
        if (ele.addEventListener) {
            ele.addEventListener(type, handler)
        } else if (ele.attachEvent) {
            ele.attachEvent(`on${type}`, handler)
        } else {
            ele[`on${type}`] = handler
        }
    },
    removeEvent(ele, type, handler) {
        if (ele.removeEventListener) {
            ele.removeEventListener(type, handler)
        } else if (ele.detachEvent) {
            ele.detachEvent(`on${type}`, handler)
        } else {
            ele[`on${type}`] = null
        }
    },
    getEvent(event) {
        return event || window.event
    },
    target(event) {
        return event.target || event.srcElement
    },
    stopPropagation(event) {
        if (event.stopPropagation) {
            event.stopPropagation()
        } else {
            event.cancelBubble = true
        }
    },
    perventDefault(event) {
        if (event.perventDefault) {
            event.perventDefault()
        } else {
            event.returnValue = false
        }
    }
}

事件委托

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

有这么一个需求,点击某个li后执行一个的操作,那么我该怎么做呢,每一个li都绑定一个事件?那么我们新添加、修改、删除li标签的时候是不是也要去处理对应的事件处理函数呢?是不是很冗余很麻烦,那么我们应该怎么做呢,没错,就是我们说的事件委托来实现,只需要在他们公共的父级绑定一个事件就好了,可以根据e.target拿到触发元素,可以通过自定义属性或者nodeName等属性知道是哪个li触发的,那么我们的代码是不是就很简洁了。如下:

<ul id="btn">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

<script>
    var btn = document.getElementById('btn');

    function btnFuncBubble(e) {
       console.log('事件委托 ==> ', e.target) // IE浏览器用event.srcElement
    }

    btn.addEventListener('click', btnFuncBubble)
</script>

到这里我们关于DOM事件模型就结束了,也希望能讲清楚了一些。