Introducing asynchronous JavaScript

 

在本文中,我们简要回顾了与同步JavaScript相关的问题,并首先了解了您将遇到的一些不同的异步技术,展示了它们如何帮助我们解决此类问题.

Prerequisites: 基本的计算机知识,对JavaScript基础的合理理解.
Objective: To gain familiarity with what asynchronous JavaScript is, how it differs from synchronous JavaScript, and what use cases it has.

Synchronous JavaScript

为了让我们了解异步 JavaScript是什么,我们应该首先确保我们了解同步 JavaScript是什么. 本节概述了我们在上一篇文章中看到的一些信息.

我们在先前的学习领域模块中看到的许多功能都是同步的—您运行了一些代码,并且浏览器可以尽快返回结果. 让我们看一个简单的示例( 在此处实时查看查看源代码 ):

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

在此块中,各行依次执行:

  1. 我们获取对DOM中已经可用的<button>元素的引用.
  2. We add a click event listener to it so that when the button is clicked:
    1. 出现alert()消息.
    2. 解除警报后,我们将创建一个<p>元素.
    3. 然后,我们给它一些文本内容.
    4. 最后,我们将该段落附加到文档正文中.

在处理每个操作时,别无他法-渲染暂停. 这是因为正如我们在上一篇文章中所述, JavaScript是单线程的 . 一次只能在一个主线程上发生一件事情,其他所有事情都将被阻塞,直到操作完成.

因此,在上面的示例中,单击按钮后,只有在警告框中单击确定按钮之后,该段落才会显示. 您可以自己尝试:

注意 :重要的是要记住, alert()对于演示同步阻塞操作非常有用,但是在现实世界中使用时却很糟糕.

Asynchronous JavaScript

由于前面提到的原因(例如,与阻止有关),许多Web API功能现在都使用异步代码来运行,尤其是那些从外部设备访问或获取某种资源的功能,例如从网络获取文件,访问数据库和从中返回数据,从网络摄像头访问视频流,或将显示内容广播到VR耳机.

为什么使用同步代码很难上班? 让我们看一个简单的例子. 从服务器获取图像时,无法立即返回结果. 这意味着以下(伪代码)将不起作用:

let response = fetch('myImage.png');
let blob = response.blob();
// display your image blob in the UI somehow

那是因为您不知道下载映像将花费多长时间,因此当您运行第二行时,由于response尚不可用,它将抛出错误(可能是间歇性的,可能是每次). 相反,您需要代码等待response返回之前,尝试对它执行其他任何操作.

您将在JavaScript代码中遇到两种主要类型的异步代码样式,即老式回调和较新的Promise样式代码. 在以下各节中,我们将依次检查每个.

Async callbacks

异步回调是在调用将在后台开始执行代码的函数时指定为参数的函数. 当后台代码完成运行时,它将调用回调函数以使您知道工作已完成,或者使您知道发生了一些有趣的事情. 现在,使用回调有点过时,但您仍会在许多较旧但仍常用的API中看到它们的使用.

异步回调的一个示例是addEventListener()方法的第二个参数(如我们在上面的操作中所见):

btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

第一个参数是要侦听的事件的类型,第二个参数是在触发事件时调用的回调函数.

当我们将回调函数作为参数传递给另一个函数时,我们只是将函数的引用作为参数传递,即,回调函数不会立即执行. 它在包含函数的主体内部某处被异步地称为" back"(因此得名). 时间到时,包含函数负责执行回调函数.

您可以很容易地编写自己的包含回调的函数. 让我们看另一个示例,该示例通过XMLHttpRequest API加载资源( 实时运行它 ,并查看源代码 ):

function loadAsset(url, type, callback) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', url);
  xhr.responseType = type;

  xhr.onload = function() {
    callback(xhr.response);
  };

  xhr.send();
}

function displayImage(blob) {
  let objectURL = URL.createObjectURL(blob);

  let image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}

loadAsset('coffee.jpg', 'blob', displayImage);

在这里,我们创建了一个displayImage()函数,该函数简单地表示作为对象URL传递给它的blob,然后创建一个图像以显示URL,并将其附加到文档的<body> . 但是,然后我们创建一个loadAsset()函数,该函数将回调作为参数,以及要获取的URL和内容类型. 它使用XMLHttpRequest (通常缩写为" XHR")来获取给定URL处的资源,然后再将响应传递给回调以执行某些操作. 在这种情况下,回调将等待XHR调用完成下载(使用onload事件处理程序),然后再将资源传递给回调.

回调具有多种功能-不仅使您可以控制函数运行的顺序以及在函数之间传递哪些数据,还可以使您根据情况将数据传递给不同的函数. 因此,您可以对下载的响应执行不同的操作,例如processJSON()displayText()等.

请注意,并非所有的回调都是异步的,有些是同步运行的. 例如,当我们使用Array.prototype.forEach()遍历数组中的项目时( 请参见livesource ):

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

在此示例中,我们遍历希腊神的数组,并将索引号和值打印到控制台. forEach()的预期参数是一个回调函数,该函数本身带有两个参数,即对数组名称和索引值的引用. 但是,它不会等待任何东西,它会立即运行.

Promises

承诺是新样式的异步代码,您将在现代Web API中看到它们. 一个很好的例子是fetch() API,它基本上类似于XMLHttpRequest的现代,更有效的版本. 让我们看一个简单的例子,来自我们从服务器获取数据一文:

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
});

注意 :您可以在GitHub上找到完成的版本( 请参阅此处的源 ,也可以实时运行它 ).

在这里,我们看到fetch ()接受一个参数-您要从网络中获取资源的URL-并返回一个promise . promise是一个对象,代表异步操作的完成或失败. 它代表着一种中间状态. 本质上,这是浏览器说"我保证会尽快答复您"的方式,因此命名为"承诺".

这个概念可以通过实践来适应. 感觉有点像薛定ding的猫 . 尚未发生任何可能的结果,因此,获取操作当前正在等待浏览器尝试在将来某个时候完成该操作的结果. 然后,我们将另外三个代码块链接到fetch()的末尾:

  • 两个then()块. 两者都包含一个回调函数,如果先前的操作成功完成,则该函数将运行,并且每个回调都将先前成功的操作的结果作为输入接收,因此您可以继续进行其他操作. 每个.then()块都返回另一个promise,这意味着您可以将多个.then()块彼此链接在一起,因此可以使多个异步操作依次运行.
  • 如果任何.then()块发生故障.then()最后的catch()块就会运行-与同步try...catch块类似,在catch()内部提供了一个错误对象,该对象可用于报告已发生的错误类型. 但是请注意,同步try...catch不能与promise一起使用,尽管它可以与async / await一起使用 ,您将在稍后学习.

注意 :您将在本模块的稍后部分中了解有关诺言的更多信息,所以如果您还没有完全理解诺言,请不要担心.

The event queue

诸如Promise之类的异步操作被放入事件队列中 ,该事件在主线程完成处理后运行,以便它们不会阻止后续的JavaScript代码运行. 排队的操作将尽快完成,然后将其结果返回到JavaScript环境.

Promises versus callbacks

承诺与旧式回调有一些相似之处. 它们本质上是一个返回对象,您可以在其中附加回调函数,而不必将回调传递给函数.

但是,promise是专门用于处理异步操作的,与旧式回调相比,它们具有许多优点:

  • 您可以使用多个.then()操作将多个异步操作链接在一起,将一个结果作为输入传递给下一个. 这对于回调来说要困难得多,而回调通常会导致混乱的"厄运金字塔"(也称为回调地狱 ).
  • 始终以严格的顺序调用Promise回调,将它们放置在事件队列中.
  • 错误处理要好得多-所有错误都由该块末尾的单个.catch()块处理,而不是在"金字塔"的每个级别中单独处理.
  • 与老式的回调不同,Promise避免控件的反转,后者在将回调传递给第三方库时完全无法控制函数的执行方式.

The nature of asynchronous code

让我们探索一个示例,该示例进一步说明了异步代码的性质,显示了当我们不完全了解代码执行顺序以及尝试将异步代码视为同步代码的问题时可能发生的情况. 下面的示例与我们之前看到的非常相似( 请参见livesource ). 区别在于,我们包含了许多console.log()语句,以说明您可能认为代码将在其中执行的顺序.

console.log ('Starting');
let image;

fetch('coffee.jpg').then((response) => {
  console.log('It worked :)')
  return response.blob();
}).then((myBlob) => {
  let objectURL = URL.createObjectURL(myBlob);
  image = document.createElement('img');
  image.src = objectURL;
  document.body.appendChild(image);
}).catch((error) => {
  console.log('There has been a problem with your fetch operation: ' + error.message);
});

console.log ('All done!');

浏览器将开始执行代码,请参阅第一个console.log()语句( Starting )并执行它,然后创建image变量.

然后它将移至下一行并开始执行fetch()块,但是由于fetch()异步执行而没有阻塞,因此代码在执行与promise相关的代码之后继续执行,从而到达最终的console.log()语句( All done! )并将其输出到控制台.

只有在fetch()块完全完成运行并通过.then()块传递其结果之后.then()我们才能最终看到第二个console.log()消息( It worked :) ). 因此,消息的显示顺序与您预期的顺序不同:

  • Starting
  • 全部完成!
  • 有效 :)

如果这使您感到困惑,请考虑以下较小的示例:

console.log("registering click handler");

button.addEventListener('click', () => {
  console.log("get click");
});

console.log("all done");

这在行为上非常相似-第一个和第三个console.log()消息将立即显示,但是第二个消息将被阻止运行,直到有人单击鼠标按钮. 前面的示例以相同的方式工作,除了在这种情况下,第二条消息在Promise链上被阻止,以获取资源,然后在屏幕上而不是单击时显示该资源.

In a less trivial code example, this kind of setup could cause a problem — you can't include an async code block that returns a result, which you then rely on later in a sync code block. You just can't guarantee that the async function will return before the browser has processed the sync block.

要查看实际效果,请尝试获取本示例的本地副本,并将第三个console.log()调用更改为以下内容:

console.log ('All done! ' + image.src + 'displayed.');

现在,您应该在控制台中看到错误,而不是第三条消息:

TypeError: image is undefined; can't access its "src" property

这是因为在浏览器尝试运行第三个console.log()语句时, fetch()块尚未完成运行,因此没有为image变量赋值.

注意 :出于安全原因,您不能从本地文件系统中fetch()文件(或在本地运行其他此类操作); 要在本地运行上面的示例,您必须通过本地网络服务器运行该示例.

Active learning: make it all async!

要修复有问题的fetch()示例并使三个console.log()语句按所需顺序显示,您还可以使第三个console.log()语句也异步运行. 可以通过将其移动到链接到第二个末尾的另一个.then()块内,或简单地将其移动到第二个then()块内来完成. 现在尝试修复此问题.

注意 :如果遇到困难,可以在此处找到答案 (也可以实时查看答案 ). 您也可以在本模块后面的" 带有承诺的Graceful异步编程"指南中找到有关Promise的更多信息.

Conclusion

在最基本的形式中,JavaScript是一种同步的,阻塞的单线程语言,一次只能进行一个操作. 但是网络浏览器定义了允许我们注册不应同步执行的功能和API的功能,而是应在发生某种事件(时间流逝,用户与鼠标的交互或数据的到达)时异步调用这些功能.例如通过网络). 这意味着您可以让您的代码同时执行多项操作,而无需停止或阻塞您的主线程.

我们要同步还是异步运行代码将取决于我们要执行的操作.

有时候,我们希望事情能够立即加载并发生. 例如,当将某些用户定义的样式应用于网页时,您将希望尽快应用这些样式.

但是,如果我们正在运行耗时的操作,例如查询数据库并使用结果填充模板,则最好将其从主线程中推送出去并异步完成任务. 随着时间的流逝,您将了解何时选择一种异步技术而不是一种同步技术.

In this module