深度解析JS事件驱动模型:如何理解浏览器中的异步回调和事件循环

日常刷JS面试题,难免会遇到这么一道:请简述JS的事件驱动模型。各位小伙伴是否也曾经有过这样的经历,对着这道题,一脸懵逼,一脸蒙圈。今天,掌门人就来给大家普及一下 JS 事件驱动模型的相关知识。

JS异步编程的背景:从回调地狱到Promise和async/await

在了解事件驱动模型之前,我们先回顾一下JS异步编程的历史。

记得刚开始写JS的时候,还没有现在流行的Promise和async/await,那时候我们可能还在回调地狱里挣扎。比如,要实现服务端的请求数据一般需要这样写:

getDataFromServer(params, function(err, result1){
  if (!err) {
    getNextResult(result1, function(err, result2) {
      if (!err) {
        getFinalResult(result2, function(err, result3) {
          if (!err) {
            // Do something with the result
          } else {
            console.log(err);
          }
        }
      } else {
        console.log(err);
      }
    });
  } else {
      console.log(err);
  }
});

这样的代码对于我们来说,一股绝望,如果嵌套多了,直接影响开发者的心情。但随着时间的推移,Promise和async/await逐渐逐渐变得越来越流行,如今已成为JS异步编程的标配。

用Promise和async/await实现上面代码的异步请求,大家一定更为熟悉,更为好看:

async function getDataFromServer(params) {
  try {
    const result1 = await fetch(url1);
    const result2 = await fetch(url2 + result1.id);
    const finalResult = await fetch(url3 + result2.id);
    // Do something with finalResult 
  } catch (err) {
    console.log(err);
  }
}

异步编程变得更加方便了,但是背后却有什么本质的变化吗?实质上,JS的异步编程背后采用了事件驱动模型。

什么是JS事件驱动模型:理解事件循环、宏任务与微任务

事件驱动模型是什么?简单说来,是当特定事件发生时,某个函数或代码块会被触发和执行的一种编程方式。

JS事件驱动模型的核心是事件循环。所谓的事件循环就是一个不断循环的过程,JS引擎会检查宏队列和微队列里的任务有没有被处理完,如果宏队列和微队列均为空,则JS引擎会一直休眠,等待宏队列和微队列中被新加入的任务唤醒。其中, 宏任务存放在宏任务队列中,常见的宏任务有setTimeout、setInterval、setImmediate和I/O操作等。而微任务则处于微任务队列中,常见的微任务有Promise.then、process.nextTick和object.observe,主要用于对象数据的监听。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

运行上述代码,你会发现输出顺序是这样的:

script start
script end
promise1
promise2
setTimeout

我们来看一下这段代码的执行过程:

  • 执行第一行代码,输出 “script start”。
  • 设置一个0毫秒后执行的定时器,这个定时器是一个宏任务,被放到宏任务队列中。
  • 解析Promise.resolve()语句,并将其添加到微任务队列中。因为微任务队列是在当前任务完成后立即执行的,所以接下来会立即执行微任务。
  • 执行第7行代码,输出 “script end”。
  • 当前任务同步代码执行完成,开始执行微任务。
  • 执行第10行代码,输出 “promise1″,继续执行第11行代码,输出 “promise2″。
  • 微任务队列为空,开始执行宏任务队列中的任务。
  • 执行定时器回调函数,输出 “setTimeout”。

可以看到,微任务的执行优先于宏任务,而不同类型的任务又是按照队列的顺序依次执行的。了解这些背后的机制,会帮助我们更好地理解事件驱动模型在JS中的应用。

浏览器中的事件机制:从事件触发到事件处理的整个流程

在浏览器中,事件是如何被触发和处理的呢?下面是整个流程的简述:

  1. 用户在浏览器中触发一个事件,比如点击鼠标、按下键盘等。

  2. 事件被封装成Event对象。

  3. Event对象被放入宏任务队列中,等待JS引擎执行。

  4. 当宏任务队列中的事件被执行时,JS引擎会把这个Event对象拿出来,并调用该事件对应的监听器或回调函数。

  5. 监听器或回调函数执行完成后,如果有相关的异步请求,JS引擎会把它们放入微任务队列中,等待当前宏任务执行完成后执行。

document.addEventListener('click', function(event) {
  console.log('You clicked the button');
});

在上述代码中,当用户点击页面中的某个元素时,就会触发click事件并被封装为Event对象,然后被添加到宏任务队列中等待执行。当JS引擎执行到这个宏任务时,会调用监听器函数并输出 “You clicked the button”。

常见事件类型的处理方式:鼠标/键盘事件、网络请求、DOM操作等

在浏览器中,事件类型很多,处理方式也各不相同。常见的事件类型包括鼠标/键盘事件、页面加载事件、网络请求事件、DOM操作等。下面是一些常见事件类型和处理方式的示例。

鼠标/键盘事件

document.addEventListener('mousemove', function(event) {
  console.log('Mouse position:', event.clientX, event.clientY);
});

document.addEventListener('keydown', function(event) {
  console.log('Key pressed:', event.keyCode);
});

当用户移动鼠标时,mousemove事件会被触发并产生一个Event对象,其中clientX和clientY属性表示鼠标的坐标位置。当用户按下键盘时,keydown事件也会产生一个Event对象,其中keyCode表示按下的键位。

网络请求事件

const req = new XMLHttpRequest();
req.open('GET', 'https://example.com/data');
req.addEventListener('load', function(event) {
  console.log('Data loaded:', req.responseText);
});
req.send();

使用XMLHttpRequest发送HTTP请求,当服务器返回响应时,load事件将被触发并产生一个Event对象,其中可以通过responseText属性获取服务器响应的数据。

DOM操作

const btn = document.getElementById('my-button');
btn.addEventListener('click', function(event) {
  event.preventDefault();
  console.log('Button clicked');
  // do something else
});

当用户点击按钮时,click事件会被触发并产生一个Event对象,其中preventDefault方法可以阻止默认行为,比如链接跳转等。

JS事件驱动模型的优化和性能问题:如何避免长任务影响用户体验?

在JS事件驱动模型的应用中,有一些常见的问题需要注意。其中一个重要的问题就是长任务的执行可能会导致UI线程阻塞,造成用户体验的不良影响。为了避免这种情况,我们需要优化代码,让长任务异步执行,并通过一些非阻塞的方式来处理。

代码示例:

const btn = document.getElementById('my-button');
btn.addEventListener('click', function(event) {
  console.log('Button clicked');
  setTimeout(function() {
    console.log('Long task started');
    // do something that takes a long time
    console.log('Long task finished');
  }, 0);
});

在上述代码中,当用户点击按钮时,会产生一个宏任务,其中的长任务被封装在一个定时器函数中并异步执行,从而不会阻塞UI线程的执行。

其他常见问题和应用场景:事件委托、事件冒泡、代码调试等

除了上述问题外,JS事件驱动模型还涉及到诸如事件委托、事件冒泡、代码调试等问题。下面我们来简要介绍这些问题和应用场景。

事件委托

事件委托指的是将事件绑定在父节点上,从而将事件的处理交给子节点。在大量的DOM节点绑定事件处理程序时,事件委托可以提高性能,避免内存泄漏等问题。

<ul id="my-list">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>

// 绑定事件委托
const list = document.getElementById('my-list');
list.addEventListener('click', function(event) {
  const target = event.target;
  if (target.tagName === 'LI') {
    console.log('Clicked item:', target.textContent);
  }
});

在上述代码中,我们将点击事件绑定在ul元素上,而不是每个li元素上,从而减少了事件处理程序的数量和内存使用。

事件冒泡

事件冒泡指的是当某个元素触发了事件时,它的父元素也会触发该事件。

<div id="parent">
  <div id="child"></div>
</div>

// 绑定事件冒泡
const parent = document.getElementById('parent');
parent.addEventListener('click', function(event) {
  console.log('Parent clicked:', event.target.id);
});

const child = document.getElementById('child');
child.addEventListener('click', function(event) {
  console.log('Child clicked:', event.target.id);
});

在上述代码中,当点击child元素时,parent元素会触发点击事件,由于事件冒泡的机制,父元素和子元素的事件处理程序均会被执行。

代码调试

JS事件驱动模型中涉及到的事件和任务可能是异步的,我们需要使用适当的调试技术来调试代码,以便更好地理解事件驱动模型的实现和调试代码。

结论和展望:JS事件驱动模型的未来发展趋势

在未来,JS事件驱动模型可能会更加智能化,一些新的API和机制也将会出现,以便更好地满足业务需求和提高开发效率。

此外,技术不断更新,在了解JS事件驱动模型的基础上,我们还需要不断学习新的技术知识,不断探索和实践,才能不断提升自己的技能水平。

当然,在使用JS事件驱动模型时还需要注意效率和可维护性等问题,不断优化代码,提高性能和可读性,从而更好地应对各种场景的需求。

最后,我希望通过这篇文章,能够帮助大家更好地理解JS事件驱动模型的基本概念、应用场景和相关技巧,从而更好地编写和优化JavaScript代码。

原文链接:https://juejin.cn/post/7245183742263541817 作者:纯爱掌门人

(0)
上一篇 2023年6月17日 上午11:08
下一篇 2023年6月18日 上午10:00

相关推荐

发表回复

登录后才能评论