【JavaScript】这一次,彻底搞懂 JS 异步及其演进历程 ~(二)

简介: 【JavaScript】这一次,彻底搞懂 JS 异步及其演进历程 ~(二)

4、同步与异步(Synchronous vs. Asynchronous)

当涉及到需要一些时间才能完成的代码时(比如经常向服务器发出请求),同步运行你的代码并不是最好的选择,因为可能需要一些时间来取回你的数据,而且你可能不希望你的程序在发出请求时等待,相反,你希望它继续做其他事情。

为了实现这一点,你需要使用一个异步函数来请求外部数据,并将一个回调函数作为参数传递给它。这样,该函数现在就可以开始,一旦请求完成并收到数据,回调函数就可以稍后运行并完成。

在JavaScript中,如果任务是一个接一个地执行,那么一个操作就是同步的或阻塞的。每一步都必须在进行下一步之前完成,而且程序是按照语句的确切顺序进行评估的。这意味着,无论完成当前任务需要多长时间,下一个任务的执行都会被阻断,直到当前任务完成。

举一个例子:

function firstTask() {
  console.log("Task 1");
}
function secondTask() {
  console.log("Task 2");
}
function thirdTask() {
  console.log("Task 3");  
}
firstTask();
secondTask();
thirdTask();

如果我们在一个JavaScript文件里有这三个函数,它们将被逐一执行,在控制台中打印如下:

Task 1
Task 2
Task 3

任务2在任务1完成之前不能执行,任务3在任务2完成之前也不能执行。

与同步编程相反,如果下一个任务可以开始其执行过程,而不需要等待当前任务的完成,那么就可以说一个操作是异步的或非阻塞的。作为异步编程的结果,你可以同时执行许多请求(提出API请求和接收响应,滚动页面,重新绘制和更新位置),从而在更短的时间内完成任务。

让我们修改我们的代码,将任务2延迟5秒。

function firstTask() {
  console.log("Task 1");
}
function secondTask() {
  setTimeout(function() {
    console.log("Task 2")
  },5000);
}
function thirdTask() {
  console.log("Task 3");  
}
firstTask();
secondTask();
thirdTask();

为了演示异步操作,我们使用 setTimeout任务2延迟5000毫秒。 setTimeout接受两个参数:第一个输入是要执行的函数,第二个输入是你想在执行该函数前等待的毫秒数。

以下内容将被记录到控制台。

Task 1
Task 3
Task 2

setTimeout 通过继续操作而不是等待任务2的完成,使操作成为异步的。它继续运行任务3,然后在5秒后执行传入setTimeout的回调函数,之后将任务2记录在控制台。

二、异步演进历程

1、回调

“回调函数是作为参数传递给另一个函数的函数,然后在外部函数内部调用该函数以完成某种例程或操作。” (MDN

回调是处理 JavaScript 中异步操作的最古老和最基本的方法。回调只是一个作为参数传递给另一个函数的函数功能, 并在操作完成时执行。比如:

function fetchData(url, callback) {
  let xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
      callback(xhr.responseText);
    }
  };
  xhr.open('GET', url);
  xhr.send();
}
fetchData('https://jsonplaceholder.typicode.com/todos/1', function(data) {
  console.log(data);
});

在这个例子中,我们定义了一个fetchData()函数,它接受一个 URL 和一个回调函数作为参数。我们创建一个XMLHttpRequest对象并将其onreadystatechange属性设置为一个函数,该函数检查响应是否准备就绪并成功,如果是,则使用响应文本调用回调函数。

然后,我们fetchData()使用一个 URL 和一个将数据记录到控制台的匿名回调函数调用该函数。

当嵌套很深时,回调很快就会变得混乱且难以阅读,从而导致通常所说的“回调地狱”。为避免这种情况,最好使用命名函数并尽可能模块化您的代码。

回调很好地实现了异步,但如果你过度使用回调这种异步方式,有可能会造成 “回调地狱”。特别是当你将回调嵌套在多个深度的回调中时,就会发生这种情况。

回调地狱的形状像金字塔,也被称为“厄运金字塔”。这使得代码很难维护和理解。比如:

getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      getEvenEvenMoreData(c, function(d) {
        getFinalData(d, function(finalData) {
          console.log(finalData);
        });
      });
    });
  });
});

这种回调的嵌套会使代码难以维护,而缩进则使人更难看到代码的整体结构。

为了避免回调地狱,你可以使用一种更现代的处理异步操作的方式,即 Promises。与回调函数相比,Promise 提供了一种更优雅的方式来处理程序的异步流程。

2、事件发布/监听模式

在JavaScript中,事件发布/监听模式(Event Emitter/Listener Pattern)也是一种常用的异步编程实现方式。它通常由一个事件触发器(Event Emitter)多个事件监听器(Event Listener)组成,通过将异步操作的结果作为事件触发后通知所有相关的监听器来完成异步编程。

如果在浏览器中写过事件监听addEventListener,那么你对这种事件发布/监听的模式一定不陌生。

借鉴这种思想,一方面,我们可以监听某一事件,当事件发生时,进行相应回调操作;另一方面,当某些操作完成后,通过发布事件触发回调。这样就可以将原本捆绑在一起的代码解耦。

const events = require('events');
const eventEmitter = new events.EventEmitter();
eventEmitter.on('db', (err, kw) => {
    db.find(`select * from sample where kw = ${kw}`, (err, res) => {
        eventEmitter('get', res.length);
    });
});
eventEmitter.on('get', (err, count) => {
    get(`/sampleget?count=${count}`, data => {
        console.log(data);
    });
});
fs.readFile('./sample.txt', 'utf-8', (err, content) => {
    let keyword = content.substring(0, 5);
    eventEmitter. emit('db', keyword);
});

使用这种模式的实现需要一个事件发布/监听的库。上面代码中使用node原生的events模块,当然你可以使用任何你喜欢的库。

还可以自己封装一个事件发布/监听类来实现异步控制,下面让我们通过一个简单的示例来说明事件发布/监听模式如何实现异步编程。

class AsyncOperation {
  constructor() {
    this.subscribers = [];
  }
  subscribe(callback) {
    this.subscribers.push(callback);
  }
  async execute() {
    const data = await fetchData();
    this.subscribers.forEach(callback => callback(data));
  }
}
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      const data = 'Async Data';
      resolve(data);
    }, 1000);
  });
}
console.log('Start');
const operation = new AsyncOperation();
operation.subscribe(data => console.log(data));
operation.execute();
console.log('End');

在上述代码中,定义了一个名为 AsyncOperation 的类,该类包含了两个方法:subscribeexecute。其中,subscribe 方法用于向该对象添加事件监听器,execute 方法用于执行异步操作,并在操作结束后通知所有相关的监听器。

在该类内部,我们使用了一个数组 subscribers 来保存所有的事件监听器。在 execute 方法中,我们先执行异步操作 fetchData 并等待其返回。当 fetchData 返回结果后,我们遍历 subscribers 数组并依次调用每个监听器函数,将异步结果作为参数传入其中。最后,所有监听器都得到了异步结果并处理完成。

在主线程中,我们依次输出了 Start、End 和订阅的回调函数的结果。由于异步操作是异步执行的,因此所有的输出都是异步完成的。

事件发布/监听模式也是一种常用的实现异步编程的方式,在JavaScript中尤其常见。它可以使异步代码更加清晰易懂,并具有良好的可扩展性和灵活性。

3、Promise

Promises 提供了一种更结构化的方式来处理 JavaScript 中的异步操作。Promise 是表示异步操作最终完成(或失败)的对象。可以使用 then()catch() 方法链接和处理 Promise。比如:

function fetchData(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.responseText);
        } else {
          reject(new Error('Network response was not ok'));
        }
      }
    };
    xhr.open('GET', url);
    xhr.send();
  });
}
fetchData('https://jsonplaceholder.typicode.com/todos/1')
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

在此示例中,定义了一个fetchData()返回新 Promise 的函数。创建一个XMLHttpRequest对象并将其onreadystatechange属性设置为一个函数,如果请求成功,该函数将使用响应文本解析承诺,否则将返回错误。

然后调用 fetchData() 函数,用链式的 then()catch() 方法来处理这个 Promise。如果 Promise 得到解决,就将数据记录到控制台。如果 Promise 被拒绝,就将错误记录到控制台。Promises 提供了一种比回调更结构化和可读性更强的处理异步操作的方式,并且可以与现代语法一起使用,例如async/await

相关文章
|
5月前
|
资源调度 JavaScript 前端开发
Day.js极简轻易快速2kB的JavaScript库-替代Moment.js
dayjs是一个极简快速2kB的JavaScript库,可以为浏览器处理解析、验证、操作和显示日期和时间,它的设计目标是提供一个简单、快速且功能强大的日期处理工具,同时保持极小的体积(仅 2KB 左右)。
282 24
|
8月前
|
Web App开发 JavaScript 前端开发
如何在JavaScript中确定异步操作之间的依赖关系?
如何在JavaScript中确定异步操作之间的依赖关系?
197 58
|
8月前
|
前端开发 JavaScript
有没有方法可以保证在JavaScript中多个异步操作的执行顺序?
有没有方法可以保证在JavaScript中多个异步操作的执行顺序?
335 58
|
7月前
|
JavaScript 前端开发 算法
JavaScript 中通过Array.sort() 实现多字段排序、排序稳定性、随机排序洗牌算法、优化排序性能,JS中排序算法的使用详解(附实际应用代码)
Array.sort() 是一个功能强大的方法,通过自定义的比较函数,可以处理各种复杂的排序逻辑。无论是简单的数字排序,还是多字段、嵌套对象、分组排序等高级应用,Array.sort() 都能胜任。同时,通过性能优化技巧(如映射排序)和结合其他数组方法(如 reduce),Array.sort() 可以用来实现高效的数据处理逻辑。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
8月前
|
JavaScript 前端开发
JavaWeb JavaScript ③ JS的流程控制和函数
通过本文的详细介绍,您可以深入理解JavaScript的流程控制和函数的使用,进而编写出高效、可维护的代码。
179 32
|
7月前
|
JavaScript 前端开发 API
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)
array.map()可以用来数据转换、创建派生数组、应用函数、链式调用、异步数据流处理、复杂API请求梳理、提供DOM操作、用来搜索和过滤等,比for好用太多了,主要是写法简单,并且非常直观,并且能提升代码的可读性,也就提升了Long Term代码的可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
7月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
7月前
|
数据采集 JavaScript 前端开发
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
用array.filter()来实现数据筛选、数据清洗和链式调用,相对于for循环更加清晰,语义化强,能显著提升代码的可读性和可维护性。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
10月前
|
JavaScript 前端开发
【JavaScript】——JS基础入门常见操作(大量举例)
JS引入方式,JS基础语法,JS增删查改,JS函数,JS对象

热门文章

最新文章