JavaScript核心概念:事件

122 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

浏览器页面构建过程

首先我们要搞清楚,当我们在浏览器地址栏中输入:www.baidu.com 后点击Enter,浏览器发生了什么?下图很直观的描述了整个过程。 截屏2022-06-16 18.23.12.png

页面构建阶段

在页面构建阶段,当遇到包裹JS代码的脚本元素,浏览器就会停止从HTML构建DOM,并开始执行JS代码。JS引擎执行到脚本元素中最后一行,浏览器就退出JS执行模式,并继续DOM节点的构建,如果在此期间又遇到了JS脚本元素,则DOM渲染在此暂停,并执行JS代码。只要还有没处理完的HTML元素和没执行完的JavaScript代码,下面两个步骤都会一直交替执行:

  1. 解析HTML代码并构建DOM树
  2. 执行JS代码

所有包含在脚本元素中的JS都由浏览器的JS引擎执行,比如Chrome的V8引擎。浏览器暴露给JS引擎全局对象window,然后两者通过它来交互并改变页面内容。

页面构建阶段结束之后进入事件处理阶段

事件处理

  • 页面构建阶段执行的JS代码,不仅会影响全局应用状态和修改DOM,还会注册事件监听器(或处理器)。这类监听器会在事件发生时,由浏览器调用执行。有了这些事件处理器,我们的应用也就有了交互能力。
  • 由于JS是在一个单线程执行的特性,在同一时刻,只能处理多个不同事件中的一个,处理顺序是事件生成的顺序。
  • 事件处理阶段大量依赖事件队列,所有的事件都以其出现的顺序存储在事件队列中。事件循环会检查事件队列的队头,如果检测到了一个事件,那么相应的事件处理器就会被调用。

事件循环

事件循环不仅在前端环境中用到,在后端的NodeJS中也在使用。 任务分为宏任务和微任务 事件循环的实现至少应该含有一个用于宏任务的队列和至少一个用于微任务的队列。

宏任务

宏任务的例子很多,大致包括

  • 创建主文档对象
  • 解析HTML
  • 执行主线(或全局)JavaScript代码
  • 更改当前URL
  • 各种事件:如页面加载、输入、网络事件和定时器事件、onclick等事件

从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。

微任务

而微任务是更小的任务。 微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行, 浏览器任务包括重新渲染页面的UI

微任务的案例包括:

  • Promise回调函数
  • DOM发生变化等。

微任务三大特点:

  • 尽可能快地执行
  • 通过异步方式执行
  • 不能产生全新的微任务

微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。

同时包含宏任务和微任务的例子

onclick事件绑定的function中包含Promise代码。

其中整个onclick事件就是宏任务,Promise中的回调函数中包含的就是微任务。

使用计时器处理复杂任务

定时器主要有如下方法

启动一个计时器,在指定的延迟时间结束时执 行一次回调函数,返回标识计时器的唯一值

id = setTimeout(fn,delay)

当指定的计时器尚未触发时,取消(消除)计时器

clearTimeout(id)

启动一个计时器,按照指定的延迟间隔不断执行回调函数,直至取消。返回标识计时器的唯一值

id = setInterval(fn,delay)

取消(消除)指定的计时器

clearInterval(id)

由于任务开始之后无法被其他任务打断,并且每个任务都有其执行事件,所以不能保证定时器的准确性。

防抖

函数防抖动(debounce)

  • 防止在短时间内过于频繁的执行相同的任务
  • 通过 clearTimeout 销毁上一次产生的定时器(回调也就销毁了)
  • 多次事件响应只会执行最后一次

场景 比如用户快速点击同一个按钮时可以用防抖来防止多次响应用户点击事件。

截流

函数节流(throttle) 由于某个事件导致某个函数被不停的调用

在一段连续操作中,每一段时间只执行一次

使用计时器管理动画

使用事件冒泡和委派

一个事件的处理的两种方式

1.捕获——首先被顶部元素捕获,并依次向下传递。

2.冒泡——目标元素捕获之后,事件处理转向冒泡,从目标元素向顶部元素冒泡。

通过捕获,事件最终传递到目标元素。通过冒泡,事件从目标元素向上冒泡,具体逻辑如下图所示: 截屏2022-06-16 21.55.01.png


const outerContainer = document.getElementById("outerContainer");
const innerContainer = document.getElementById("innerContainer");

//addEventListener没有指定第三个参数,启用默认的冒泡模式
document.addEventListener("click", () => {
    report("Document click");
});  

//第三个参数传入ture,则启用捕获模式
outerContainer.addEventListener("click", () => {
    report("Outer container click");
}, true);

//传入false,启用冒泡模式
innerContainer.addEventListener("click", () => {
    report("Inner container click");
}, false); 

事件冒泡委托常见用途

我们常用的列表结构中包含很多item,我们不需要在每个item上都注册一个事件监听,而只需要在item的外层元素注册一个元素,可以将点击的事件由单元格的上层元素 通过冒泡可以处理所有的单元格单击事件。我们知道单元格是表格的后代元素,通过event.target即可获得被单击的元素。将事件处理器代理到表格上优雅得多

使用自定义事件

浏览器自带了很多事件如 onclick、onload、onmousedown,但是有时候并不能完全满足我们的需求。比如我们在请求Ajax时,我们想在请求开始时展示loading动画,请求结束时隐藏loading动画,我们就可以使用CustomEvent构造函数构造一个自定义事件,并指定派发的事件。具体过程如下代码:


<style>

#loading-view {display: none; }

</style>
//单击按钮,模拟Ajax请求
<button type="button" id="clickMe">Start</button>  
//使用旋转的图片表示正在加载
<img id="loading-view" src="whirly-thing.gif" /> 
<script>

function triggerEvent(target, eventType, eventDetail) {
    // 使用CustomEvent构造器创建一个新事件
    const event = new CustomEvent(eventType, {
        // 通过detail属性为事件对象传入信息
        detail: eventDetail
    });
    //使用内置的dispatchEvent方法向指定的元素派发事件
    target.dispatchEvent(event);
}

function performAjaxOperation() {

    triggerEvent(document, 'ajax-start', {url: 'my-url'});
    setTimeout(() => {
        //使用延迟计时器模拟Ajax请求。开始执行时,触发ajax-start事件,一段时间过去之后,
        //激活ajax-complete事件。传入URL作为事件额外信息
        triggerEvent(document, 'ajax-complete');
    }, 5000);
}

const button = document.getElementById('clickMe');
button.addEventListener('click', () => {
    //当单击一个按钮时,Ajax操作开始
    performAjaxOperation(); 
});
显示旋转图片,处理
document.addEventListener('ajax-start', e => { 
    //ajax-start 事件
    document.getElementById('loading-view').style.display = 'inline-block';
});

document.addEventListener('ajax-complete', e => { 
    //处理ajax-complete事件,隐藏旋转图片
    document.getElementById('loading-view').style.display = 'none';
 });
</script>