【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

相关文章
|
2月前
|
机器学习/深度学习 人工智能 JavaScript
js和JavaScript
js和JavaScript
21 4
|
2天前
|
JavaScript 前端开发 开发工具
【JavaScript 技术专栏】Node.js 基础与实战
【4月更文挑战第30天】本文介绍了Node.js的基础及应用,包括事件驱动的非阻塞I/O、单线程模型和模块系统。内容涵盖Node.js的安装配置、核心模块(如http、fs、path)及实战应用,如Web服务器、文件操作和实时通信。文章还讨论了Node.js的优劣势、与其他技术的结合,并通过案例分析展示项目实施流程。总结来说,Node.js是高效后端开发工具,适合构建高并发应用,其广阔的应用前景值得开发者探索。
|
3天前
|
JSON JavaScript 前端开发
深入探讨javascript的流程控制与分支结构,以及js的函数
深入探讨javascript的流程控制与分支结构,以及js的函数
|
3天前
|
JavaScript 大数据 开发者
Node.js的异步I/O模型与事件循环:深度解析
【4月更文挑战第29天】本文深入解析Node.js的异步I/O模型和事件循环机制。Node.js采用单线程与异步I/O,遇到I/O操作时立即返回并继续执行,结果存入回调函数队列。事件循环不断检查并处理I/O事件,通过回调函数通知结果,实现非阻塞和高并发。这种事件驱动编程模型简化了编程,使开发者更专注业务逻辑,为高并发场景提供高效解决方案。
|
9天前
|
JavaScript 前端开发 算法
< JavaScript小技巧:如何优雅的用【一行代码 】实现Js中的常用功能 >
在开发中,采用简洁的语法和结构,遵循一致的命名规范,具有良好的代码组织和注释,能很好的提高代码的质量。可读性:易于阅读和理解。清晰的命名、简洁的语法和良好的代码结构可以使代码的意图更加明确,降低理解代码的难度,提高代码的可读性。可维护性:易于维护。当代码逻辑清晰、结构简洁时,开发者可以更快速地定位和修复bug,进行功能扩展或修改。同时,可读性高的代码也有助于后续的代码重构和优化。可扩展性:更具有扩展性和灵活性。清晰的代码结构和简洁的代码风格使得添加新功能、修改现有功能或扩展代码更加容易。
< JavaScript小技巧:如何优雅的用【一行代码 】实现Js中的常用功能 >
|
9天前
|
JavaScript 前端开发
js开发:请解释this关键字在JavaScript中的用法。
【4月更文挑战第23天】JavaScript的this关键字根据执行环境指向不同对象:全局中指向全局对象(如window),普通函数中默认指向全局对象,作为方法调用时指向调用对象;构造函数中指向新实例,箭头函数继承所在上下文的this。可通过call、apply、bind方法显式改变this指向。
8 1
|
21天前
|
Web App开发 缓存 JavaScript
|
1月前
|
JavaScript 前端开发
JS 单线程还是多线程,如何显示异步操作
JS 单线程还是多线程,如何显示异步操作
22 2
|
1月前
|
JavaScript 前端开发
JavaScript生成的随机数随机字符串JS生成的随机数随机字符串
JavaScript生成的随机数随机字符串JS生成的随机数随机字符串
16 1
|
2月前
|
前端开发 JavaScript
如何处理 JavaScript 中的异步操作和 Promise?
如何处理 JavaScript 中的异步操作和 Promise?
15 1