面试题常考

DOM事件模型

DOM事件机制主要有2个阶段,分别是:捕获阶段和冒泡阶段

事件

  • 事件

是用户或者浏览器自己执行的某种动作,是文档或者浏览器发生的一些交互瞬间,比如点击(click)按钮等,这里的click就是事件的名称。JS与html之间的交互是通过事件实现的,DOM支持大量的事件。

  • DOM事件
    1. event.target.nodeName   //获取事件触发元素标签名(li,p,div,img,button…)
    2. event.target.id      //获取事件触发元素id
    3. event.target.className  //获取事件触发元素classname
    4. event.target.innerHTML  //获取事件触发元素的内容(li)

事件流

事件流是一个事件沿着特定数据结构传播的过程。冒泡捕获是事件流在DOM中两种不同的传播方法

事件捕获&事件冒泡

当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。

DOM是树形的,当我们在页面上单击一个按钮时,先发生事件捕获,也就是从不具体的节点到最具体的节点,一般是从document对象开始从外网内传播,然后再事件冒泡,也就是事件是由最具体的元素接收,然后逐级向上传播,在每一级的节点上都会发生,直到传播到document对象

  • 从外向内找监听函数,叫事件捕获
  • 从内向外找监听函数,叫事件冒泡
  • 注意:两个阶段都会走一遍,且先捕获后冒泡

image.png

当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件
fn 分别在捕获阶段和冒泡阶段监听click事件

addEventListener(事件绑定API)

捕获和冒泡两个阶段都会走一遍,但具体执行取决于事件绑定API的参数设定

addEventListener(‘click’,fn,bool)

  • 如果第三个参数bool 不传,或者传false, 那么我们会在冒泡阶段调用fn
  • 如果第三个参数Bool传值为true, 那么我们会在捕获阶段调用fn

target 和 currentTarget的区别

可能相同,也可能不同

  • e.target 用户在操作的元素
  • e.currentTarget 程序员监听的元素
    • 事件 前的对象 即为监听的对象
      • level1.addEventListener('click',(e) => {console.log('npc')}) level1为监听的元素
  • this是 e.currentTarget ,监听代码里不推荐使用它
  • target在事件流的目标阶段;currentTarget在事件流的捕获,目标及冒泡阶段。只有当事件流处在目标阶段的时候,两个的指向才是一样的,而当处于捕获和冒泡阶段的时候,target指向被单击的对象而currentTarget指向当前事件活动的对象(一般为父级)。
    1. <div>
    2. <span>文字</span>
    3. </div>
    假设我们监听的是div, 但用户实际点击的是文字,那么
    e.target就是span标签
    e.currentTarget就是div标签
    示例:
    image.png

无捕获和冒泡阶段的特例

当监听的元素就是用户操作的元素,且没有父子的包含关系,那么谁先监听谁就先执行

阻止冒泡

  • 捕获不能取消,但是冒泡有时候可以

stopPropagation()可以中断冒泡,浏览器不再往上走
一般用于封装独立组件

  • 有些特定冒泡不可取消

如 scroll event(滚动事件)
具体可以在MDN上查询

  • Bubbles 该事件是否冒泡
  • Cancelable 开发者是否可以取消冒泡

    自定义事件

    CustomEvent( type,detail ) 创建一个自定义事件
    type
    事件的类型名称。
    detail
    当事件初始化时传递的数据。 ```javascript // 添加一个适当的事件监听器 obj.addEventListener(“cat”, function(e) { process(e.detail) })

// 创建并分发事件 var event = new CustomEvent(“cat”, {“detail”:{“hazcheeseburger”:true}}) obj.dispatchEvent(event)

  1. <a name="v4tKG"></a>
  2. # DOM事件委托
  3. <a name="IpC7R"></a>
  4. ## 事件委托原理机制
  5. 简而言之:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件
  6. 由于冒泡阶段,浏览器从用户点击的内容从下往上遍历至 window,逐个触发事件处理函数,<br />因此**可以监听一个祖先节点(例如爸爸节点、爷爷节点)来同时处理多个子节点的事件**
  7. 举例:<br />给最外面的节点添加事件,那么里面的子节点在做事件的时候,都会冒泡到最外层的父节点上,所以都会触发,这就是事件委托,委托它们父级 代为执行事件
  8. <a name="jftw1"></a>
  9. ## 应用场景
  10. 出现多个嵌套的深层元素,并且需要在最深层处触发事件时,将相同类型的事件绑定在父级元素上进行统一管理<br />eg.<br />点击表格中的各个单元格,触发对应事件,若表格单元格过多,需要绑定大量事件,使用事件委托给单元格的上级table标签,只在table标签处绑定事件,在事件声明中使用event.target输出真正点击到的单元格
  11. <a name="fcni2"></a>
  12. ## 获取触发事件的元素的值
  13. 根据元素的类型使用:<br />input类型:event.target.value<br />其他类型:event.target.innerHTML
  14. <a name="ycKvL"></a>
  15. ## 事件委托的优势
  16. - 节省监听器(节省内存),提升js性能,减少DOM压力
  17. 监听父级节点,等冒泡的时候,判断event.target是不是目标子节点的一个
  18. - 动态监听(可监听目前不存在的节点)
  19. 监听父级节点,等到冒泡时,判断target是不是想要监听的元素
  20. <a name="RcBC8"></a>
  21. ## 封装事件委托函数
  22. 需求:监听所有的li标签,如果用户点击li标签,就console.log('用户点击了Li标签')
  23. ```javascript
  24. <ul id="test">
  25. <li>1</li>
  26. <li>2</li>
  27. <li>3</li>
  28. <li>4</li>
  29. </ul>

那么实现的JS代码就是:

  1. // 监听父元素 ul#test
  2. test.addEventListener('click', (e)=> {
  3. //通过浏览器传进来的e参数,找到当前点击元素
  4. const t = e.target
  5. // 判断当前元素是不是Li标签
  6. if(t.matches('li') {
  7. console.log('用户点击了li')
  8. }
  9. })
  1. 首先监听父元素,
  2. 然后根据浏览器传进去的事件信息,拿到当前点击元素,
  3. 再判断当前点击元素是不是li元素, 如果是,就console.log(‘用户点击Li标签’)

基于此,我们可以封装一个事件委托函数

  1. on('click', '#test', 'li', ()=>{
  2. console.log('用户点击了li')
  3. })
  4. function on(eventType, parentElement, selector, fn) {
  5. // 先判断是不是element,
  6. //如果传进来的是选择器,不是element本身,就先变成element,
  7. // 因为只有element才能监听事件
  8. if (!(parentElement instanceof Element)) {
  9. parentElement = parentElement.querySelectorAll(parentElement)
  10. }
  11. parentElement.addEventListener(eventType, (e)=>{
  12. let target = e.target
  13. if (target.matches(selector)) {
  14. fn(e)
  15. }
  16. })
  17. }