JavaScript进阶—一文带你认识事件

前言

JavaScript与html之间的交互通过事件实现,它代表浏览器窗口或者是文档的某个特殊的时刻,可以使用事件发生时的处理程序来订阅事件,这也就是观察者模式,它能够做到行为和展示的分离

一、事件流

说明: 事件流描述了页面接收事件的顺序

1.事件冒泡

说明: IE的事件流被称为事件冒泡,其原因是事件在定义时会从最具体的元素开始触发,然后一直向上传播到文档节点

<!DOCTYPE html>
<html>
  <body>
    <div id="myDiv">Click Me</div>
  </body>
</html>

div上面的click事件触发后,会按照顺序,在bodyhtmldocument上都会触发这个事件

2.事件捕获

说明: 这也是一种事件流,只不过事件触发的顺序与事件冒泡相反,它会从最不具体的元素开始触发,也就是document,最具体的元素最后触发,在上面的例子也就是div,这样做的目的是为了在事件到达最终目标前拦截事件

如果以上面例子为例,其触发顺序:documenthtmlbodydiv

3.DOM事件流

说明: 规范中规定事件流存在三个阶段:事件捕获到达目标事件冒泡,事件捕获最先开始为了提前拦截事件提供可能,响应事件最迟在冒泡阶段完成,一般认为到达目标存在于冒泡阶段

对于上面的例子,捕获阶段是document -> html -> body,到达目标是body -> div,冒泡则是div -> body -> html -> document

二、事件处理程序

说明: 事件意味着用户或者浏览器执行了某个动作,比如上面的click点击事件,在事件结束会调用一个函数,这个函数称为事件处理程序,其名字以on开头,那么click事件对应的处理程序则是onclick

1.HTML事件处理程序

说明: 在html元素上面绑定事件可以以属性的方式进行绑定,不过属性的值必须是能够执行的JavaScript代码,那也就不能使用未经转义的html的字符,关于属性的值,可以直接将逻辑写在里面,也可以将逻辑封装成一个函数,再调用这个函数

<!-- 直接写逻辑 -->
<input type="button" value="Click Me" onclick="console.log('Clicked')"/>
<!-- 将逻辑进行封装 -->
<script>
  function showMessage() {
    console.log("Hello world!");
  }
</script>
<input type="button" value="Click Me" onclick="showMessage()" />

this:全局作用域下的具名函数,this指向window

2.DOM0事件处理程序

说明: 传统方式是把一个函数赋值给DOM元素的一个事件处理程序属性,当然,可以将事件处理程序属性设置为null,从而移除添加的处理程序

let btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
 console.log("Clicked"); 
}; 

this:这样所赋函数被视为元素的方法。因此,事件处理程序会在元素的作用域中运行,this指向触发该事件的元素

3.DOM2事件处理程序

说明: 主要通过addEventListener(事件名,处理函数,函数是否在捕获阶段调用)removeEventListener(事件名,处理函数,函数是否在捕获阶段调用)来完成,它们的第三个参数默认值是false,也就是默认在事件冒泡的阶段执行处理函数,对于removeEventListener想要移除添加的处理程序,需要传入与addEventListener相同的参数才可以,这也就是说添加的匿名函数无法移除

// 添加一个处理程序
let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
 console.log(this.id); 
}, false); 
// 添加多个处理程序(按执行顺序触发)
let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
 console.log(this.id); 
}, false); 

btn.addEventListener("click", () => { 
 console.log("Hello world!"); 
}, false); 
// 无法移除
let btn = document.getElementById("myBtn"); 
btn.addEventListener("click", () => { 
 console.log(this.id); 
 }, false); 

btn.removeEventListener("click", function() { 
 console.log(this.id); 
}, false); 
// 可以移除
let btn = document.getElementById("myBtn"); 
let handler = function() { 
 console.log(this.id); 
}; 

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

this:使用addEventListener添加事件,this指向事件左边的节点

4.IE事件处理程序

说明: 它也是使用的两个方法,attachEvent()detachEvent(),与上面的是类似的,只不过存在下面的区别:

  • IE的两个方法的第一个参数是需要加上on
  • IE的两个方法的函数中的this指向window
  • IE的方法如果添加多个事件其触发的顺序反向
  • IE的方法在处理事件的阶段都在事件冒泡阶段

5.兼容事件处理程序

说明: 这个可以选择JavaScript完成,也可以自己进行封装,必须下面这样简单实现

let EventUtil = { 
  /**
   * @description 添加事件
   * @param element 元素节点
   * @param type 事件类型
   * @param handler 事件处理函数
   */
  addHandler: function(element, type, handler) { 
    if (element.addEventListener) { 
      element.addEventListener(type, handler, false); 
    } else if (element.attachEvent) { 
      element.attachEvent("on" + type, handler); 
    } else { 
      element["on" + type] = handler; 
    } 
  }, 
  /**
   * @description 移除事件
   * @param element 元素节点
   * @param type 事件类型
   * @param handler 事件处理函数
   */
  removeHandler: function(element, type, handler) { 
    if (element.removeEventListener) { 
      element.removeEventListener(type, handler, false); 
    } else if (element.detachEvent) { 
    element.detachEvent("on" + type, handler); 
    } else { 
      element["on" + type] = null; 
    } 
  } 
 }; 

三、事件对象

说明: 在 DOM中发生事件时,所有相关信息都会被收集并存储在一个名为event的对象中,有时候在使用的时候将其简写为e,同时event对象是传给事件处理程序的唯一参数,并且不同的事件的事件对象存在不同的属性和方法,但是它们存在公共的(只读)

公共的属性和方法:

  • bubbles(布尔值):表示事件是否冒泡
  • cancelable(布尔值): 表示是否可以取消事件的默认行为
  • currentTarget(元素):当前事件处理程序所在的元素
  • defaultPrevented(布尔值):true 表示已经调用preventDefault()方法
  • detail(整数):事件相关的其他信息
  • eventPhase(整数):表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段
  • preventDefault()(函数):用于取消事件的默认行为。只有 cancelable 为 true 才可以调用这个方法
  • stopImmediatePropagation()(函数):用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序
  • stopPropagation()(函数):用于取消所有后续事件捕获或事件冒泡。只有 bubbles为 true 才可以调用这个方法
  • target(元素):事件目标,也就是需要操作的元素
  • trusted(布尔值):true 表示事件是由浏览器生成的。false 表示事件是开
    发者通过 JavaScript 创建的
  • type(字符串):被触发的事件类型
  • View(AbstractView):与事件相关的抽象视图。等于事件所发生的 window 对象

1.DOM事件对象

注意: event对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。

<input type="button" value="Click Me" onclick="console.log(event.type)"> 
let btn = document.getElementById("myBtn"); 
btn.onclick = function(event) { 
 console.log(event.type); // "click" 
}; 

btn.addEventListener("click", (event) => { 
 console.log(event.type); // "click" 
}, false);

如果事件处理函数直接挂载在操作的节点上面,那么thiscurrentTargettarget是相等的

<!-- 比如我操作按钮,然后给按钮绑定事件 -->
<button>点击</button>
<script>
  let btn = document.getElementsByTagName("button")[0];

  btn.addEventListener("click", function (e) {
    console.log(e.currentTarget === this); // true
    console.log(e.target === this); // true
  });
</script>

preventDefault()方法可以阻止默认行为

// 链接的默认行为就是在被单击时导航到 href 属性指定的 URL,
// 阻止后就不会跳转了
let link = document.getElementById("myLink"); 
link.onclick = function(event) { 
 event.preventDefault(); 
}; 

stopPropagation()方法一般用于阻止事件冒泡

let btn = document.getElementById("myBtn"); 
btn.onclick = function(event) { 
 console.log("Clicked"); 
 event.stopPropagation(); 
}; 

// 这个click事件并不会触发,因为冒泡的行为取消掉了
document.body.onclick = function(event) { 
 console.log("Body clicked"); 
}; 

2.IE事件对象

说明: IE的event对象会因为事件注册的方式不同而导致访问的形式不一样,如果 事件处理程序是使用DOM0方式指定的,则event对象只是window对象的一个属性,如果事件处理程序是使用attachEvent()指定的,则 event对象会作为唯一的参数传给处理函数,但其还是window的属性,如果是使用HTML属性方式指定的事件处理程序,则event对象同样可以通过变量event访问,当然它也会存在不同的事件其事件对象不一样,但是也存在相同的属性和方法

相同的属性和方法:

  • cancelBubble(布尔值):默认为 false,设置为 true 可以取消冒泡
  • returnValue(布尔值):默认为 true,设置为 false 可以取消事件默认行为
  • srcElement(元素):它是只读的,表示事件目标,这里可以用于替换this
  • type(字符串):它是只读的,表示触发的事件类型
var btn = document.getElementById("myBtn"); 
btn.onclick = function() { 
 let event = window.event; 
 console.log(event.type); // "click" 
};
var btn = document.getElementById("myBtn"); 
btn.attachEvent("onclick", function(event) { 
 console.log(event.type); // "click" 
});
<input type="button" value="Click Me" onclick="console.log(event.type)" />

3.兼容事件对象

说明: 虽然 DOM 和 IE 的事件对象并不相同,但它们有足够的相似性可以实现跨浏览器方案,所以可以简单处理下

var EventUtil = { 
  getEvent: function(event) { 
    return event ? event : window.event; 
  }, 

  getTarget: function(event) { 
    return event.target || event.srcElement; 
  }, 

  preventDefault: function(event) { 
    if (event.preventDefault) { 
    event.preventDefault(); 
    } else { 
    event.returnValue = false; 
    } 
  },

  stopPropagation: function(event) { 
    if (event.stopPropagation) { 
      event.stopPropagation(); 
    } else { 
      event.cancelBubble = true; 
    } 
  } 
}; 

四、事件类型

1.用户界面事件

说明: 它们是涉及与BOM交互的通用浏览器事件,主要有以下几个:

  • load:在 window 上当页面加载完成后触发,在窗套都加载完成后触发,在<img>元素上当图片加载完成后触发,在<object>元素上当相应对象加载完成后触发。
  • unload:在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在<object>元素上当相应对象卸载完成后触发。
  • abort:在<object>元素上当相应对象加载完成前被用户提前终止下载时触发。
  • error:在 window 上当 JavaScript 报错时触发,在<img>元素上当无法加载指定图片时触发,
    在<object>元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时
    触发。
  • select:在文本框(<input>或 textarea)上当用户选择了一个或多个字符时触发。
  • resize:在 window 或窗格上当窗口或窗格被缩放时触发。
  • scroll:当用户滚动包含滚动条的元素时在元素上触发。<body>元素包含已加载页面的滚动条。

(1)load

触发时刻:window上当页面加载完成后触发,在窗套(<frameset>)上当所有窗格(<frame>) 都加载完成后触发,在<img>元素上当图片加载完成后触发,在<object>元素上当相应对象加载完成后触发

// 使用js的方式添加load事件
window.addEventListener("load", (event) => {
    console.log(event);
});

JavaScript进阶---一文带你认识事件

根据规定,load事件应该在document而非window上触发。可是为了向后兼容,所有浏览器都在window上实现了load事件。

<!DOCTYPE html>
<html>
  <head>
    <title>向body元素添加onload属性</title>
  </head>
  <body onload="console.log(event)"></body>
</html>

JavaScript进阶---一文带你认识事件

由于html上没有window属性,那么任何在window上发生的事件,都可以通过给元素上对应的属性赋值来指定,这是一种向后兼容的策略,其次,如果html标签上面存在onload这个属性都可以这样添加事件,不过需要注意下<img>这个标签,当它被设置src属性的时候就会开始下载,<script>这个标签会在JavaScript文件加载完成触发load事件,如果需要下载文件,则同时需要src属性被设置以及<script>这个元素被添加到页面中去

(2)unload

触发时刻:window上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在<object>元素上当相应对象卸载完成后触发。

说明: 这个事件一般是在从一个页面导航到另一个页面时触发,最常用于清理引用,以避免内存泄漏

window.addEventListener("unload", (event) => { 
  console.log(event); 
});
<!DOCTYPE html>
<html>
  <body onunload="console.log(event)"></body>
</html>

无论使用何种方式,都要注意事件处理程序中的代码,避免报错

(3)resize

触发时刻: 当浏览器窗口被缩放到新高度或宽度

说明: 它会在window上面触发,那么添加事件也有两种方法(跟上面一样),不过优先使用JavaScript的方式添加,其次这个事件会随着用户缩放浏览器窗口不断触发,那也就应该避免在这个事件处理程序中执行过多计算。否则可能由于执行过于频繁而导致浏览器响应明显变慢。

window.addEventListener("resize", (event) => { 
  console.log("Resized"); 
});

(4)scroll

触发时刻: 用户滚动包含滚动条的元素时在元素在元素上面触发,通常指用户滚动页面

说明: 虽然scroll事件发生在window上,但实际上反映的是页面中相应元素的变化。

window.addEventListener('scroll', function() { 
    // 在这里编写处理scroll事件的代码 
});

2.焦点事件

  • focus:当元素获取焦点的时候触发,它被所有浏览器支持但是这个事件不冒泡
  • blur:当元素失去焦点的时候触发,它被所有浏览器支持但是这个事件不冒泡

3.鼠标和滚轮事件

说明: 其主要作用是定位设备,使用的事件如下:

  • click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。这主要是基于无障碍的考
    虑,让键盘和鼠标都可以触发 onclick 事件处理程序。
  • dblclick:在用户双击鼠标主键(通常是左键)时触发。
  • mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。
  • mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。
  • mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。
  • mousemove:在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。
  • mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元
    素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。
  • mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。
  • mouseup:在用户释放鼠标键时触发。这个事件不能通过键盘触发。

注意: click和dblclick之间触发的事件有:mousedown -> mouseup -> click -> mousedown -> mouseup -> click -> dblclick,这是一个过程,如果前面的事件少触发一个,那么后面的事件就不会触发

(1)客户端坐标

说明: 客户端坐标是事件发生时鼠标光标在客户端视口中的坐标,说简单点就是将当前元素距离左侧和上边的距离写成坐标(x,y)的形式,这个坐标就是客户端坐标(如下图),x值和y值可以通过event.clientXevent.clientY来获取

JavaScript进阶---一文带你认识事件

将页面中滚动条和顶部工具栏去掉,剩下的地方就是视口

JavaScript进阶---一文带你认识事件

注意,这两个值跟页面有没有滚动是没有关系的,比如当前元素的位置的event.clientX的值是100,我把滚动条下拉1000px的距离,event.clientX的值也还是100

(2)页面坐标

说明: 页面坐标是事件发生时鼠标光标在页面上的实际坐标,也就是说这个坐标的值会包含滚动条滚动的距离,页面坐标(x,y)分别使用(event.pageX,event.pageY)来获取,至于滚动条滚动的距离,使用scrollLeftscrollTop这两个属性获取,如果是混杂模式,属性存在document.body上,如果是标准模式,属性存在于document.documentElement

JavaScript进阶---一文带你认识事件

页面没有滚动:pageX = clientX、clientY = pageY
页面存在滚动:pageX / pageY = clientX + scrollLeft / scrolTop

(3)屏幕坐标

说明: 这个就是指当前元素在屏幕上实际的位置,那根据意思,这个坐标(x,y)的值与滚动条是否滚动就没有关系了,这两个值分别使用event.screenXevent.screenY来获取

JavaScript进阶---一文带你认识事件

(4)修饰键

说明: 修饰键存在四个:shiftctrlwindowalt,这四个按键分别对应event对象的四个属性shiftKeyctrlKeymetaKeyaltKey,它们的属性值都是布尔值,表示这个键是否被按下

(5)相关元素

说明: 对 mouseover和 mouseout 事件而言,还存在与事件相关的其他元素。这两个事件都涉及从一个 元素的边界之内把光标移到另一个元素的边界之内。对 mouseover 事件来说,事件的主要目标是获得 光标的元素,相关元素是失去光标的元素。类似地,对 mouseout 事件来说,事件的主要目标是失去光 标的元素,而相关元素是获得光标的元素,获取相关元素的信息可以使用event.target属性获取,这个属性只有事件的取值是这两个的时候才会存在值,其它时间都是null

(6)鼠标按键

说明: 对于mousedownmouseup两个事件,其返回的event对象中存在一个button属性,表示你按的是鼠标的哪一个键,0表示鼠标左键,1表示鼠标滚轮,2表示鼠标右键

(7)额外事件信息

说明: event对象上面存在一个detail属性,对于鼠标事件来说,它的值是一个数字,表示在同一个像素上面发生了多少次一次mousedown紧跟一次mouseup的行为,它的初始值是0,如果连续发生这样的行为,他它就会累加,如果鼠标在 mousedownmouseup之间移动了,则detail会重置为0

4.键盘与输入事件

说明: 键盘事件是用户操作键盘才触发的,它包含两个事件keydownkeyup,分别在按下触发(按住不松开会持续触发)松开触发

(1)键码

说明: 对于keydownkeyup事件,当按下一个键盘的按键,其event对象中的keyCode(已废弃)属性会保存对应按键的ASCII码,具体的值可以参考菜鸟教程

五、性能

说明: 在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。首先,每个函数都是对象,都占用内存空间,对象越多,性能越差。其次,为指定事件处理 程序所需访问 DOM 的次数会先期造成整个页面交互的延迟。

1.事件委托

说明: 这个的原理是使用的事件冒泡。它可以使用一个事件处理程序来处理一类事件,以此解决过多事件处理程序的问题,由于事件会冒泡到document上,那也就可以用一个click事件绑定页面的多个或者所有元素,看下面的例子:

<!-- 给每个li都绑定click事件 -->
<ul id="myLinks">
  <li id="goSomewhere">Go somewhere</li>
  <li id="doSomething">Do something</li>
  <li id="sayHi">Say hi</li>
</ul>
// 不用事件委托处理
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"); 
}); 
// 使用事件委托处理
// 这里只需要一个处理程序就可以,然后使用event的id属性进行区别处理
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对象随时可用,任何时候都可以给它添加事件处理程序,这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
  • 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也 可以节省时间。
  • 减少整个页面所需的内存,提升整体性能。

2.删除事件处理程序

说明: 把事件处理程序指定元素后,浏览器代码会与负责交互的代码产生联系,联系越多,性能越差,所以删除无用的事件处理程序也能提升性能,很多 Web 应用性能不佳都是由于无用的事件处理程序长驻内存导致的,导致的原因有两个,一个是删除带有事件处理程序的元素,虽然元素被删除,但是事件处理程序无法被垃圾回收,存放在内存中,久而久之就会影响性能,二是页面卸载,影响的原因也是一样

<div id="myDiv">
  <button id="myBtn">点击</button>
</div>
<script type="text/javascript">
  let btn = document.getElementById("myBtn");
  btn.onclick = function () {
    // btn.onclick = null; // 删除事件处理程序

    // 执行操作,虽然节点删除掉,但是事件处理程序依然存在
    document.getElementById("myDiv").innerHTML = "Processing...";
  };
</script>

在事件处理程序中删除节点也会阻止事件冒泡,只有目标节点仍然存在于文档中,事件冒泡才会继续

原文链接:https://juejin.cn/post/7344260655757131813 作者:NGC_2237

(0)
上一篇 2024年3月11日 上午10:56
下一篇 2024年3月11日 上午11:07

相关推荐

发表回复

登录后才能评论