【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

相关文章
|
10天前
|
JSON 前端开发 JavaScript
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。
|
1月前
|
前端开发 JavaScript 开发者
JS 异步解决方案的发展历程以及优缺点
本文介绍了JS异步解决方案的发展历程,从回调函数到Promise,再到Async/Await,每种方案的优缺点及应用场景,帮助开发者更好地理解和选择合适的异步处理方式。
|
1月前
|
人工智能 JavaScript 前端开发
使用Node.js模拟执行JavaScript
使用Node.js模拟执行JavaScript
|
1月前
|
消息中间件 JavaScript 前端开发
用于全栈数据流的 JavaScript、Node.js 和 Apache Kafka
用于全栈数据流的 JavaScript、Node.js 和 Apache Kafka
44 1
|
1月前
|
JavaScript 前端开发
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
电话号码正则表达式 代码 javascript+html,JS正则表达式判断11位手机号码
103 1
|
1月前
|
Web App开发 JavaScript 前端开发
Node.js:JavaScript世界的全能工具
Node.js:JavaScript世界的全能工具
|
1月前
|
JSON JavaScript 前端开发
使用JavaScript和Node.js构建简单的RESTful API服务器
【10月更文挑战第12天】使用JavaScript和Node.js构建简单的RESTful API服务器
17 0
|
1月前
|
移动开发 JavaScript 前端开发
【JavaScript】JS执行机制--同步与异步
【JavaScript】JS执行机制--同步与异步
21 0
|
1月前
|
数据采集 JavaScript 前端开发
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
JavaScript中通过array.filter()实现数组的数据筛选、数据清洗和链式调用,JS中数组过滤器的使用详解(附实际应用代码)
|
存储 JavaScript 前端开发
JavaScript与PHP中正则
有个在线调试正则的工具,点击查看工具。下面的所有示例代码,都可以在codepen上查看到。
JavaScript与PHP中正则