8.dom事件

216 阅读16分钟

dom事件

术语

  • 事件:发生一件事

  • 事件类型:发生什么事情;点击、鼠标按下、鼠标抬起、鼠标移入、鼠标移出、键盘按下、键盘抬起...

  • 事件处理程序:一个函数,当某件事情发生时运行。

  • 事件注册:将一个事件处理程序,挂载到某个事件上。

事件流

事件流:当某个事件发生的时候,哪些元素会监听到该事件发生,这些元素发生该事件的顺序。

事件冒泡:先触发最里层的元素,然后再依次触发外层元素 事件捕获:先触发外层的元素,然后再依次触发里面元素 上面的外层里层可以理解为父,祖先元素,子元素。 当一个元素发生了某个事件时,那该元素的所有祖先元素都发生了该事件

事件冒泡是由IE提出的,而事件捕获则是由Netscape(网景)提出的事件流概念。 后来ECMAScript将两种模型进行了整合,制定了统一的标准:先捕获再冒泡

事件流.jpg

现在整合后的标准事件流就有了三个阶段:

  1. 事件捕获阶段(目标在捕获阶段不接收事件)
  2. 目标阶段 (事件的执行阶段,此阶段会被归入冒泡阶段)
  3. 事件冒泡阶段 (事件传回Dom根节点)

Tips: DOM2级事件规定了在捕获阶段不会涉及到目标阶段事件,但在IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发目标事件上的事件

目前,标准规定,默认情况下(现在的默认情况是在冒泡触发,但并不是代表没有事件的处理没有捕获阶段,这是两个概念),事件是冒泡的方式触发。 上面的这句话和前面的事件流是先捕获后冒泡不冲突。

事件源、事件目标:事件目标阶段的元素

事件注册

事件绑定

dom0

将事件名称前面加上on,作为dom的属性名,给该属性赋值为一个函数,即为事件注册。

移除:重新给事件属性赋值,通常赋值为null和undefined

dom2

dom对象.addEventListener:注册事件

与dom0的区别

  1. dom2可以为某个元素的同一个事件,添加多个处理程序,按照注册的先后顺序运行
 <script>
        var btn = document.getElementById("btn1");
        btn.addEventListener("click", function(){
            console.log("1");
        })

        btn.addEventListener("click", function(){
            console.log("2");
        })
    </script>
    

addEventListener 为同一个元素添加多个事件,依次执行。

  1. dom2允许开发者控制事件处理的阶段,使用第三个参数,表示是否在捕获阶段触发,传true 表示在捕获阶段触发,传false表示在冒泡阶段触发。默认在冒泡阶段触发。
    1. 如果元素是目标元素(事件源),第三个参数无效,啥是目标元素呢?目标元素就是你最终点击或者进行其他动作的元素。如果目标元素第三个参数都是传一样的值,那么还是按照注册先后执行,如果一个是传false,一个是传true,那么在传true的先执行。
    2. 第三个参数有两种写法,可以直接传一个值就是 true或者false,表示是否在捕获阶段触发事件,还有可以传一个配置对象。配置对象很明显就是为了传多个值,这个配置对象就可以传两个值,一个表示是否在捕获阶段触发,一个是事件是否就执行一次。
 <script>
        var btn = document.getElementById("btn1");
        btn.addEventListener("click", function(){
            console.log("1");
        })

        btn.addEventListener("click", function(){
            console.log("2");
        },true)
    </script>

上面的代码输出结果就是 2,1。因为前面的一个事件的注册是在默认阶段,也就是冒泡阶段,而后面的一个事件是在捕获阶段,可以看下上面的图,事件都是先捕获而后冒泡的。

第三个参数写成对象的示例

 div.addEventListener("click", divHandler1, {
          capture: true, //是不是在冒泡阶段捕获的
          once:true,//是不是在就执行一次
      })

事件的移除:dom对象.removeEventListener(事件名, 处理函数);

dom2中如果要移除事件,不能使用匿名函数 为什么不能使用匿名函数呢? 如果你使用匿名函数添加的事件,后面你在移除这个方法的时候,你无法拿到事件对象,然后你就无法移除。

btn.addEventListener("click", function(){
        console.log("这是一个匿名函数");
    });
btn.removeEventListener("click",function(){
    console.log("这是一个匿名函数");
});

上面看着函数是同一个,其实不是,是因为函数本身也是一个对象,两个步骤是两个函数对象,所以后者并不能将前面的那个移除掉。

正确的做法是:将函数的对象用一个参数引用着

function handler1() {
console.log("btn1");
}

btn.addEventListener("click", handler1, {
once: true
});

btn.removeEventListener("click", handler1);

细节

  1. dom2在IE8及以下不兼容,需要使用attachEvent,detachEvent添加和移除事件
  2. 添加和移除事件时,可以将第三个参数写为一个对象,进行相关配置

事件对象

事件对象封装了事件的相关信息

获取事件对象

  • 通过事件处理函数的参数获取
  • 旧版本的IE浏览器通过window.event获取

获取事件处理函数的参数的方式

var a = document.querySelector("a");
var btn = document.querySelector("button");

a.addEventListener("click", function(e) {
    // e.preventDefault(); //阻止浏览器的默认行为
    console.log("被点击了!");
})

btn.onclick = function(e){
// 下面三行代码,任何一行都可以阻止浏览器的默认行为。
    return false;
    // e.preventDefault();
    // e.returnValue = false;
}

事件对象的通用成员(包括本章点的所有事件 鼠标事件,键盘事件或者其他等,上面的代码就是方法里面的e)

  • target & srcElement 正常使用target就够了,这里用srcElement是为解决兼容性。

事件目标(事件源)可以理解为iOS里面highTest寻找最合适的view,但是这里寻找target的过程和寻找最佳view 的过程有区别的

事件委托:通过给祖先元素注册事件,在程序处理程序中判断事件源进行不同的处理。

通常,事件委托用于动态生成元素的区域。 还有一种情况是区域内的子元素太多了,添加或者删除事件都太麻烦了。

  • currentTarget 当前目标:获取绑定事件的元素,等效于this,可以看做上面onclick 和 addEventListener的调用者。而不是上面的target.举一个例子理解一下,比方说div里面有很多几个button,对div注册点击事件,此时点击了button,target就是button,而currentTarget就是当前的点击事件注册对象div。

  • type

字符串,得到事件的类型

  • preventDefault & returnValue

preventDefault方法

阻止浏览器默认行为。可以理解阻止浏览器的正常的行为,比方说本该输入文字的,阻止后就不输入了。默认行为是一个统称。

<a href="https://baidu.com">百度</a>
var a = document.querySelector("a");
       
a.addEventListener("click", function(e) {
    e.preventDefault(); //阻止浏览器的默认行为
    console.log("被点击了!");
})

a标签默认行为点击会跳转,这里调用e.preventDefault()之后a标签就不会再有默认行为跳转了。

<form action="https://www.taobao.com">
        <p>
            <input type="text" name="account">
        </p>
        <p>
            <button>提交</button>
        </p>
</form>


var btn = document.querySelector("button");
btn.onclick = function(e){
        return false;
        // e.preventDefault();
        // e.returnValue = false;
    }

阻止form标签的默认行为。

dom0的方式:在事件处理程序中返回false

针对a元素,可以设置为功能性链接解决跳转问题。跳转对a元素来说就是默认行为。

  • stopPropagation方法

阻止事件冒泡,propagation的意思就是传播的意思,stopProgagatition就是阻止冒泡,阻止事件的传播。

  • eventPhase

得到事件所处的阶段,phase的意思是阶段,连起来就是事件所处的阶段。这个属性是一个枚举值。捕获阶段得到是1,目标阶段是2,冒泡的阶段是

1: 事件捕获 2: 事件目标 3: 事件冒泡

鼠标事件

事件类型

  • click:用户单击主鼠标按钮(一般是左键)或者按下在聚焦时按下回车键时触发
  • dblclick:用户双击主鼠标按键触发(频率取决于系统配置)mac 系统上触发可能有问题,目前上不知道原因。
  • mousedown:用户按下鼠标任意按键时触发 这个很简单,按下就会触发。
  • mouseup:用户抬起鼠标任意按键时触发 这个也很简单,抬起就会触发。
  • mousemove:鼠标在元素上移动时触发
  • mouseover:鼠标进入元素时触发(移动进入就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
  • mouseout:鼠标离开元素时触发 (移动离开就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
  • mouseenter:鼠标进入元素时触发,该事件不会冒泡(鼠标进入元素时触发(移动进入就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
  • mouseleave:鼠标离开元素时触发,该事件不会冒泡(移动离开就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
    var btn = document.querySelector("button")
    btn.onclick = function(){
        console.log("被点击了!");
    }

    btn.ondblclick = function(){
        console.log("被双击了!");
    }

    btn.onmousedown = function(){
        console.log("按下!");
    }

    btn.onmouseup = function(){
        console.log("抬起");
    }

    btn.onmousemove = function(){
        console.log("移动");
    }

    btn.onmouseover = function(){
        console.log("进入元素:over");
    }

    btn.onmouseout = function(){
        console.log("离开元素:out");
    }

    btn.onmouseenter = function(){
        console.log("进入元素:enter");
    }

    btn.onmouseleave = function(){
        console.log("离开元素:leave");
    }

区别:

  • over和out,不考虑子元素,从父元素移动到子元素,对于父元素而言,仍然算作离开
  • enter和leave考虑子元素,子元素仍然是父元素的一部分。如果我们平时子元素和当前的元素处理的事件是一致的话,给当前元素注册这个事件就可以了
  • mouseenter和mouseleave不会冒泡

stopPropagation 是阻止事件冒泡,阻止的事件就是当前的注册事件,这点很重要。当前元素会触发这个注册事件,不希望他的父元素再触发这个事件了。正常是父元素也会触发这个事件。

div.onmouseover = function(e){
    console.log("鼠标进入:over", e)
}

div.onmouseout = function(e){
    console.log("鼠标离开:out", e)
}

btn.onmouseover = function(e){
    e.stopPropagation();
}
btn.onmouseout = function(e){
    e.stopPropagation();
}

Snip20231230_3.png

  1. 正常进入div,会触发over,出去会触发out。
  2. 如果从div进入button,因为上面讲的over 和out 的特点,不考虑子元素,从父元素移动到子元素,对于父元素而言,仍然算作离开,所以会触发一次 out,但是div 还会再触发div 一次 over,这是为什么呢?这是因为进入其实是触发了button 的over事件,因为这个over事件有冒泡,所以会触发div的over事件。这就是根源,怎么解决呢?解决办法就是buton 的onmouseover阻止他冒泡传递给button。同样的原因,button out 的时候也会触发div 的out事件,原因同样是因为冒泡导致的。
  3. 上面的几行代码就可以真正的做到进入到button就只触发button的方法,进入div只触发div 的相关方法。

对于enter和leave呢?

  1. 进入div,进入会触发 enter,出去会触发 leave。
  2. 如果从div进入button,会触发button的enter,但不会触发div 的leave,从button 出去,会触发button的leave,同时也会触发div 的leave。

事件对象

所有的鼠标事件,事件处理程序中的事件对象,都为 MouseEvent

  • altKey:触发事件时,是否按下了键盘的alt键
  • ctrlKey:触发事件时,是否按下了键盘的ctrl键
  • shiftKey:触发事件时,是否按下了键盘的shift键
  • button:触发事件时,鼠标按键类型
    • 0:左键
    • 1:中键
    • 2:右键
btn.onmouseup = function(e){
   console.log(e.altKey, e.ctrlKey, e.shiftKey);
   console.log(e.button);
   }

上面的e.altKey e.ctrlKey e.shiftKey 代表事件触发时候用户是否也按着下面这些按键,因此返回值是ture 或者false.

e.button 返回的是数字,0 代表左键,1代表中键,2代表右键。

位置:

  • page:pageX、pageY,当前鼠标距离页面的横纵坐标,距离页面,这里包括上面的可滚动区域。
  • client: clientX、clientY,鼠标相对于视口的坐标
  • offset:offsetX、offsetY,鼠标相对于事件源的内边距的坐标
  • screen: screenX、screenY,鼠标相对于屏幕
  • x、y,等同于clientX、clientY
  • movement:movementX、movementY,只在鼠标移动事件中有效,相对于上一次鼠标位置,偏移的距离

键盘事件

事件类型

  • keydown:按下键盘上任意键触发,如果按住不放,会重复触发此事件
  • keypress:按下键盘上一个字符键时触发
  • keyup:抬起键盘上任意键触发

keydown、keypress 如果阻止了事件默认行为,文本不会显示。

事件对象

KeyboardEvent

键盘事件也有冒泡,因此在控件注册事件的时候尽量给大的组件比方说window,document注册事件。 如果你给一个div注册事件,事件是不会触发的,因为div不能聚焦,除非你给这个div加一个input元素,并且点一下这个input才能触发。

  • code:得到按键字符串,适配键盘布局。适配键盘布局代表可以键盘左右区域同样的按键问题。比方说你是按了左边的option还是右边的option的键盘。
  • key:得到按键字符串,不适配键盘布局。能得到打印字符。
  • keyCode、which:得到键盘编码
  <input type="text">
    <script>
        var inp = document.querySelector("input")
       
        //keycode 得到键盘编码
        inp.onkeydown = function(e) {
            console.log("键盘按下了", e.code, e.key, e.keyCode)
        }

        inp.onkeypress = function(e) {
            console.log("键盘按下了!key press")
        }

        inp.onkeyup = function() {
            console.log("键盘抬起了!key up")
        }
    </script>

其他事件

表单事件

  • focus:元素聚焦的时候触发(能与用户发生交互的元素,都可以聚焦),该事件不会冒泡。比方说a标签,input。 事件可以通过bubbles,知道该事件是否冒泡。
<div>
      <input type="text" class="noinput" value="请输入关键字">
</div>

<script>
    var inp = document.querySelector("input")
    inp.onfocus = function (e){
        console.log("聚焦了",e.bubbles);
    }
 </script>
  • blur:元素失去焦点时触发,该事件不会冒泡。
<style>
    .noinput {
        color: #ccc;
    }
</style>
 
<div>
   <input type="text" class="noinput" value="请输入关键字">
</div>

 <script>
        var inp = document.querySelector("input")

        inp.onfocus = function() {
            if (this.value === this.defaultValue && this.className === "noinput") {
                this.value = "";
                this.className = "";
            }
        }

        inp.onblur = function() {
            if (!this.value) {
                this.value = this.defaultValue;
                this.className = "noinput";
            }
        }
 </script>
  • submit:提交表单事件,仅在form元素有效。
 <form action="https://baidu.com">
    <p><input type="text" placeholder="请输入账号"></p>
    <button>按钮一</button> <button>按钮二</button>
 </form>
    
<script>
    var form = document.forms[0];
    var inp = document.querySelector("input");
    form.onsubmit = function (e){
        console.log(e.bubbles);
        if(!inp.value.trim()){
            e.preventDefault();
            console.log("请输入账号");
        }
    }
</script>
  • change:文本改变事件
  • input: 文本改变事件,即时触发

其他事件

window全局对象

  • load、DOMContentLoaded、readystatechange

window的load:页面中所有资源全部加载完毕的事件

图片的load:图片资源加载完毕的事件

浏览器渲染页面的过程:

  1. 得到页面源代码
  2. 创建document节点
  3. 从上到下,将元素依次添加到dom树中,每添加一个元素,进行预渲染
  4. 按照结构,依次渲染子节点

document的DOMContentLoaded: dom树构建完成后发生

readystate(准备的状态): loading(正在加载)、interactive(可交互)、complete(完成)

interactive:触发DOMContentLoaded事件

complete:触发window的load事件

image.png 渐变结束的事件:

js代码应该尽量写到页面底部

  • css应该写到页面顶部:避免出现闪烁(如果放到页面底部,会导致元素先没有样式,使用丑陋的默认样式,然后当读到css文件后,重新改变样式)
  • JS应该写到页面底部:避免阻塞后续的渲染,也避免运行JS时,得不到页面中的元素。

一道简单的面试题

//loading
console.log(document.readyState);

//interactive
document.addEventListener("DOMContentLoaded", function () {
    console.log(document.readyState);
})

//complete
window.onload = function(){
    console.log(document.readyState);
}

如何正确获取img的尺寸?

document.addEventListener("DOMContentLoaded", function () {
    var img = document.querySelector("img");
    getImgSize(img, function (size) {
        console.log(size);
    });
})

function getImgSize(img, callback) {
    if (img.width === 0 && img.height === 0) {
        img.onload = function () {
            callback({
                width: img.width,
                height: img.height
            });
        }
    }
    else {
        callback({
            width: img.width,
            height: img.height
        });
    }
}

  • unload、beforeunload

beforeunload: window的事件,关闭窗口时运行,可以阻止关闭窗口 unload:window的事件,关闭窗口时运行

  • scroll 窗口发生滚动时运行的事件 通过scrollTop和scrollLeft,可以获取和设置滚动距离。 这个事件不仅可以给window设置,也可以给元素设置值,类似于UIScrollview的offset. 其中scrollLeft相当于offset.x,scrollTop相当于offset.y.

image.png

 <p>
     dolor.此处1000个单词
 </p>
    <div>
        <button>回到顶部</button>
        <button>到底部</button>
    </div>
<script>
        window.onscroll = function(){
            console.log(document.documentElement.scrollTop + document.body.scrollTop);
        }
        var p = document.querySelector("p");
        p.onscroll = function() {
            console.log(this.scrollTop, this.scrollLeft);
        }

        document.getElementsByTagName("button")[0].onclick = function() {
            p.scrollTop = 0;
            p.scrollLeft = 0;
        }

        document.getElementsByTagName("button")[1].onclick = function() {
            p.scrollTop = p.scrollHeight;
            p.scrollLeft = p.scrollWidth;
        }
 </script>

注意的是给window注册onscroll滚动事件,要用document.documentElement 和document.body相加去获取,因为其中一个可能是0. 上面的代码后面是给他设置值。

  • resize 窗口尺寸发生改变时运行的事件,监听的是视口尺寸,注意这个事件只能给window注册,别的元素注册没有用。
window.onresize = function () {
}

window尺寸相关的参数具体指代

尺寸1.png

尺寸2.png

尺寸3.png

image.png 默认情况width是内容宽度。如果设置了boder-box 是加上border padding 后的宽度。

注意:offsetWidth 和clientWidth差别就差在滚动条上。

  • contextmenu 右键菜单事件,可以给元素注册事件,也可以给window注册事件。
  • paste 粘贴事件
  • copy 复制事件
  • cut 剪切事件

元素位置

  • offsetParent 获取某个元素第一个定位的祖先元素,如果没有,则得到body body的offsetParent为null

  • offsetLeft、offsetTop 相对于该元素的offsetParent的坐标 如果offsetParent是body,则将其当作是整个网页

  • getBoundingClientRect方法 该方法得到一个对象,该对象记录了该元素相对于视口的距离

image.png

一个吸附效果的代码实现

var div = document.querySelector(".container");
var scrollTop = 0;

window.onscroll = function() {
    var rect = div.getBoundingClientRect();
    if (rect.top < 0) {
        div.style.position = "fixed";
        div.style.top = 0;
        div.style.left = rect.left + "px";
        div.style.width = rect.width + "px";
        div.style.height = rect.height + "px";
        div.style.boxSizing = "border-box";
        //记录滚动条的位置
        scrollTop = window.scrollY + rect.top;
    }
    if (window.scrollY < scrollTop) {
        div.setAttribute("style", "");
    }
}

div.setAttribute("style", "") 相当于清空div的内部样式。 其实实现悬浮效果的核心逻辑是监听window的滚动,rect.top 表示div距离视口顶部的距离,rect.top < 0表示上边缘滚动到了视口的上面去了,这是就可以设置让他固定了,此时还有一个问题就是要记录一下,这个其实就是当前div上边缘的距离整个内容的位置。其实就是鼠标事件里面的pageY。这里可以通过window.scrollY 减去上边缘滚超的rect.top来计算。

事件模拟

可以用代码模拟用户的各种操作,点击按钮,提交表单,鼠标操作等事件。

  • click
  • sumbit
  • dispatchEvent

其他补充

  • window.scrollX、window.pageXOffset、window.scrollY、window.pageYOffset

window.scrollX、window.pageXOffset: 相当于根元素的scrollLeft window.scrollY、window.pageYOffset: 相当于根元素的scrollTop

  • scrollTo、scrollBy scrollTo: 手动设置滚动条位置,代表是整体值。 scrollBy(x,y)是代表原来的基础上增加了多少,代表的是偏移量。

  • resizeTo、resizeBy 差别上面的大小类似,一个是整体值,一个是偏移量,只有通过代码打开的窗口才能控制。

实践中一个注意点

image.png

上面的imgNumber无法用上面的写法,为什么呢?

image.png

赋值语句尚未完成,所以下面的写法也不行。因为运行右边语句的时候config还是undefined。

image.png

也不能使用this,因为this使用在函数中,这里不是函数,不能使用。这里如果用了是指向全局对象window。 this可以在两种情况下使用: 1.全局环境 2.函数里面

正确的做法是配置完对象之后再设置。

image.png

其实这就是在对象初始化的时候,因为一个属性要使用另外一个属性的问题,解决办法就是初始化完成后再去设置另外一个属性。

而下面这种写法是可以的。

image.png