JS中的事件流

1.事件流

在浏览器中,JavaScript和HTML之间的交互是通过事件去实现的,常用的事件有代表鼠标单击的click事件、代表加载的load事件、代表鼠标指针悬浮的mouseover事件。在事件发生时,会相对应地触发绑定在元素上的事件处理程序,以处理对应的操作。
通常一个页面会绑定很多的事件,那么具体的事件触发顺序是什么样的呢?
这就会涉及事件流的概念,事件流描述的是从页面中接收事件的顺序。事件发生后会在目标节点和根节点之间按照特定的顺序传播,路径经过的节点都会接收到事件。我们通过下面的场景来直观地想象一下事件的流转顺序。
页面上有一个div,分别在body、div、p、span上绑定了click事件。假如我在span上执行了单击的操作,那么将会产生什么样的事件流呢?

<body>
    <div>
        <p>
            <span>
                文本一
            </span>
        </p>
    </div>
</body>

第一种事件传递顺序是先触发最外层的body元素,然后向内传播,依次触发div、p与span元素。
第二种事件传递顺序先触发由最内层的span元素,然后向外传播,依次触发p、div与body元素。
第一种事件传递顺序对应的是捕获型事件流,第二种事件传递顺序对应的是冒泡型事件流。
一个完整的事件流实际包含了3个阶段:事件捕获阶段>事件目标阶段>事件冒泡阶段。上述两种类型的事件流实际对应其中的事件捕获阶段与事件冒泡阶段。


事件流.png
  • 事件捕获阶段
    事件捕获阶段的主要表现是不具体的节点先接收事件,然后逐级向下传播,最具体的节点最后接收到事件。根据图中的指示就是Window > Document > html > body > div > p > span。
  • 事件目标阶段
    事件目标阶段表示事件刚好传播到用户产生行为的元素上,可能是事件捕获的最后一个阶段,也可能是事件冒泡的第一个阶段。
  • 事件冒泡阶段
    事件冒泡阶段的主要表现是最具体的元素先接收事件,然后逐级向上传播,不具体的节点最后接收事件,根据图中的指示就是span > p > div > body > html > Document >Window。
let spanDom = document.querySelector('span');
spanDom .addEventListener("click", function (e) {
    console.info(`span trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
}, false)

let p1Dom = document.querySelector('p');
p1Dom.addEventListener('click', function (e) {
    console.info(`p trigger  target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})

let divDom = document.querySelector('div');
divDom.addEventListener('click', function (e) {
    console.info(`div trigger  target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})

let bodyDom = document.querySelector('body');
bodyDom.addEventListener('click', function (e) {
    console.info(`body trigger  target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})

当点击span标签时候,将输出


span click

使用addEventListener()函数绑定的事件在默认情况下,即第三个参数默认为false时,按照冒泡型事件流处理。第三个参数为true时,按捕获型事件流处理,再次点击span时候会输出如下:


span click

如果有元素绑定了捕获类型事件,则会优先于冒泡类型事件而先执行。

2.事件处理程序

简单理解事件处理程序,就是响应某个事件的函数,例如onclick()函数、onload()函数就是响应单击、加载事件的函数,对应的是一段JavaScript的函数代码。
根据W3C DOM标准,事件处理程序分为DOM0、DOM2、DOM3这3种级别的事件处理程序。由于在DOM1中并没有定义事件的相关内容,因此没有所谓的DOM1级事件处理程序。

  • DOM0级事件处理程序
    DOM0级事件处理程序是将一个函数赋值给一个事件处理属性,有两种表现形式。第一种是先通过JavaScript代码获取DOM元素,再将函数赋值给对应的事件属性。
var btn = document.getElementById("btn"); 
btn.onclick = function(){}

第二种是直接在html中设置对应事件属性的值,值有两种表现形式,一种是执行的函数体,另一种是函数名,然后在script标签中定义该函数。

<button onclick="alert('hi');">单击</button>
<button onclick="clickFn()">单击</button>
<script>
    function clickFn() {
        alert('hi');
    }
</script>

以上两种DOM0级事件处理程序同时存在时,第一种在JavaScript中定义的事件处理程序会覆盖掉后面在html标签中定义的事件处理程序。
DOM0级事件处理程序只支持事件冒泡阶段,一个事件处理程序只能绑定一个函数。

  • DOM2级事件处理程序
    在DOM2级事件处理程序中,当事件发生在节点时,目标元素的事件处理函数就会被触发,而且目标元素的每个祖先节点也会按照事件流顺序触发对应的事件处理程序。DOM2级事件处理方式规定了添加事件处理程序和删除事件处理程序的方法。
    在IE10及以下版本中,只支持事件冒泡阶段
element.attachEvent("on"+ eventName, handler);         //添加事件处理程序
element.detachEvent("on"+ eventName, handler);         //删除事件处理程序

在IE11及其他非IE浏览器中,同时支持事件捕获和事件冒泡两个阶段,可以通过addEventListener()函数添加事件处理程序,通过removeEventListener()函数删除事件处理程序。

addEventListener(eventName, handler, useCapture);       //添加事件处理程序
removeEventListener(eventName, handler, useCapture);  //删除事件处理程序
//其中的useCapture参数表示是否支持事件捕获,true表示支持事件捕获,false表示支持事件冒泡,默认状态为false。
  • DOM3级事件处理程序
    DOM3级事件处理程序是在DOM2级事件的基础上重新定义了事件,也添加了一些新的事件。最重要的区别在于DOM3级事件处理程序允许自定义事件,自定义事件由createEvent("CustomEvent")函数创建,返回的对象有一个initCustomEvent()函数,通过传递对应的参数可以自定义事件。
    函数可以接收以下4个参数。
    · type:字符串、触发的事件类型、自定义,例如“keyDown”“selectedChange”。
    · bubble(布尔值):表示事件是否可以冒泡。
    · cancelable(布尔值):表示事件是否可以取消。
    · detail(对象):任意值,保存在event对象的detail属性中。
    创建完成的自定义事件,可以通过dispatchEvent()函数去手动触发,触发自定义事件的元素需要和绑定自定义事件的元素为同一个元素。
<body>
<div>
    <p>
            <span>
                自定义事件
            </span>
    </p>
</div>
</body>
<script>
    var customEvent;
    (function () {
        if (document.implementation.hasFeature('CusomEvents', '3.0')) {
            let detailData = {name: 'climber.lee'}
            customEvent = document.createEvent('CustomEvent');
            customEvent.initCustomEvent('myEvent', true, false, detailData)
            let p = document.querySelector('p');
            p.addEventListener('myEvent', function (e) {
                console.info(`p监听到自定义事件执行,参数为:`,e.detail)
            });
            let span = document.querySelector('span');
            span.addEventListener('click', function () {
                p.dispatchEvent(customEvent);
            })
        }
    })();
</script>

点击span后输出


自定义事件

3.事件(Event)对象

事件在浏览器中是以Event对象的形式存在的,每触发一个事件,就会产生一个Event对象。该对象包含所有与事件相关的信息,包括事件的元素、事件的类型及其他与特定事件相关的信息。
在Event对象中有两个属性总是会引起大家的困扰,那就是target属性和currentTarget属性。两者都可以表示事件的目标元素,但是在事件流中两者却有不同的意义。
· target属性在事件目标阶段,理解为真实操作的目标元素。
· currentTarget属性在事件捕获、事件目标、事件冒泡这3个阶段,理解为当前事件流所处的某个阶段对应的目标元素。
可以参考第一部分事件流的实例来理解target和currentTarget的区别
有时我们并不想要事件进行冒泡,可通过stopPropagation和stopImmediatePropagation两个方法阻止冒泡
· stopPropagation()函数仅会阻止事件冒泡,其他事件处理程序仍然可以调用。
· stopImmediatePropagation()函数不仅会阻止冒泡,也会阻止其他事件处理程序的调用。
在众多的HTML标签中,有一些标签是具有默认行为的
· a标签,在单击后默认行为会跳转至href属性指定的链接中。
那么该如何编写代码来阻止元素的默认行为呢?
很简单,就是通过event.preventDefault()函数去实现。

4.事件委托

过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。例如,click事件冒泡到document。这意味着可以为整个页面指定一个onclick事件处理程序,而不用为每个可点击元素分别指定事件处理程序。举个栗子

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

这里的HTML包含3个列表项,在被点击时应该执行某个操作。对此,通常的做法是像这样指定3个事件处理程序

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

item1.addEventListener("click", (event) => {
  location.href = "http:// www.wrox.com";
});

item2.addEventListener("click", (event) => {
  document.title = "I changed the document's title";
});

item3.addEventListener("click", (event) => {
  console.log("hi");
});

如果对页面中所有需要使用onclick事件处理程序的元素都如法炮制,结果就会出现大片雷同的只为指定事件处理程序的代码。使用事件委托,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题。

let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
  let target = event.target;

  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":
      console.log("hi");
      break;
  }
});

事件委托具有如下优点

  • document对象随时可用,任何时候都可以给它添加事件处理程序(不用等待DOMContentLoaded或load事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省DOM引用,也可以节省时间。
  • 减少整个页面所需的内存,提升整体性能。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容