javaScript 浏览器事件总结

javaScript 浏览器事件

1.事件基本概念

事件是指文档或者浏览器中发生的一些特定交互瞬间,比如打开某一个网页,浏览器加载完成后会触发load事件,当鼠标浮于某一个元素上时会触发hover事件,当鼠标点击某一个元素时会触发click事件等等。

事件处理就是当事件被触发吼,浏览器响应这个事件的行为,而这个行为所对应的代码即为事件处理程序

2.事件操作:监听与移除监听

2.1监听事件

浏览器会根据一些事件作出相对应的事件处理,事件处理的前提是需要监听事件,监听事件的方法主要有以下三种:

2.1.1 HTML内联属性

即在HTML元素里直接填写与事件相关的属性,属性值为事件处理程序。示例如下:

<button onclick="console.log('You clicked me!');"></button>

onclick对应着click事件,所以当按钮被点击后,便会执行事件处理程序,即控制台输出“You clicked me!”。

不过我们需要指出的是,这种方式将HTML代码与JavaScript代码耦合在一起,不利于代码的维护,所以应该尽量避免使用这样的方式。

2.1.2 DOM属性绑定

通过直接设置某个DOM节点的属性来指定事件和事件处理程序,上代码

const btn = document.getElementById("btn");
btn.onclick = function(e) {
    console.log("You clicked me!")
}

上面示例中,首先获得btn这个对象,通过给这个对象添加onclick属性的方式来监听click事件,这个属性值对应的就是事件处理程序。这段程序也被成为DOM 0级事件处理程序。

2.1.3 事件监听函数

标准的事件监听函数如下:

const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
    console.log("You click me!");
}, false);

上面的示例表示先获得表示节点的btn对象,然后在这个对象上面添加了一个事件监听器,当监听到click事件发生时,则调用回调函数,即在控制台输出“You clicked me!”addEventListener函数包含了第三个参数false,第三个参数的含义在后面的事件触发三个阶段之后再讲解。这段程序也被称作DOM 2级事件处理程序。IE9+、FireFox、Safari、Chrome 和 Opera 都是支持 DOM 2 级事件处理程序的,对于 IE8 及以下版本,则用 attacEvent() 函数绑定事件。

所以我们得写一段具有兼容性的代码:

function addEventHandler(obj, eventName, handler) {
    if (document.addEventListener) {
        obj.addEventListener(eventName, handler, false);
    } else if (document.attachEvent) {
        obj.attachEvent("on" + eventName, handler);
    } else {
        obj["on" + eventName] = handler;
    }
}
2.2移除事件监听

在为某个元素绑定了一个事件后,如果想解除绑定,则需要removeEventListener方法。

const handler = function() {
    // hanlder logic
}
const btn = document.getElementById("btn");

btn.addEventListener("click", handler);
btn.removeEventListener("click", handler);

需要注意的是,绑定事件的回调函数不能是匿名函数,必须是一个已经被声明的函数,因为解除事件绑定时需要传递这个回调函数的引用。

同样,IE8 及以下版本也不支持上面的方法,而是用 detachEvent 代替。

const handler = function() {
    // hnalder logic
}
const btn = document.getElementById("btn");

btn.attachEvent("onclick", handler);
btn.detachEvent("onclick", handler);

同样,可以写一段具有兼容性的删除事件函数:

function removeEventHandler(obj, eventName, handler) {
    if (document.removeEventListener) {
        obj.removeEventListener(eventName, handler, false);
    } else if (document.detachEvent) {
        obj.detachEvent("on" + eventName, handler);
    } else {
        obj["on" + eventName] = null;
    }
}
DOM事件级别

DOM级别一共可以分为四个级别:DOM0级DOM1级DOM2级DOM3级。而DOM事件分为3个级别:DOM 0级事件处理,DOM2级事件处理和DOM3级事件处理。由于DOM1级中没有事件的相关内容,所以没有DOM1级事件。

DOM0级事件

el.onclick=function(){}

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

当希望为同一个元素、标签绑定多个同类型事件的时候(如给上面的这个btn元素绑定3个点击事件),是不背允许的。DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。

DOM 2级事件

el.addEventListener(event-name,callback,useCapture)

  • event-name: 事件名称,可以是标准的DOM事件。
  • callback:回调函数,当事件触发时,函数会被注入一个参数为当前事件对象event。
  • useCpature:默认是false,代表事件句柄在冒泡执行阶段执行(或者说注册的是冒泡事件),true表示事件句柄在捕获阶段执行(或者说注册的是捕获事件)
var btn = document.getElementById('btn');
btn.addEventListener("click", test, false);
function test(e) {
    e = e || window.event;
    alert((e.target || e.srcElement).innerHTML);
    btn.removeEventListner("click", test);
}

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

DOM 3级事件

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

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

总结:

  • DOM2级的好处是可以添加多个事件处理程序;DOM0级对每个事件支持持一个事件处理程序;
  • 通过DOM2级添加的匿名函数无法移除,addEventListenerremoveEventListenerhandler必须同名
  • 作用域:DOM 0的handler会在所属元素的作用域内运行,IE的handler会在全局作用域运行,this === window
  • 触发顺序:添加多个事件时,DOM2会按照添加顺序执行,IE会以相反的顺序执行

3.事件触发过程

事件流描述了页面接收事件的顺序。现代浏览器事件流包含三个过程,分别是捕获阶段、目标阶段和冒泡阶段。

下面详细地讲解这三个过程。

3.1捕获阶段

当我们对 DOM元素进行操作时,比如鼠标点击、悬浮等,就会有一个事件传输到这个DOM元素,这个事件从Window开始,依次经过document、html、body,再不断经过过子节点直到到达目标元素,从 Window到达目标元素父节点的过程称为捕获阶段,注意此时还未到达目标节点。

3.2目标阶段

捕获阶段结束时,事件到达了目标节点的父节点,最终到达目标节点,并在目标节点上触发了这个时间,这就是目标阶段

需要注意的是,事件触发的目标节点为最底层的节点。比如下面这个例子:

<div>
    <p>
        你猜,目标在这里还是<span>哪里</span>
    </p>
</div>

当我们点击“哪里“的时候,目标节点是<span></span>,点击这里的时候,目标节点是<p></p>,而当我们点击<p></p>区域之外,<div></div>区域之内,目标节点就是<div></div>

3.3 冒泡阶段

当事件到达目标节点之后,就会沿着原路返回,这个过程有点类似于水泡从水底浮出水面的过程,所以称这个过程为冒泡阶段

现在再看addEventListener(eventName, handler, useCapture)函数。第三个参数是useCapture,代表是否在捕获阶段进行事件处理,如果是false,则在冒泡阶段进行事件处理,如果是true则在捕获阶段进行处理,默认是false

冒泡事件的流程刚好是事件捕获的逆过程。我们来看个事件冒泡的例子:

<div id="outer">
    <div id="inner">
        
    </div>
</div>
<script>
    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');
    }
</script>
// 输出为 inner outter body html dcoument window

4.事件委托

JavaScript中,事件的委托表示给元素的父级或者祖级,甚至页面,由他们来绑定事件,然后利用事件冒泡的基本原理,通过事件目标对象进行检测,然后执行相关操作。

事件委托有两个优点:

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

假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件。

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>

如果给每个列表项都一一绑定函数,那么对内存的消耗是非常大的,需要消耗更多性能。借助事件委托,我们只需要给父容器ul绑定方法即可,这样不管点击哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为处罚,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。

  • 动态绑定事件

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

接下来我们来实现上例中父元素#list下的li元素的事件委托到他的父层元素上:

// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function(e) {
    // 兼容性处理
    var event = e || window.event;
    var target = event.target || event.srcElement;
    // 判断是否匹配目标元素
    if (target.nodeName.toLocaleLowerCase === 'li') {
        console.log('the content is:', target.innerHTML);
    }
});

5.事件对象

DOM0DOM2的事件处理程序都会自动传入event对象,即触发DOM上的某个事件时,会产生一个事件对象,里面包含着所有和事件有关的信息。IE中的event对象取决于指定的事件处理程序的方法。

IE的handler会在全局作用域运行,this === window所以在IE中会有window.eventevent两种情况。

另外在IE中,事件对象的属性也不一样,对应关系如下:

srcElement => target returnValue => preventDefault() cancelBubble => stopPropagation() IE不支持事件捕获,因而只能取消事件冒泡,但stopPropagtion可以同时取消事件捕获和冒泡。

只有在事件处理程序期间,event对象才会存在,一旦事件处理程序执行完成,event对象就会被销毁。

1、event.preventDefault()

如果调用这个方法,默认事件行为将不在触发。什么是默认事件呢?例如表单 - 点击提交按钮跳转页面、a标签默认页面跳转或是锚点定位等。

很多时候我们使用a标签仅仅是想当做一个普通的按钮,点击实现一个功能,不想页面跳转,也不想锚点定位。

// 方法一
<a href="javascript:;">链接</a>

也可以通过JS方法来阻止,给其click事件绑定方法,当我们点击A标签的时候,先触发click事件,其次才会执行自己的默认行为

// 方法二
<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
    test.onclick = function(e) {
        e = e || window.event;
        return false;
    }
</script>

// 方法三
<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
    test.onclick = function(e) {
        e = e || window.event;
        e.preventDefalut();
    }
</script>

接下来我们看个例子:输入框最多只能输入六个字符,如何实现?

// 例子5
<input type="text" id="tempInp">
<script>
    tempInp.onkeydown = function(ev) {
        ev = ev || window.event;
        let val = this.value.trim();
        let len = val.lenght;
        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>

2.event.stopPropagation() & event.stopImmediatePropagation()

event.stopPropagation()方法阻止事件冒泡到父元素,阻止任何父事件处理程序被执行(一般我们认为stopPropagation是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件)。上面提到事件冒泡阶段是指事件从目标节点紫霞而上的向window对象传播的阶段。我们在上面的例子中的inner元素click事件上,添加event.stopPropagation()这句话后,就阻止了父事件的执行,最后只打印了'inner'

inner.onclick = function(ev) {
    console.log('inner');
    ev.stopPropagation();
}

stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件类型的其它监听器被触发。而 stopPropagation 只能实现前者的效果。我们来看个例子:

<body>
  <button id="btn">click me to stop propagation</button>
</body>
<script>
    const btn = document.querySelector('#btn');
    btn.addEventListener('click', event => {
      console.log('btn click 1');
      event.stopImmediatePropagation();
    });
    btn.addEventListener('click', event => {
      console.log('btn click 2');
    });
    document.body.addEventListener('click', () => {
      console.log('body click');
    });
// btn click 1
</script>

如上所示,使用stopImmediatePropagation后,点击按钮时,不仅body绑定事件不会触发,与此同时按钮的另一个点击事件也不触发。

event.target & event.currentTarget

event.target指向引起触发事件的元素,而event.currentTarget则是事件绑定的元素,只有被点击的那个目标元素的event.target才会等于event.currentTarget也就是说,event.currentTarget始终是监听事件这,而event.target是事件的真正发出者

6.捕获与冒泡的顺序问题

当有很多层交互嵌套时,事件捕获和时间冒泡的先后顺序看起来是不好确定的。下面分5种情况讨论给它们的顺序,以及如何规避意外情况的发生。

1、在外层div注册事件,点击内层div来触发事件时,捕获事件总是要比冒泡事件先触发(与代码顺序无关)

假设,有这样的html结构:

<div id="test" class="test">
    <div id="testInner" class="test-inner">
        
    </div>
</div>

然后,我们在外层div上注册两个click事件,分别是捕获事件和冒泡事件,代码如下:

const btn = document.getElementById("test");

// 捕获事件
btn.addEventListener("click", function(e) {
    alert("capture is ok");
}, true);

// 冒泡事件
btn.addEventListener("click", function(e) {
    alert("bubble is ok");
}, false)

点击内层的div,先弹出capture is ok,后弹出bubble is ok。只有当真正触发事件的 DOM元素是内层时,外层DOM元素才有机会模拟捕获事件和冒泡事件。

2、当在触发事件的DOM元素上注册事件时,那个先注册就先执行那个

html 结构同上,js代码如下:

const btnInner = document.getElementById("testInner");

// 冒泡事件
btnInner.addEventListener("click", function(e) {
    alert("bubble is ok");
}, false);

// 捕获事件
btnInner.addEventListener("click", function(e) {
    alert("caapture is ok");
}, true);

在本例中,冒泡事件先注册,所以先执行。所以,点击内层div,先弹出bubble is ok,再弹出caputre is ok

3、当外层div和内层div同时注册了捕获事件时,点击内层的div时,外层div的事件一定会先触发

const btn = document.getElementById("test");
const btnInner = document.getElementById("testInner");

btnInner.addEventListener("click", function(e) {
    alert("inner capture is ok");
}, true);

btn.addEventListener("click", function(e) {
    alert("outer capture is ok");
}, true)

虽然外层 div 的事件注册在后面,但会先触发。所以,结果是先弹出 outer capture is ok,再弹出 inner capture is ok

4、同理,当外层div和内层div都同时注册了冒泡事件,点击内层div时,一定是内层div事件先触发。

const btn = document.getElementById("test");
const btnInner = document.getElementById("testInner");

btn.addEventLisntener("click", function(e) {
    alert("outer bubble is ok");
}, false);

btnInner.addEventListener("click", function(e) {
    alert("inner bubble is ok");
}, false)

先弹出inner bubble is ok,再弹出outer bubble is ok

5、阻止事件的派发

通常情况下,我们都希望点击某个div时,就触发自己的事件回调。比如,明明点击的是内层div,但是外层div的事件也触发了,这就不是我们想要的了。这时,就需要阻止事件的派发。

事件触发时,会默认传入一个event对象,这个event对象上有一个方法:stopPropagation。MDN上的解释是:阻止捕获和冒泡阶段中,当前事件的进一步传播。所以通过此方法,让外层div接收不到事件,自然也就不会触发了。

btnInner.addEventListener("click", function(e) {
    // 阻止冒泡
    e.stopPropagation();
    alert(”inner bubble is ok“);
}, false);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容