DOM系列-事件

客户端JavaScript程序使用异步事件驱动的编程模型。在这种编程风格下,浏览器会在文档、浏览器或者某些元素或与之关联的对象发生某些值得关注的事情时生成事件。事件往往是元素本身所具备的,我们需要做的是为这些事件做出反馈,也就是当一个事件触发时,程序需要做些什么。在JavaScript中事件可以注册一个或多个函数,让这些函数在该类型事件发生时被调用。

任意具有图形用户界面的应用都是这样的,界面就在那里等待用户与之交互(等待事件的发生),然后给出反馈。

事件类型

事件类型是一个字符串,也叫事件名称,表示发生了什么事。例如,"mousermove"表示用户移动了鼠标,"keydown"表示用户按下了键盘上的某个键。而"load"表示文档(或其他资源)已经通过网络加载完成。

事件目标

事件目标也叫事件源,它是一个对象,事件就发生在该对象上或者与该对象有关。比如window对象发生了加载(load)事件,或者一个<button>元素发生了单机(click)事件。

注册事件处理函数

有两种注册事件处理函数的方式,第一种方式是设置作为事件目标的对象或文档元素的一个属性。第二种是把事件处理函数传给这个对象或元素的addEventListener()方法。

设置事件处理程序属性:JavaScript

第一种方式是把事件目标的一个属性设置为关联的事件处理函数,按照惯例,事件处理函数的名字都由on和事件名称组成,比如onclickonloadonchange等等,注意这些名称必须全部小写。所以一个完整的事件由以下几个部分组成:

  • 元素(事件目标) -> oDiv
  • 句柄(事件类型) -> on + eventType
  • 处理函数(事件反馈) - > onclick = function(){...}
ini 复制代码
oDiv.onclick = function(){
    this.style.backgroundColor = 'orange';
}

使用属性设置事件处理函数有一个缺点,无法绑定多个同类型事件的处理函数,后一个会覆盖前一个。比如以下代码中,当我们点击div元素时,控制台只会打印出2。

xml 复制代码
<div>

</div>
<script>
    var oDiv = document.getElementsByTagName('div')[0];

    oDiv.onclick = function(){
        console.log(1); // 不会打印
    }

    oDiv.onclick = function(){
        console.log(2);
    }
</script>

设置事件处理程序属性:HTML

文档元素的事件处理函数可以直接在HTML文件中作为对应HTML标签的属性来定义,也叫内联事件监听器(不建议使用,因为结构应该和事件要分开。)

ini 复制代码
<button onclick="console.log('点击成功')">点击我</button>

addEventListener()

任何可以作为事件目标的对象都定义了一个名为addEventListener()的方法,可以使用它来注册目标为调用对象的事件处理函数。

addEventListener()方法 也叫事件监听器,它可以接收三个参数,第一个参数是事件类型,是一个字符串(不需要加on)。第二个参数是事件处理函数,第三个是可选参数,用于设置事件流类型是否为捕获,默认为fasle。(IE8及以下不兼容)。

ini 复制代码
var oBtn = document.getElementsByTagName('button')[0];

oBtn.addEventListener('click', function(){
    this.innerHTML = '加载中...';

    var self = this;
    setTimeout(function(){
        self.innerHTML = '加载完毕';
    }, 2000)
}, false);

它可以绑定多个同类型事件的处理函数,执行顺序是按照绑定的先后顺序执行。以下代码会打印两次1。

javascript 复制代码
var oBtn = document.getElementsByTagName('button')[0];

oBtn.addEventListener('click', function(){
    console.log(1);
}, false);

oBtn.addEventListener('click', function(){
    console.log(1);
}, false);

如果绑定事件的处理函数是同一个,多次绑定也只执行一次,以下代码只会打印一次1。

javascript 复制代码
var oBtn = document.getElementsByTagName('button')[0];

function test(){
    console.log(1);
}

oBtn.addEventListener('click', test, false);
oBtn.addEventListener('click', test, false);

针对IE8版本及以下的浏览器,可以使用attachEvent()方法注册事件处理函数,它有两个参数,第一个参数是事件类型(需要加on),第二个参数是事件处理函数。

attachEvent()一样可以绑定多个同类型事件的处理函数,执行顺序是后绑定先执行(并且如果绑定事件的处理函数是同一个,多次绑定也会多次执行)。但要注意它的处理函数是window调用的,需要修改this的指向。

为了兼容性,我们可以对事件监听器做一下封装,注意要修改this指向。

rust 复制代码
function addEvent(el, type, fn){
    if(el.addEventListener){
        el.addEventListener(type, fn, false);
    }else if(el.attachEvent){
        el.attachEvent('on' + type, function(){
            // 使用call修改this指向
            fn.call(el);
        })
    }else{
        el['on' + type] = fn;
    }
}

调用事件处理程序

注册事件处理函数后,浏览器会在指定对象发生指定事件时自动调用它。

事件处理程序的参数

当事件的处理函数被触发时,浏览器每次都会将一个事件对象(event)作为参数传递给处理函数。DOM中存在着多种不同类型的事件对象,但是它们有一个共同祖先Event函数。

注意在IE8及以下版本的浏览器中,event是存在window对象中的,所以要考虑兼容性写法。

javascript 复制代码
oBtn.addEventListener('click', function(e){
    var e = e || window.event;
}, false);

这个event对象具有很多属性,这些属性提供了事件的详细信息。因为是参数,所以事件对象也可以用其它名字代替,比如常用e表示。

type属性 返回发生事件的类型。

target属性和srcElement属性 返回触发此事件的目标,谷歌浏览器这两个属性都有,火狐只有target,IE只有srcElement,所以又需要考虑兼容性写法:

ini 复制代码
oBtn.addEventListener('click', function(e){
    var e = e || window.event,
        tar = e.target || e.srcElement;
}, false);

currentTarget属性 返回注册当前事件处理函数的对象(同this)。

事件对象中封装了当前事件相关的一切信息,比如鼠标事件对象中有鼠标的水平/垂直坐标,键盘事件对象中有按下的键名等。

  • clientX/clientY属性 返回当前可视窗口的鼠标指针的水平/垂直坐标(IE8及以下版本不支持)。

  • pageX/pageY属性 返回相对于整个页面的鼠标指针的水平/垂直坐标,这个方法是包含页面的边框的。(IE8及以下版本不支持)。

事件处理程序的this指向

在通过设置属性注册事件处理函数时,看起来就像是为事件目标的对象定义了一个新的方法,作为方法调用时,this自然是指向该对象的,很合理。使用addEventListener()方法注册也是一样,以事件目标作为其this值。

不过要注意箭头函数除外,箭头函数中的this的值始终等定义它的作用域的值。

解绑事件处理程序

JavaScript中事件的解绑一般遵循"用什么方式绑定,就用什么方式解绑",不过需要注意的是,addEventListener()方法和attachEvent()方法绑定的函数不能是匿名函数,否则将无法解绑。

elem.onclick = null/fasle

elem.removeEventListener()

elem.detachEvent()

非严格模式下可以用callee

javascript 复制代码
var oBtn = document.getElementsByTagName('button')[0];

oBtn.addEventListener('click', function(){
    this.className = 'got';
    this.innerHTML = '已领取';

    this.removeEventListener('click', arguments.callee, false);
}, false)

严格模式下只能将事件处理函数写在外部,然后进行解绑。

javascript 复制代码
var oBtn = document.getElementsByTagName('button')[0];

oBtn.addEventListener('click', test, false)

function test(){
    this.className = 'got';
    this.innerHTML = '已领取';

    this.removeEventListener('click', test, false);
}

事件传播

如果事件的目标是Window或其它独立对象,浏览器对这个事件的响应就是简单地调用该对象上对应的事件处理函数。如果事件目标是Document或其它文档元素,就没有那么简单了,涉及到元素嵌套的问题。

事件冒泡

多数事件都会沿DOM树向上"冒泡"。事件冒泡指的是事件的向上传导,当目标元素的事件被触发时,其祖先元素的相同事件也会触发,一层一层向上传导触发(这种传导是由HTML结构决定的,和CSS样式无关),一直到Document对象,然后到Window对象。

以下代码中,当我们点击inner时,其外部的两个div也会触发点击事件。

css 复制代码
.wrapper{
    width: 300px;
    height: 300px;
    background-color: green;
}

.wrapper .outer{
    width: 200px;
    height: 200px;
    background-color: red;
}

.wrapper .outer .inner{
    width: 100px;
    height: 100px;
    background-color: orange;
}
xml 复制代码
<div class="wrapper">
    <div class="outer">
        <div class="inner">

        </div>
    </div>
</div>

<script>
    var wrapper = document.getElementsByClassName('wrapper')[0],
        outer = wrapper.getElementsByClassName('outer')[0],
        inner = outer.getElementsByClassName('inner')[0];

    wrapper.addEventListener('click', function(){
        console.log('wrapper');
    }, false)
    outer.addEventListener('click', function(){
        console.log('outer');
    }, false)
    inner.addEventListener('click', function(){
        console.log('inner');
    }, false) 
</script>

事件捕获

事件捕获 是一种向下传导模式,当目标元素的事件被触发时,从最外层祖先元素开始向内捕获目标元素(默认情况下这个过程不会调用事件处理函数)。

事件传播阶段

IE提出的是事件冒泡流(Event Bubbling)。

Netscape提出的是事件捕获流(Event Capturing)。

W3C综合以上两种事件流,将事件传播分为三个阶段:

  1. 捕获阶段 从最外层祖先元素开始向内捕获目标元素(默认情况下这个过程不会调用事件处理函数)。
  2. 目标阶段 事件捕获到目标元素,并开始调用事件处理函数。
  3. 冒泡阶段 事件从目标元素开始向祖先元素一层一层传递,依次触发同类型事件并调用事件处理函数。

如果需要在捕获阶段触发事件,比如addEventListener()监听器绑定事件处理函数时,第三个参数设置true即可。

  • 当我们点击inner时,会发现依次打印的是wrapperouterinner。这是因为捕获阶段是从最外层祖先元素开始向内捕获目标元素,而因为我们设置了在捕获阶段触发事件,所以是最外层祖先元素先触发事件,之后依次向下。
css 复制代码
.wrapper{
    width: 300px;
    height: 300px;
    background-color: green;
}

.wrapper .outer{
    width: 200px;
    height: 200px;
    background-color: red;
}

.wrapper .outer .inner{
    width: 100px;
    height: 100px;
    background-color: orange;
}
xml 复制代码
<div class="wrapper">
    <div class="outer">
        <div class="inner">

        </div>
    </div>
</div>

<script>
    var wrapper = document.getElementsByClassName('wrapper')[0],
        outer = wrapper.getElementsByClassName('outer')[0],
        inner = outer.getElementsByClassName('inner')[0];

    wrapper.addEventListener('click', function(){
        console.log('wrapper');
    }, true)
    outer.addEventListener('click', function(){
        console.log('outer');
    }, true)
    inner.addEventListener('click', function(){
        console.log('inner');
    }, true) 
</script>

优先级

并且还有两点要注意,首先捕获阶段的优先级是高于冒泡阶段的,其次对于事件目标本身而言,是不存在冒泡或者捕获的,它就只是按照程序执行顺序去执行。

事件对象有一个eventPhase属性 返回事件触发的阶段,返回值1|2|3分别表示捕获|目标|冒泡,我们可以利用该属性去看程序是在哪个阶段执行。

以下代码中,当我们点击inner时,依次打印结果如下。由此可见:

  • 首先是第三个参数设置为true的事件在捕获阶段调用事件处理函数。
  • 然后是事件目标inner调用事件处理函数。
  • 最后是第三个参数设置为false的事件在冒泡阶段调用事件处理函数。
javascript 复制代码
wrapper.addEventListener('click', function(e){
    console.log('bubblewrapper', e.eventPhase);
}, false);
outer.addEventListener('click', function(e){
    console.log('bubbleouter', e.eventPhase);
}, false);
inner.addEventListener('click', function(e){
    console.log('bubbleinner', e.eventPhase);
}, false);

wrapper.addEventListener('click', function(e){
    console.log('wrapper', e.eventPhase);
}, true);
outer.addEventListener('click', function(e){
    console.log('outer', e.eventPhase);
}, true);
inner.addEventListener('click', function(e){
    console.log('inner', e.eventPhase);
}, true);

多数事件都会冒泡,但focus、blur、change、submit、reset、select等事件是没有冒泡和捕获阶段的。

事件取消

浏览器对很多用户事件都会做出响应,比如用户在一个链接上点击鼠标,浏览器会跟随该链接。再比如用户在浏览器右击会打开一些菜单。有时候我们不希望这些默认动作,就可以通过事件对象的preventDefault()方法取消其默认动作(IE9及以下版本浏览器不支持)。

ini 复制代码
document.oncontextmenu = function(e){
    var e = e || window.event;
    e.preventDefault();
    console.log(1);
}

事件对象还有一个returnValue属性,默认值是true,IE9及以下版本浏览器可以通过将该属性设为false实现取消默认动作。

ini 复制代码
document.oncontextmenu = function(e){
    var e = e || window.event;
    // e.preventDefault();
    e.returnValue = false;
    console.log(1);
}

如果我们使用设置属性的方式注册事件处理函数,还有一种兼容性更好的取消默认动作的写法,直接在函数体中写return false,但是这种写法仅限于这种注册方式。

javascript 复制代码
document.oncontextmenu = function(e){
    return false;
    console.log(1);
}

取消与事件关联的默认动作只是事件取消的一种情况,除此之外,还可以看调用事件对象的stopPropagation()方法,取消事件传播。

stopPropagation()方法是W3C的方法规定的方法,IE浏览器没有该方法。

xml 复制代码
<div class="wrapper">
    <div class="outer">
        <div class="inner">

        </div>
    </div>
</div>

<script>
    var wrapper = document.getElementsByClassName('wrapper')[0],
        outer = wrapper.getElementsByClassName('outer')[0],
        inner = outer.getElementsByClassName('inner')[0];

    wrapper.addEventListener('click', function(){
        console.log('bubblewrapper');
    }, false);
    outer.addEventListener('click', function(){
        console.log('bubbleouter');
    }, );
    inner.addEventListener('click', function(e){
        e.stopPropagation();
        console.log('bubbleinner');
    }, false);
</script>

IE提供的是事件对象的一个属性cancelBubble,默认值为false,将其设为true即可阻止事件传播。

针对兼容性我们可以对阻止事件传播进行封装:

ini 复制代码
function cancelBubble(e){
    var e = e || window.event;
    if(e.stopPropagation){
        e.stopPropagation();
    }else{
        e.cancelBubble = true;
    }
}

事件委托与代理

事件委托(event delegation)是指将事件处理函数给一个祖先元素注册,后代元素的同一类型事件触发时,会冒泡到祖先元素,这样就可以利用祖先元素绑定的处理函数完成对后代元素的处理,减少了事件绑定的次数,提高程序性能。

  • 以下代码中我们只给ul注册点击事件,利用事件捕获可以让所有的li都可以触发该事件。我们之前提到了事件对象的属性,这里可以看出target返回的是触发点击事件的li元素,而currentTarget返回的是注册事件的ul元素。
xml 复制代码
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

<script>
    var oList = document.getElementsByTagName('ul')[0], 
        oLi = oList.getElementsByTagName('li'),
        len = oLi.length,
        item;

    oList.onclick = function(e){
        var e = e || window.event,
            tar = e.target || e.srcElement

        console.log(tar); // <li>...</li>
        console.log(e.currentTarget); // <ul>...</ul>
    }
</script>
  • 如果我们要打印li元素在ul中的索引,可以使用call去调用indexOf()方法。
xml 复制代码
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

<script>
    var oList = document.getElementsByTagName('ul')[0], 
        oLi = oList.getElementsByTagName('li'),
        len = oLi.length,
        item;

    oList.onclick = function(e){
        var e = e || window.event,
            tar = e.target || e.srcElement

        var index = Array.prototype.indexOf.call(oLi, tar);
        console.log(index);
    }
</script>
相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu7 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss