JS事件模型

266 阅读15分钟

学习JS事件模型的意义

JavaScript的事件模型是Web开发中至关重要的概念之一,它的意义包括但不限于以下几个方面:

  1. 交互性: 事件模型使得网页和用户之间可以进行交互。通过捕获用户的操作,如点击、键盘输入等,JavaScript可以执行相应的操作,从而实现丰富的用户体验。
  2. 响应式: 事件模型使得网页可以对用户的行为作出实时响应。例如,当用户点击按钮时,可以触发相应的事件处理函数,执行特定的操作,如显示或隐藏元素、提交表单等。
  3. 解耦: 事件模型使得代码的结构更加清晰和模块化。通过将事件处理函数与特定的事件关联起来,可以将代码分解成多个小模块,使得代码更易于理解、维护和重用。
  4. 异步编程: JavaScript的事件模型是基于异步编程的概念构建的。通过事件循环机制,JavaScript可以处理多个事件,而不会阻塞代码的执行,从而提高了程序的性能和响应速度。
  5. 跨平台兼容性: 事件模型是Web开发的基础,几乎所有的Web浏览器都支持JavaScript事件模型,这意味着你可以使用相同的代码在不同的浏览器上实现相似的交互效果,从而提高了开发的效率和可移植性。

什么是DOM事件

DOM (Document Object Model ,⽂档对象模型)是针对HTML⽂档XML⽂档的⼀个API。DOM描绘了⼀个层次化的节点树,允许开发⼈员添加、移出和修改⻚⾯的某⼀部分,DOM 脱胎于Netscape 及微软公司创始的 DHTML(动态HTML)。但现在它已经成为表现和操作⻚⾯标记的真正跨平台语⾔中⽴的⽅式。

Netscape Navigator 4 和 IE4 分别发布于 1997 年的 6 ⽉和 10 ⽉发布的 DHTML,由于 IE 4 和 Netscape Navigator4 分别⽀持不同的DHTML,为了统⼀标准,W3C开始制定DOM。1998 年10 ⽉ W3C 总结了 IE 和 Navigator4 的规范,制定了 DOMLevel 1DOM1,之前 IE 与Netscape 的规范则被称为 DOMLevel 0DOM0

详细的dom事件参考:官网

DOM(Document Object Model)事件是指在网页中发生的各种交互行为,如鼠标点击键盘输入元素加载等。事件模型则是描述这些事件如何被捕获(capturing)和冒泡(bubbling)传播到文档中的各个节点的规范

DOM事件模型定义了事件处理的流程。它包括三个阶段:

  1. 捕获阶段(Capturing phase):事件从文档的根节点开始传播到目标节点之前的阶段。
  2. 目标阶段(Target phase):事件到达目标节点的阶段。
  3. 冒泡阶段(Bubbling phase):事件从目标节点传播回文档根节点的阶段。

DOM事件模型的工作方式影响了事件处理程序的执行顺序。具体来说,如果在同一元素上注册了多个事件处理程序,这些处理程序将按照它们所注册的顺序依次执行。在事件传播的过程中,可以使用addEventListener方法注册事件处理程序,并在参数中指定是否在捕获阶段冒泡阶段执行。

因此,DOM事件事件模型之间的关系在于,事件模型定义了事件在DOM中传播处理的方式,而DOM事件则是触发这一传播和处理的实际事件

事件模型可以分为三种:

1. DOM0级事件(原始事件模型)

DOM0级事件是指直接将事件处理函数赋值给DOM元素属性的方式,比如直接将一个函数赋值给元素的onclick属性。开发者可以通过这种方式来感知DOM0级事件,具体步骤如下:

  1. 选择DOM元素: 首先,开发者需要选择要感知事件的DOM元素。可以使用document.getElementByIddocument.querySelector等方法选择具体的元素。
  2. 为元素属性赋值: 开发者可以直接为选定的DOM元素属性(比如onclick、onmouseover等)赋值一个事件处理函数,将其与相应的事件关联起来。例如:
<button id="myButton">Click me</button>

<script>
  // 为按钮元素的点击事件(onclick)绑定一个事件处理函数
  document.getElementById('myButton').onclick = function() {
    alert('Button clicked!');
  };
</script>
特性
  • 绑定速度快

DOM0级事件具有很好的跨浏览器优势,会以最快的速度绑定,但由于绑定速度太快,可能页面还未完全加载出来,以至于事件可能无法正常运行

  • 只支持冒泡,不支持捕获
  • 同一个类型的事件只能绑定一次
<input type="button" id="btn" onclick="fun1()">

var btn = document.getElementById('.btn');
btn.onclick = fun2;

如上,当希望为同一个元素绑定多个同类型事件的时候(上面的这个btn元素绑定2个点击事件),是不被允许的,后绑定的事件会覆盖之前的事件。

删除 DOM0 级事件处理程序只要将对应事件属性置为null即可

btn.onclick = null;
click事件过程

在上述的例⼦中,click 事件并没有像其他函数⼀样,必须要调⽤才可以执⾏,click 事件并不确定什么时候发⽣,⽽当浏览器发现⽤户点击该按钮时,浏览器就检测 btn.onclick 是否有值,如果有,就会 执⾏ btn.onclick.call(btn,event) ,此时函数执⾏,call() ⽅法接收两个参数,第⼀个指向调⽤当前⽅法的对象,也就是this

需要注意的是,指定的 this 值并不⼀定是该函数执⾏时真正的this值,如果这个函数处于⾮严格模式下,则指定为 nullundefinedthis 值会⾃动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的⾃动包装对象。

另⼀个参数则是事件对象 event,该对象也可以通过 arguments[0] 来访问,它包含了事件相关的所有信息,如本例⼦中,则包含了点击事件的全部信息。可以通过给函数传参来获取事件信息。

btn.onclick = function(e){
    console.log('this is a click event');
    console.log(e); // 事件对象
}

但是在 IE 中,在使⽤ DOM0 级⽅法添加事件处理程序时,event 是作 window 对象的⼀个属性⽽存在的。此时访问事件对象需要通过 window.event

btn.onclick = function(){
    console.log(window.event); // IE中事件对象
}

DOM0级中,如果想要实现⼀个对象绑定多个函数,可以这样实现:

function fn1(){
    // do something
}

function fn2(){
    // do something
}

btn.onclick = function(e){
    fn1.call(this.xxx);
    fn2.call(this.yyy);
}

2. DOM2级事件(标准事件模型)

DOM级别1于1998年10⽉1⽇成为W3C推荐标准。1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型。在2级DOM中除了定义了⼀些DOM相关的操作之外还定义了⼀个事件模型,这个标准下的事件模型就是我们所说的2级DOM事件模型

W3C 后来将 DOM1 升级为 DOM2,DOM2级规范开始尝试以⼀种符合逻辑的⽅式来标准化 DOM事件。DOM0级 可以认为 onclick 是 btn 的⼀个属性,DOM2级 则将属性升级为队列

DOM2级 事件定义了两个⽅法,⽤于处理指定删除事件处理程序的操作, addEventListener(eventType, handler, useCapture)removeEventListener(eventType, handler, useCapture),所有的 DOM 节点中都包含这两个⽅法,它们都接收3个参数。

  1. eventType指定要处理的事件类型
  2. handler作为事件处理程序的函数
  3. useCapture是一个布尔值,true 代表在捕获阶段调⽤事件处理程序,false 表示在冒泡阶段调⽤事件处理程序,默认为 false(冒泡阶段);
btn.addEventListener('click',function(){
    // do something
})

btn.addEventListener('click',function(){
    // do something else
})

addEventListener() 将事件加⼊到监听队列中,当浏览器发现⽤户点击按钮时,click 队列中依次执⾏匿名函数1、匿名函数2。

function fn1(){
    // do something
}

function fn1(){
    // do something else
}

btn.addEventListener('click',fn1)
btn.addEventListener('click',fn2)

如果这样写,click 队列中依次 fn1.call(btn,event) , fn2.call(btn,event) 。

通过 addEventListener() 添加的事件只能由 removeEventListener() 来移除,并且 removeEventListener() 只能移除具名函数,不能移除匿名函数

3. IE 中 DOM2级事件(IE事件模型 基本不用)

IE8 及之前,实现类似 addEventListener()removeEventListener()的两个⽅法是 attachEvent()detachEvent(),这两个⽅法接受相同的两个参数。

  1. 要处理的事件名;
  2. 作为事件处理程序的函数;

IE8 之前的只⽀持事件冒泡,所以通过 attachEvent()添加的事件处理程序只能添加到冒泡阶段

btn.attachEvent('click',fn1)
btn.attachEvent('click',fn2)

当⽤户点击时,click 队列依次 fn1.call(undefined,undefined) , fn2.call(undefined,undefined) 。

类似的 detachEvent() 也只能移除具名函数,不能移除匿名函数

function eventHandler() {
    console.log('xianzao);
}

btn.attachEvent('onClick', eventHandler);
btn.detachEvent('onClick, eventHandler);

兼容处理

//判断浏览器是否支持addEventListener()方法,如果支持这个方法,说明是现代浏览器,可以使用标准的事件绑定方式。
if(typeof btn.addEventListener === 'function'){
    btn.addEventListener('click',fn);
}else if(typeof btn.attachEvent === 'function'){ //attachEvent()方法是IE浏览器早期版本支持的一种事件绑定方式。
    btn.attachEvent('onclick',fn)
}else{ //如果以上两种方法都不支持,说明是非常老旧的浏览器,可能是IE5或更早版本,这时使用DOM0级事件绑定的方式。
    btn.onclick=function(){
        // do something
    }
}

跨浏览器的事件处理程序

var EventUtil = {
    // element是当前元素,可以通过getElementById(id)获取
    // type 是事件类型,⼀般是click ,也有可能是⿏标、焦点、滚轮事件等等
    // handle 事件处理函数
    addHandler: (element, type, handler) => {
        // 先检测是否存在DOM2级⽅法,再检测IE的⽅法,最后是DOM0级⽅法(⼀般不会到这)
        if (element.addEventListener) {
            // 第三个参数false表示冒泡阶段
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent) {
            element.attachEvent(`on${type}`, handler)
        } else {
            element[`on${type}`] = handler;
        }
    },

    removeHandler: (element, type, handler) => {
        if (element.removeEventListener) {
            // 第三个参数false表示冒泡阶段
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent) {
            element.detachEvent(`on${type}`, handler)
        } else {
            element[`on${type}`] = null;
        }

    }
}

// 获取元素
var btn = document.getElementById('btn');

// 定义handler
var handler = function(e) {
    console.log('我被点击了');
}

// 监听事件
EventUtil.addHandler(btn, 'click', handler);

// 移除事件监听
// EventUtil.removeHandler(button1, 'click', clickEvent);

在2024年,一般情况下不再需要过多地考虑兼容Internet Explorer(IE)浏览器。随着时间的推移,越来越多的网站和应用程序放弃了对IE的支持,转而专注于现代浏览器的兼容性和性能优化。

然而,具体是否需要编写兼容性代码仍然取决于你的项目需求和目标用户群。如果你的项目需要支持特定的老旧浏览器,或者面向企业环境等场景,可能仍然需要考虑到对IE的兼容性。但是,这种情况已经越来越少见,大多数情况下,现代浏览器提供了足够的功能和性能,使得针对最新标准编写的代码可以在大多数浏览器中正常运行。

总结

var btn = document.getElementById('btn');

btn.onClick = () => {
    console.log('我是DOM0级事件处理程序');
}

btn.onClick = null;

btn.addEventListener('click', () => {
    console.log('我是DOM2级事件处理程序');
}, false);

btn.removeEventListener('click', handler, false)

btn.attachEvent('onclick', () => {
    console.log('我是IE事件处理程序')
})

btn.detachEvent('onclicn', handler);
  1. DOM Level 0 事件模型

    • 事件处理程序直接赋值给DOM元素的事件属性。
    • 事件只在冒泡阶段执行。
    • 不支持事件捕获。
  2. DOM Level 2 事件模型

    • 使用addEventListener()方法注册事件处理程序。
    • 支持事件捕获和事件冒泡。
    • 事件处理程序可以添加多个,并且支持在不同阶段执行。
  3. IE(Internet Explorer)事件模型

    • 使用attachEvent()方法注册事件处理程序。
    • 只支持事件冒泡,不支持事件捕获。
    • 事件处理程序只能添加一个,且在冒泡阶段执行。

综上所述,主要差异在于支持的事件传播阶段支持的事件处理程序数量以及注册事件处理程序的方式DOM Level 2 事件模型是最通用的,并且是现代网页开发中推荐使用的事件模型。

2. 事件捕获 & 事件冒泡

事件流描述的是从⻚⾯中接收事件的顺序。

IE 的事件流是事件冒泡流

⽽ Netscape Communicator的事件流是事件捕获流

DOM2级事件规定的事件流包括三个阶段:

  • 事件捕获阶段;
  • 处于⽬标阶段;
  • 事件冒泡阶段;

⾸先发⽣的是事件捕获,为截获事件提供了机会。然后是实际的⽬标接收到事件。最后⼀个阶段是冒泡阶段,可以在这个阶段对事件做出响应

截屏2024-04-02 下午9.16.41.png

1、当处于⽬标阶段,没有捕获与冒泡之分,执⾏顺序会按照 addEventListener 的添加顺序决定,先添加先执⾏;

2、使⽤ stopPropagation() 取消事件传播时,事件不会被传播给下⼀个节点,但是,同⼀节点上的其他listener还是会被执⾏;如果想要同⼀层级的listener也不执⾏,可以使⽤ stopImmediatePropagation()

// list 的捕获
$list.addEventListener('click', (e) => {
    console.log('list capturing');
    e.stopPropagation();
}, true)

// list 捕获 2
$list.addEventListener('click', (e) => {
    console.log('list capturing2');
}, true)

// list capturing
// list capturing2

3、 preventDefault() 只是阻⽌默认⾏为,跟JS的事件传播⼀点关系都没有;

4、⼀旦发起了 preventDefault() ,在之后传递下去的事件⾥⾯也会有效果;

2.1. 测试

如果有以下例⼦:

<!DOCTYPE html>
<html>
    <head>
        <title>Event Bubbling Example</title>
    </head>
    <body>
        <div id="myDiv">Click Me</div>
    </body>
</html>
  1. 事件捕获最不具体的节点最先收到事件,⽽最具体的节点最后收到事件。事件捕获实际上是为了在事件到达最终⽬标前拦截事件

如果前⾯的例⼦使⽤事件捕获,则点击<div>元素会以下列顺序触发 click 事件:

  • document;
  • <html>
  • <body>
  • <div>
  1. 事件冒泡 在点击⻚⾯中的<div>元素后,click 事件会以如下顺序发⽣:
  • <div>
  • <body>
  • <html>
  • document;

<div>元素,即被点击的元素,最先触发 click 事件。然后click 事件沿 DOM 树⼀路向上,在经过的每个节点上依次触发,直⾄到达 document 对象。

3. 事件对象

DOM0DOM2的事件处理程序都会⾃动传⼊event对象

IE中的event对象取决于指定的事件处理程序的⽅法。

IE的handler会在全局作⽤域运⾏, this === window,所以在IE中会有 window.eventevent 两种情况,只有在事件处理程序期间,event对象才会存在,⼀旦事件处理程序执⾏完成,event对象就会被销毁

event对象⾥需要关⼼的两个属性:

  1. target:target永远是被添加了事件的那个元素;
  2. eventPhase:调⽤事件处理程序的阶段,有三个值:
    • 1:捕获阶段;
    • 2:处于⽬标;
    • 3:冒泡阶段;
3.1. preventDefault与stopPropagation

preventDefault:⽐如链接被点击会导航到其href指定的URL,这个就是默认⾏为;

stopPropagation:⽴即停⽌事件在DOM层次中的传播,包括捕获和冒泡事件;

IE中对应的属性:

  • srcElement => target
  • returnValue => preventDefaukt()
  • cancelBubble => stopPropagation()

IE 不⽀持事件捕获,因⽽只能取消事件冒泡,但 stopPropagation可以同时取消事件捕获和冒泡。 再针对上⾯不同类型的事件及属性进⾏区分:

var EventUtil = {
    // element是当前元素,可以通过getElementById(id)获取
    // type 是事件类型,⼀般是click ,也有可能是⿏标、焦点、滚轮事件等等
    // handle 事件处理函数
    addHandler: (element, type, handler) => {
        // 先检测是否存在DOM2级⽅法,再检测IE的⽅法,最后是DOM0级⽅法(⼀般不会到这)
        if (element.addEventListener) {
            // 第三个参数false表示冒泡阶段
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent) {
            element.attachEvent(`on${type}`, handler)
        } else {
            element[`on${type}`] = handler;
        }
    },
    removeHandler: (element, type, handler) => {
        if (element.removeEventListener) {
            // 第三个参数false表示冒泡阶段
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent) {
            element.detachEvent(`on${type}`, handler)
        } else {
            element[`on${type}`] = null;
        }
    },
    // 获取event对象
    getEvent: (event) => {
        return event ? event : window.event
    },
    // 获取当前⽬标
    getTarget: (event) => {
        return event.target ? event.target : event.srcElement
    },
    // 阻⽌默认⾏为
    preventDefault: (event) => {
        if (event.preventDefault) {
            event.preventDefault()
        } else {
            event.returnValue = false
        }
    },
    // 停⽌传播事件
    stopPropagation: (event) => {
        if (event,stopPropagation) {
            event.stopPropagation()
        } else {
            event.cancelBubble = true
        }
    }
}

4. 事件委托

事件委托:⽤来解决事件处理程序过多的问题

⻚⾯结构如下

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>

按照传统的做法,需要像下⾯这样为它们添加 3 个事 件处理程序。

var item1 = document.getElementById("goSomewhere");
var item2 = document.getElementById("doSomething");
var item3 = document.getElementById("sayHi");

EventUtil.addHandler(item1, "click", function(event){
    location.href = "http://www.xiaoman.com";
});

EventUtil.addHandler(item2, "click", function(event){
    document.title = "I changed the document's title";
});

EventUtil.addHandler(item3, "click", function(event){
    alert("hi");
});

如果在⼀个复杂的 Web 应⽤程序中,对所有可单击的元素都采⽤这种⽅式,那么结果就会有数不清的代码⽤于添加事件处理程序。此时,可以利⽤事件委托技术解决这个问题。使⽤事件委托,只需在DOM树中尽量最⾼的层次上添加⼀个事件处理程序,如下⾯的例⼦所示

var list = document.getElementById("myLinks");

EventUtil.addHandler(list, "click", function(event) {
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    switch(target.id) {
        case "doSomething":
            document.title = "I changed the document's title";
            break;
        case "goSomewhere":
            location.href = "http://www.wrox.com";
            break;
        case "sayHi": 9 alert("hi");
        break;
    }
}

⼦节点的点击事件会冒泡到⽗节点,并被这个注册事件处理

最适合采⽤事件委托技术的事件包括 click 、 mousedown 、 mouseup、 keydown、 keyup和 keypress 。 虽然 mouseover 和 mouseout 事件也冒泡,但要适当处理它们并不容易,⽽且经常需要计算元素的位置。

可以考虑为 document 对象添加⼀个事件处理程序,⽤以处理⻚⾯上发⽣的某种特定类型的事件,需要跟踪的事件处理程序越少,移除它们就越容易(移除事件处理程序关乎内存和性能)。只要是通过 onload 事件处理程序添加的东⻄,最后都要通过 onunload 事件处理程序将它们移除。