[技术翻译]在现代JavaScript中编写异步任务

本周再来翻译一些技术文章,本次预计翻译三篇文章以下:javascript

我翻译的技术文章都放在一个github仓库中,若是以为有用请点击star收藏。我为何要建立这个git仓库?目的是经过翻译国外的web相关的技术文章来学习和跟进web发展的新思想和新技术。git仓库地址:https://github.com/yzsunlei/javascript-article-translatenode

在本文中,咱们将探讨过去围绕异步执行的JavaScript的演变以及它如何改变咱们编写和读取代码的方式。咱们将从Web开发的开始,一直到现代异步模式示例。
JavaScript做为编程语言具备两个主要特征,这两个特征对于理解咱们的代码是如何工做的都很重要。首先是它的同步特性,这意味着代码将几乎在您阅读时逐行运行,其次,它是单线程的,任什么时候候都只执行一个命令。git

随着语言的发展,新的模块出如今场景中以容许异步执行。开发人员在解决更复杂的算法和数据流时尝试了不一样的方法,从而致使围绕它们的新接口和模式的出现。github

同步执行和观察者模式

如引言中所述,JavaScript一般会逐行运行您编写的代码。即便在最初的几年中,该语言也有例外,尽管它们不多,您可能已经知道它们:HTTP请求,DOM事件和时间间隔。web

const button = document.querySelector('button');

// observe for user interaction
button.addEventListener('click', function(e) {
  console.log('user click just happened!');
})

若是添加事件侦听器(例如,单击元素并触发用户交互),则JavaScript引擎会将事件侦听器回调的任务放入队列,但将继续执行其当前堆栈中的内容。完成那里的调用以后,它如今将运行侦听器的回调。算法

此行为相似于网络请求和计时器发生的状况,它们是Web开发人员访问异步执行的第一个模块。编程

尽管这些是JavaScript中常见的同步执行例外的,但相当重要的是要了解该语言仍然是单线程的,而且尽管它能够将Task排队,异步运行它们而后返回主线程,但它只能一次执行一段代码。

咱们的工具手册,其中Alla Kholmatova探索了如何建立有效且可维护的设计系统来设计出色的数字产品。认识Design Systems,了解常见的陷阱,陷阱和Alla多年来汲取的经验教训。

例如,让咱们发送一个网络请求。

var request = new XMLHttpRequest();
request.open('GET', '//some.api.at/server', true);

// observe for server response
request.onreadystatechange = function() {
  if (request.readyState === 4 && xhr.status === 200) {
    console.log(request.responseText);
  }
}

request.send();

服务器返回时,分配给该方法的任务将onreadystatechange放入队列(代码在主线程中继续执行)。

注意:解释JavaScript引擎如何将任务排队和处理执行线程是一个很复杂的主题,可能值得一读。不过,我仍是建议您查看“事件循环究竟是什么?”菲利普·罗伯茨(Phillip Roberts)提供的帮助,以帮助您更好地理解。

在上述每种状况下,咱们都在响应外部事件。达到必定的时间间隔,用户操做或服务器响应。咱们自己没法建立异步任务,咱们始终观察到发生的事件超出了咱们的范围。

这就是为何将这种模板式的代码称为“观察者模式”,addEventListener在这种状况下,能够更好地由接口表示。很快,暴露这种模式的事件库或框架蓬勃发展。

NODE.JS和事件触发器

一个很好的例子是Node.js,该页面将本身描述为“异步事件驱动的JavaScript运行时”,所以事件触发器和回调是一等公民。它甚至用EventEmitter已经实现了一个构造函数。

const EventEmitter = require('events');
const emitter = new EventEmitter();

// respond to events
emitter.on('greeting', (message) => console.log(message));

// send events
emitter.emit('greeting', 'Hi there!');

这不只是异步执行的通用方法,并且是其生态系统的核心模式和惯例。Node.js开辟了一个在不一样环境中甚至在网络以外编写JavaScript的新时代。结果,其余异步状况也是可能的,例如建立新目录或写入文件。

const { mkdir, writeFile } = require('fs');

const styles = 'body { background: #ffdead; }';

mkdir('./assets/', (error) => {
  if (!error) {
    writeFile('assets/main.css', styles, 'utf-8', (error) => {
      if (!error) console.log('stylesheet created');
    })
  }
})

您可能会注意到,回调error函数的第一个参数为,若是须要响应数据,则将其做为第二个参数。这被称为“错误优先回调模式”,它成为做者和贡献者为其本身的程序包和库所采用的约定。

Promise和无尽的回调链

随着Web开发面临更复杂的问题须要解决,对更好的异步工件的需求出现了。若是咱们查看最后一个代码片断,咱们会看到重复的回调链,随着任务数量的增长,回调链的扩展效果就不好。

例如,让咱们仅添加两个步骤,即文件读取和样式预处理。

const { mkdir, writeFile, readFile } = require('fs');
const less = require('less')

readFile('./main.less', 'utf-8', (error, data) => {
  if (error) throw error
  less.render(data, (lessError, output) => {
    if (lessError) throw lessError
    mkdir('./assets/', (dirError) => {
      if (dirError) throw dirError
      writeFile('assets/main.css', output.css, 'utf-8', (writeError) => {
        if (writeError) throw writeError
        console.log('stylesheet created');
      })
    })
  })
})

咱们能够看到,因为多个回调链和重复的错误处理,随着正在编写的程序变得愈来愈复杂,代码变得更加难觉得人所知。

Promise,包装和连锁模式

Promises最初宣布它们是JavaScript语言的新功能时,并无引发太多关注,它们并非一个新概念,由于其余语言在几十年前就已经实现了相似的功能。事实是,自从出现以来,他们发现我所作的大多数项目的语义和结构都发生了很大变化。

Promises不只引入了供开发人员编写异步代码的内置解决方案,并且还为Web开发(如Web规范)的新功能的构建基础打开了Web开发的新阶段fetch。

从回调方法迁移到基于Promise的方法在项目(例如库和浏览器)中变得愈来愈广泛,甚至Node.js也开始缓慢地迁移到它们。

例如,包装一下Node的readFile方法:

const { readFile } = require('fs');

const asyncReadFile = (path, options) => {
  return new Promise((resolve, reject) => {
    readFile(path, options, (error, data) => {
      if (error) reject(error);
      else resolve(data);
    })
  });
}

在这里,咱们经过在Promise构造函数中执行,resolve在方法结果成功时以及reject在定义错误对象时调用,来掩盖回调。

当一个方法返回一个Promise对象时,咱们能够经过将一个函数传递给来遵循其成功的解析then,其参数是Promise被解析的值,在这种状况下为data。

若是在方法期间引起错误catch,则将调用该函数(若是存在)。

注意:若是您须要更深刻地了解Promises的工做方式,我建议Jake Archibald 在Google的Web开发博客上写的“JavaScript Promises:Introduction”一文。

如今咱们可使用这些新方法并避免回调链。

asyncRead('./main.less', 'utf-8')
  .then(data => console.log('file content', data))
  .catch(error => console.error('something went wrong', error))

具备建立异步任务的方法和清晰的界面以跟踪其可能的结果,使该行业摆脱了观察者模式。基于Promise的代码彷佛能够解决不可读且容易出错的代码。

随着更好的语法或更清晰的错误消息在编码时突出显示有所帮助,对于开发人员来讲,更易于推理的代码变得更具可预测性,而且执行路径的状况更好,更容易捕捉可能的代码陷阱。

Promises因为在社区中的普及程度很高,Node.js迅速发布了其I/O方法的内置版本以返回Promise对象,例如从中导入文件操做fs.promises。

它甚至提供了一个promisify实用工具,用于包装遵循错误优先回调模式的全部函数,并将其转换为基于Promise的函数。

可是,Promises在全部状况下都能提供帮助吗?

让咱们从新想象一下用Promises编写的样式预处理任务。

const { mkdir, writeFile, readFile } = require('fs').promises;
const less = require('less')

readFile('./main.less', 'utf-8')
  .then(less.render)
  .then(result =>
    mkdir('./assets')
      .then(writeFile('assets/main.css', result.css, 'utf-8'))
  )
  .catch(error => console.error(error))

代码中的冗余明显减小了,尤为是在咱们如今所依赖的错误处理方面catch,可是Promises某种程度上未能提供与操做串联直接相关的清晰代码缩进。

这其实是在调用then以后的第一个语句上实现的readFile。这些行以后发生的事情是须要建立一个新的做用域,咱们能够在该做用域中首先建立目录,而后将结果写入文件中。这就致使了缩进节奏的中断,乍看之下很难肯定指令序列。

解决此问题的一种方法是预先处理该问题的自定义方法,并容许该方法正确链接,可是咱们将向彷佛已经具备实现任务所需功能的代码引入更多的复杂性。

注意:这是一个示例程序,咱们能够控制某些方法,它们都遵循行业惯例,但并不是老是如此。经过更复杂的串联或引入具备不一样类型的库,咱们能够轻松破坏代码风格。

使人高兴的是,JavaScript社区再次从其余语言语法中学到了东西,并添加了一种表示法,能够在不少状况下帮助异步任务串联而不是像同步代码那样使人愉悦或直截了当。

async和await

A Promise在执行时被定义为一个未解析的值,建立a的实例Promise是对该模块的显式调用。

const { mkdir, writeFile, readFile } = require('fs').promises;
const less = require('less')

readFile('./main.less', 'utf-8')
  .then(less.render)
  .then(result =>
    mkdir('./assets')
      .then(writeFile('assets/main.css', result.css, 'utf-8'))
  )
  .catch(error => console.error(error))

在async方法内部,咱们可使用await保留字来肯定a的分辨率,Promise而后继续执行。

让咱们使用此语法从新访问或编写代码段。

const { mkdir, writeFile, readFile } = require('fs').promises;
const less = require('less')

async function processLess() {
  const content = await readFile('./main.less', 'utf-8')
  const result = await less.render(content)
  await mkdir('./assets')
  await writeFile('assets/main.css', result.css, 'utf-8')
}

processLess()

注意:请注意,因为咱们今天不能在异步函数的范围以外使用,所以须要将全部代码移至方法await。

每次async方法找到一条await语句时,它将中止执行,直处处理中的值或Promise被解析为止。

尽管异步执行,但使用async/await表示法会有明显的后果,代码看起来好像是async,这是咱们开发人员更习惯查看和推理的。

错误处理呢?为此,咱们使用在该语言中已经存在很长时间的语句,try和catch。

const { mkdir, writeFile, readFile } = require('fs').promises;
const less = require('less');

async function processLess() {
  try {
    const content = await readFile('./main.less', 'utf-8')
    const result = await less.render(content)
    await mkdir('./assets')
    await writeFile('assets/main.css', result.css, 'utf-8')
  } catch(e) {
    console.error(e)
  }
}

processLess()

放心,在该过程当中引起的任何错误将由该catch语句内的代码处理。咱们在中心位置负责错误处理,可是如今咱们有了一个易于阅读和遵循的代码。

具备返回值的后续操做不须要存储在mkdir不会破坏代码节奏的变量中;也无需在之后的步骤中建立新的做用域来访问result的值。

能够确定地说,Promises是该语言中引入的一个基本模块,对于在JavaScript中启用async/await表示法是必需的,您能够在现代浏览器和最新版本的Node.js中使用它。

注意:最近在JSConf中,Node的建立者和第一贡献者Ryan Dahl很遗憾没有坚持Promises的早期开发,主要是由于Node的目标是建立事件驱动的服务器和文件管理,而Observer模式更适合于此。

结论

将Promises引入Web开发世界的目的是改变咱们在代码中排队操做的方式,并改变了咱们对代码执行进行推理的方式以及咱们编写库和包的方式。

可是摆脱回调链很难解决,我认为then在多年习惯于观察者模式和主要提供商采用的方法以后,不得不经过一种方法并不能帮助咱们摆脱思路。像Node.js这样的社区。

正如诺兰·劳森(Nolan Lawson)在其有关Promise串联中错误使用的出色文章中所说,旧的回调习惯会死掉!稍后,他解释了如何避免这些陷阱。

我认为Promises是中间步骤,它容许以天然的方式生成异步任务,但并无帮助咱们进一步改进更好的代码模式,有时您实际上须要更适应和改进的语言语法。

当咱们尝试使用JavaScript解决更复杂的难题时,咱们看到了对更成熟语言的需求,并尝试了之前未曾在网络上看到过的架构和模式。

咱们仍然不知道ECMAScript规范的表现如何,由于咱们一直将JavaScript治理扩展到网络以外,并尝试解决更复杂的难题。

如今很难说咱们须要从语言中真正地将这些难题转变成更简单的程序所须要的东西,可是我对Web和JavaScript自己如何推进事物,试图适应挑战和新环境感到满意。与十年前开始在浏览器中编写代码相比,如今我以为JavaScript是一个更加异步的友好的地方。

原文连接:https://www.smashingmagazine.com/2019/10/asynchronous-tasks-modern-javascript/

相关文章
相关标签/搜索