前端面试 第二篇 js 事件

在这个行业摸爬滚打已有相当长的一段时间了。回想当初,我以一个全然无知的身份踏入了这个行业,与现在相比,似乎并未取得显著的成长,也未能在这一行中稳固自己的地位。我唯一的引路人是谷歌,每当遇到疑惑,我便向它求解。那些年,我的搜索记录充斥着htmlcssjs等基础知识,每一次在浏览器中探寻答案,都带给我无尽的快乐。那段时光,虽然我对一切都所知甚少,却过得极其愉快,可能是因为我当时的心态更为单纯,无忧无虑。时至今日,那段时光仍是我职业生涯中最快乐、最难忘的一页。然而,现在的我却鲜少能感受到那样的快乐了!

JavaScript实际上有许多值得深入了解的方面,逐渐挖掘其中的奥秘是非常有趣的! 让我们一起去发掘JavaScript的闪光点---js事件

概念问题

首先,先来了解几个概念:

  • 什么是js事件?

    • 概念:JavaScript事件是指在Web页面中发生的交互性动作或状态变化,如点击按钮、键盘输入、鼠标移动等(简单理解就是系统中可以发生的各种事件(浏览器行为,用户行为)
    • 常见的事件:
      • 鼠标事件:click,dblclick,mouseover,mouseout,mousedown,mouseup,mousemove,contextmenu等,
      • 键盘事件:keydown,keyup,keypress等,
      • 进度事件:abort,error,loadend,load,loadstart,timeout,progress等,
      • 窗口事件:resize,scroll等,
      • 触摸事件(移动端):touchstart,touchend,touchend等,
      • ...
  • 事件源(Event Source):事件的来源,即导致事件发生的对象或组件。

  • 事件对象(Event):在处理事件时自动创建的对象,它包含了与事件相关的信息和方法(事件对象是在事件发生时由浏览器创建的对象,包含有关事件的详细信息,如事件类型、目标元素、鼠标位置等)。

  • 事件目标元素(Event Target) :是一个接口,表示能够接收事件的目标对象(在DOM中,几乎所有的节点类型都实现了 EventTarget 接口,包括元素节点、文档节点和文档片段等)(事件最初发生的元素(最小节点))。

  • 事件处理程序(Event Handler):确定控件如何响应事件的事件过程(当对象(如按钮、文本框等)发生某个事件(如点击、输入等)时,会执行与此事件相应的事件处理程序)。

  • 举例(button点击)介绍这几个概念:

html 复制代码
   <div id="demo">
       <!-- button 可以理解为事件源 -->
      <button onclick="handleClick(event)" id="button">点我</button>
   </div>
js 复制代码
   const buttonDom = document.getElementById("button");
   // 理解为事件源
   const handleClick = (event) => { // event 就是事件对象 其中包含了很多信息
       console.log('event', event);
       console.log('event.target', event.target); // event.target 事件目标元素
   }

事件流(Event Flow)

概念理解和介绍为主,事件模型中会相结合帮助理解~

  • 概念:是当在Web页面中发生某个事件时,这个事件是如何在DOM树中传播和处理的。它主要包括三个阶段:捕获阶段(Capturing Phase),目标阶段(Target Phase)冒泡阶段(Bubbling Phase)。在捕获阶段,事件从最外层的祖先元素开始,向下传递到目标元素;而在冒泡阶段,事件则从目标元素开始,向上冒泡到最外层的祖先元素。这种传播机制使得在事件流的不同阶段都可以对事件进行处理(描述了事件从发生到被处理的过程)。

捕获阶段(事件捕获)(Capturing Phase)

  • 理解:也称为处于捕获阶段。 在捕获阶段,事件从最外层的元素逐级向内部元素传播,直到达到事件的目标元素。在这个阶段,事件处理程序可以在父元素上捕获事件,但尚未到达目标元素.
  • 作用: 可以在事件到达目标元素之前对事件进行处理或拦截(实际使用场景较少);
  • 图解

目标阶段(Target Phase)

  • 理解:也称为处于目标阶段。在这个阶段,事件已经达到了事件的目标元素,即触发事件的DOM元素。事件处理程序会在目标元素上执行,处理与事件相关的操作。
  • 作用:事件处理程序可以访问事件对象(Event Object),并对事件进行处理。
  • 图解

冒泡阶段(事件冒泡)(Bubbling Phase)

  • 理解:也称为冒泡阶段或处于冒泡阶段。在这个阶段,事件从目标元素开始向上冒泡至DOM树的根节点。换句话说,事件首先在目标元素上触发,然后逐级向上冒泡至父级元素,直至根节点。
  • 作用:冒泡机制可以实现事件的传播和委托处理,提高页面性能和交互效果。
  • 图解

事件流图解:

事件模型 (Event Model)

  • JavaScript事件模型是描述网页中事件处理的机制和流程的模型(js交互模型)。

DOM0 事件模型

概念:也被称为原始事件模型,是早期的DOM事件处理方式(DOM0事件模型是没有事件流概念的)。

特点

  • 一个DOM节点只能绑定一个事件处理器,如果再次为同一个事件绑定处理函数,那么之前绑定的事件处理函数会被覆盖;
  • 只支持冒泡阶段;

特点代码举例

  • 只能绑定一个事件处理器:
html 复制代码
   <div id="demo">
      <button id="button">点我</button>
   </div>
js 复制代码
   const buttonDom = document.getElementById("button");
   buttonDom.onclick = (event) => {
       console.log('first===>', event);
   }
   // 只会执行这个
   buttonDom.onclick = (event) => {
       console.log('last===>', event); // last===> PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, ...}
   }
  • 只支持冒泡阶段:
css 复制代码
 #grandfather {
        display: flex;
        align-items: center;
        justify-content: center;
        background: red;
        height: 200px;
        width: 200px;
    }
    #father {
        display: flex;
        align-items: center;
        justify-content: center;
        background: greenyellow;
        height: 100px;
        width: 100px;
    }
    #target {
        background: white;
        height: 50px;
        width: 50px;
        border: none;
    }
html 复制代码
<div id="demo">
    <div id="grandfather">
        <div id="father">
            <button id="target">点我</button>
        </div>
    </div>
</div>
js 复制代码
const grandfatherDom = document.getElementById("grandfather");
const fatherDom = document.getElementById("father");
const targetDom = document.getElementById("target");

grandfatherDom.onclick = () => {
    console.log('name', 'grandfather'); // 最后一个被执行
}
fatherDom.onclick = (event) => {
    console.log('name', 'father');  // 第二个执行
}
targetDom.onclick = (event) => {
    console.log('name', 'target'); // 第一个执行
}

捕获阶段是从顶端(最外层的元素逐级向内部元素传播),是grandfather=>father=>target顺序传播,上图打印输出的是顺序target=>father=>grandfather和冒泡阶段(从目标元素到最外层元素)一致

两个途径

  • 行内事件(内联模型) :直接在HTML元素的属性中指定事件处理程序。例如在按钮元素的onclick属性中指定JavaScript代码来处理点击事件。

  • 单次绑定:

    html 复制代码
       <div id="demo">
          <button onclick="handleClick(event)" id="button">点我</button> 
       </div>
    js 复制代码
       const handleClick = (event) => {
           onsole.log('event===> ', event); // event===> PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, ...}
       }
  • 多次绑定:

    html 复制代码
    <div id="demo">
       <button onclick="handleClick(event, 'first')" onclick="handleClick(event, 'last')"id="button">点我</button> 
    </div>
    js 复制代码
    const handleClick = (event, type) => {
        console.log('type===>', type); // type===> first
    }

注意:通过多次行内绑定on+事件发现,只执行了first(第一个绑定的事件),这是因为行内绑定on+事件(同一事件)只支持绑定一个,当第一个已经绑定上,后续的同类型(同一个)事件就无法绑定上了)

  • js绑定事件(脚本模型) :通过JavaScript获取页面元素,然后使用元素的onclick等事件属性来绑定事件处理函数。

  • 单次绑定:

    html 复制代码
       <div id="demo">
          <button id="button">点我</button>
       </div>
    js 复制代码
       const buttonDom = document.getElementById("button");
       buttonDom.onclick = (event) => {
           console.log('event===> ', event); // event===> PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, ...}
       }
  • 多次绑定:

    js 复制代码
       const buttonDom = document.getElementById("button");
       buttonDom.onclick = (event) => {
           console.log('first===>', event); // 并没有执行
       }
       // 只会执行这个
       buttonDom.onclick = (event) => {
           console.log('last===>', event); // last===> PointerEvent {isTrusted: true, pointerId: 1, width: 1, height: 1, pressure: 0, ...}
       }

通过多次js绑定事发现,只执行了last(最后一个绑定的事件),是由浏览器实现的机制决定的;具有覆盖性,后者会覆盖前者

DOM2 事件模型

概念 :是用于处理Web页面中事件的一种标准化模型(实现事件处理;引入了事件流的概念)。

其提供了关键的三个方法来处理和管理事件:

addEventListener

addEventListener:向指定对象添加事件监听器;

  • 参数:
    1. type :表示要监听的事件类型,比如'click''mouseover'等。

    2. listener :表示要触发的事件处理函数,也可以是一个实现了EventListener接口的对象,比如一个实现了handleEvent方法的对象。

    3. options(可选):一个包含有关事件监听的配置信息的可选对象。常见的配置选项包括:

      • capture:一个布尔值,指定事件是否在捕获阶段进行处理,默认为false
      • once:一个布尔值,指定事件是否只能被触发一次,默认为false
      • passive:一个布尔值,指定事件处理程序是否不会调用preventDefault()方法,默认为false
  • 特点:
    • 多次绑定 :允许为同一个事件类型的对象添加多个事件监听器,而不会覆盖之前的监听器;
    • 更灵活的控制 :提供了更灵活的控制事件监听器的添加和移除,可以指定是否在捕获阶段冒泡阶段触发事件;
    • 事件委托 : 通过事件委托的方式,可以在父元素上监听子元素的事件减少事件处理函数的数量提高性能;
    • 符合现在标准 : 现代推荐的事件绑定方式,相比于传统的事件处理方式(如 element.onclick),具有更好的兼容性可维护性;
    • 支持不同类型的事件: 可以为不同类型的事件(如鼠标事件、键盘事件、表单事件等)添加监听器,实现更丰富的交互功能;

addEventListener

  • 多次绑定

    html 复制代码
       <div id="demo">
           <button id="button">点我</button>
           <div id="two">1</div>
       </div>
    js 复制代码
       //多次绑定
       const buttonDom = document.getElementById("button");
       const numDom = document.getElementById("two");
       buttonDom.addEventListener('click', (event) => {
           numDom.innerText = Number(numDom.innerText) + 1;
           console.log('first', new Date());
       })
       buttonDom.addEventListener('click', (event) => {
           numDom.innerText = Number(numDom.innerText) + 1;
           console.log('last', new Date());
       })


以上代码可以看出 addEventListener 多次绑定click事件都可以绑定上并且按照顺序执行了;

  • 更灵活的控制(控制是在捕获阶段还是冒泡阶段)

    css 复制代码
    #parent {
        display: flex;
        align-items: center;
        justify-content: center;
        background: red;
        height: 200px;
        width: 200px;
    }
    
    #target {
        background: white;
        height: 50px;
        width: 50px;
        border: none;
    }
    html 复制代码
    <div id="demo">
        <div id="parent">
            <button  id="target">点我</button> 
        </div>
    </div>
    • 控制在冒泡阶段(capture:true)
    js 复制代码
    const parentDom = document.getElementById("parent");
    const targetDom = document.getElementById("target");
    const handleClick = (type) =>{
        console.log('type===>', type)
    }
    parentDom.addEventListener('click', () => handleClick('parent'), {capture: true})
    // DOM0中无事件流的概念(可以认定为在冒泡阶段)
    targetDom.onclick = () => {
        handleClick('target')
    }

捕获阶段就是从外出元素到目标元素;parent就在target的外层,所以捕获阶段parent绑定的方法先执行,然后再执行taregt绑定的方法

  • 控制在捕获阶段(capture:false)
js 复制代码
     const parentDom = document.getElementById("parent");
     const targetDom = document.getElementById("target");
     const handleClick = (type) =>{
         console.log('type===>', type)
     }
     parentDom.addEventListener('click', () => handleClick('parent'), {capture: false})
     // DOM0中无事件流的概念(可以认定为在冒泡阶段)
     targetDom.onclick = () => {
         handleClick('target')
     }

冒泡阶段就是从目标元素到最外层;target就是目标元素,所以冒泡阶段taregt绑定的方法先执行,然后再执行parent绑定的方法

  • 事件委托(事件代理)

    html 复制代码
    <div id="demo">
        <ul id="parent">
            <li>第一个</li>
            <li>第二个</li>
            <li>第三个</li>
            <li>第四个</li>
            <li>最后一个</li>
        </ul>
    </div>
    js 复制代码
    //事件委托
    const parentElement = document.getElementById("parent");
    parentElement.addEventListener('click', (event) => {
        console.log('我是parent的方法', event);
    })

当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发

如果想要把ul作为目标元素的话怎么做呢? 那是不是阻止事件流继续向下捕获就可以了?

stopPropagation:阻止事件流过程中事件继续向下捕获

js 复制代码
const parentElement = document.getElementById("parent");
const firstElement = document.getElementById("first");
parentElement.addEventListener('click', (event) => {
    console.log('我是parent的方法', event);
    event.stopPropagation();
    return false;
},{capture: true}) // capture : true  捕获阶段
firstElement.addEventListener('click', (event) => {
    console.log('我是first的方法', event);
})

当li被点击时,由于先进行事件捕获阶段,从ul => li捕获过程,事件目标提供了stopPropagation停止捕获继续向下找寻的方法,阻止了事件捕获

removeEventListener

removeEventListener :从指定对象中移除事件监听器(方法用于移除之前使用 addEventListener 方法添加的事件监听器)

语法 :此处不多叙说, 请参考EventTarget.removeEventListener

js 复制代码
target.removeEventListener(type, listener); // 此处不多叙说,参考MDN文档 

简单)举例:

html 复制代码
  <div id="demo">
    <button id="button">点我</button>
 </div>
js 复制代码
    const buttonDom = document.getElementById("button");
    const handleClick = (event) => {
        console.log(event);
        setTimeout(()=> {
            buttonDom.removeEventListener('click',  handleClick)
            console.log('已执行移除事件程序!');
        },1000)
    }
    buttonDom.addEventListener('click', handleClick);

需要传输参数怎么办?

试一试

js 复制代码
   const buttonDom = document.getElementById("button");
    const handleClick = (event, type) => {
        console.log(event,type);
        setTimeout(()=> {
            buttonDom.removeEventListener('click' ,handleClick)
            console.log('已执行移除事件程序!');
        },1000)
    }
    buttonDom.addEventListener('click', (event) => handleClick(event, 'add'));

以上代码会发现问题:removeEventListener 并没有移除handleClick事件执行程序(多次点击,日志有输出(buttonDom绑定的监听器还在))

这是因为:JavaScript 中的函数是对象,每次定义函数(特别是匿名函数)时,都会创建一个新的函数对象。因此,如果你使用匿名函数作为事件处理函数,无法使用 removeEventListener 来移除它,因为每次定义匿名函数时,都会得到一个全新的引用

想要添加参数或者匿名函数执行程序怎么办?

js 复制代码
const buttonDom = document.getElementById("button");
// AbortSignal 接口表示一个信号对象(signal object),它允许你通过AbortController`对象与 DOM 请求(如 Fetch)进行通信并在需要时将其中止
const abortController = new AbortController();
const abortSignal = abortController.signal;

// 添加事件监听器
buttonDom.addEventListener('click', (e) => {
    console.log('click:我被点名了!');
    setTimeout(() => {
        // 移除事件监听器
        abortController.abort();
        console.log('继续click 观察还在吗?');
    },3000)
}, { signal: abortSignal });

AbortSignal需要参考:AbortSigna 解决addEventListener绑定匿名函数 | 传输参数 removeEventListener 无法移除监听器问题;

dispatchEvent

概念 : 用于在指定的元素上触发一个事件(可以模拟用户行为或触发自定义事件

举例:模拟点击实现自动登录(可以自己写个脚本去某些网站自动签到:比如掘金签到(需要了解一下谷歌插件开发))

html 复制代码
<div class="app">
        <section class="login">
            <form id="form">
                <input type="account " name="account " id="account" name="account" placeholder="请输入账号"/>
                <input type="password" name="password" id="password"  placeholder="密码"/>
            </form>
            <button id="submit" class="submit" onclick="submit()" >提交</button>
        </section>
    </div>
js 复制代码
// 假定账号密码
    const  defaultCccount = '123456';
    const  defaultPassword = '123456';
    const submit = () => {
        const form = document.getElementById('form');
        const account  = form.elements['account'].value;
        const password = form.elements['password'].value;
        if(!account || !account.length) {
            alert('请输入账号!')
            return 
        }
        if(!password || !password.length) {
            alert('请输入密码!')
            return 
        }
        if(defaultCccount === account && defaultPassword === password) {
            alert('登录成功!')
        } else {
            alert('账号或密码错误!')
        }
    }
js 复制代码
 // 浏览器控制台
 window.account.value = '123456';
 window.password.value = '123456';
 window.submit.dispatchEvent(clickEvent);

IE 事件模型

概念:IE事件模型是Internet Explorer浏览器特有的事件处理机制(事件流只有两个阶段:目标阶段和冒泡阶段;在冒泡阶段,事件会从目标元素开始,然后向上冒泡至父元素,直至达到根元素)

事件模型
类型 DOM0事件模型 DOM2事件模型 IE事件模型
特点 是最早的事件处理机制,直接在HTML元素上定义事件处理函数,如通过onclickonload等属性来绑定事件 是W3C标准模型,更强大和灵活的事件处理机制; * 它使用addEventListener方法来绑定事件处理函数; * 支持事件传播(包括捕获阶段、目标阶段和冒泡阶段) 是Internet Explorer浏览器特有的事件处理机制,使用attachEventdetachEvent方法进行事件的绑定和解除。
优点 简单直观,易于理解和使用,所有浏览器都支持 * 支持为同一事件类型绑定多个处理函数; * 允许在捕获或冒泡阶段处理事件; * 提供了取消事件默认行为和阻止事件传播的机制 在IE浏览器中直接使用,无需额外的兼容性处理
缺点 * 不支持事件传播(没有捕获和冒泡阶段); * 无法为同一事件类型绑定多个处理函数,无法取消事件的默认行为或阻止事件传播。 在一些老版本的浏览器(如IE6-8)中可能不被支持 * 不支持标准的事件传播模型(没有捕获阶段); * 不支持为同一事件类型绑定多个处理函数; * 事件处理函数的this指向window而不是触发事件的元素,需要通过全局变量event来访问事件对象; * IE事件模型的应用范围逐渐缩小(逐渐退出舞台)

提出一个疑问(问题?)

  • 一个button 即HTML行内模式绑定onclick 同时通过脚本onClick绑定事件, 又通过 addEventListener 绑定click事件后,哪一个先执行? 为什么?

    html 复制代码
      <div id="demo">
       <button onclick="handleClick(event)" id="button">点我</button>
      </div>
    js 复制代码
    const buttonDom = document.getElementById("button");
    const handleClick = (event) => { 
        console.log('我是行内', event.target);  // 不会执行
    }
    
    buttonDom.addEventListener('click', (event) => {
        console.log('我是监听器', event.target); // 最后执行
    });
    buttonDom.onclick = (event) => {
        console.log('我是js脚本', event.target);  // 第一个执行
    }
  • 以上可以看出 第一个执行的是 js脚本绑定的时间程序,其次是addEventListener绑定的时间程序执行,行内的没有执行;

  • 行内事件处理器(比如直接在HTML元素上设置的onclick属性)通常具有比通过addEventListener添加的事件监听器更高的优先级,这是因为行内处理器是元素属性的一部分,它们在DOM解析过程中就被绑定到元素【随后通过buttonDom.onclick(这实际上也是设置行内处理器的一种方式)覆盖了原来的行内处理器handleClick,但由于浏览器处理行内处理器的特殊机制,原本通过onclick属性设置的行内处理器(尽管被覆盖了)仍然会首先执行】

  • 行内绑定的事件被js脚本绑定的事件覆盖了;当一个元素同时使用行内模式绑定onclick事件和通过脚本绑定onClick事件时,脚本绑定的事件会覆盖行内模式绑定的事件。这是因为脚本绑定的事件会在页面加载后动态地替换行内模式绑定的事件,从而覆盖了之前的事件处理函数

  • 通过addEventListener绑定的click事件会在捕获或冒泡阶段被执行,取决于addEventListener的第三个参数(默认是在冒泡阶段执行)

onclick 与 addEventListener html <div id="demo"> <button id="button">点我</button> </div>

csharp 复制代码
```js
const buttonDom = document.getElementById("button");

buttonDom.addEventListener('click', (event) => {
    console.log('我是监听器', event.target); // 第一个执行
});
buttonDom.onclick = (event) => {
    console.log('我是js脚本', event.target);  //最后一个执行
}
```

onclick(冒泡阶段执行) 与 addEventListener 执行顺序与绑定顺序和 addEventListener第三个参数(执行阶段有关)

-----------------------------------------------END----------------------------------------------------

个人总结以及理解,有误欢迎大佬指正;

相关推荐
辻戋43 分钟前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun2 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp2 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.3 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl5 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理8 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻8 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js