如何在现代JavaScript中编写异步任务


Posted in Javascript onJanuary 31, 2021

前言

在本文中,我们将探讨过去异步执行的 JavaScript 的演变,以及它是怎样改变我们编写代码的方式的。我们将从最早的 Web 开发开始,一直到现代异步模式。

作为编程语言, JavaScript 有两个主要特征,这两个特征对于理解我们的代码如何工作非常重要。首先是它的同步特性,这意味着代码将逐行运行,其次是单线程,任何时候都仅执行一个命令。

随着语言的发展,允许异步执行的新工件出现在场景中。开发人员在解决更复杂的算法和数据流时尝试了不同的方法,从而导致新的接口和模式出现。

同步执行和观察者模式

如简介中所述,JavaScript 通常会逐行运行你编写的代码。即使在最初的几年中,该语言也有这种规则的例外,尽管很少,你可能已经知道了它们:HTTP 请求,DOM 事件和time interval。

如果我们通过添加事件侦听器去响应用户对元素的单击,则无论语言解释器在运行什么,它都会停止,然后运行在侦听器回调中编写的代码,之后再返回正常的流程。

与 interval 或网络请求相同,addEventListener,setTimeout 和 XMLHttpRequest 是 Web 开发人员访问异步执行的第一批工件。

尽管这些是 JavaScript 中同步执行的例外情况,但重要的是你要了解该语言仍然是单线程的。我们可以打破这种同步性,但是解释器仍然每次运行一行代码。

例如检查一个网络请求。

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);
 }
}

11request.send();

不管发生什么情况,当服务器恢复运行时,分配给 onreadystatechange 的方法都会在取回程序的代码序列之前被调用。

对用户交互做出反应时,也会发生类似的情况。

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

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

你可能会注意到,我们正在连接一个外部事件并传递一个回调,告诉代码当事件发生时应该怎么做。十多年前,“什么是回调?”是一个非常受期待的面试问题,因为在很多代码库中到处都有这种模式。

在上述每种情况下,我们都在响应外部事件。不管是达到一定的时间间隔、用户操作还是服务器响应。我们本身无法创建异步任务,我们总是 观察 发生在我们力所能及范围之外的事件。

这就是为什么这种方式的代码被称为观察者模式的原因,在这种情况下,它最好由 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 开辟了一个在不同环境中甚至在 web 之外编写 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');
  })
 })
 })
16})

我们可以看到,由于多个回调链和重复的错误处理,编写程序变得越来越复杂,代码变得更加难以理解。

Promise、包装和链模式

当 Promises 最初被宣布为 JavaScript 语言的新成员时,并没有引起太多关注,它们并不是一个新概念,因为其他语言在几十年前就已经实现了类似的实现。事实上自从它出现以来,他们就改变了我从事的大多数项目的语义和结构。

Promises不仅为开发人员引入了用于编写异步代码的内置解决方案,,而且还开辟了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 函数(如果存在)。

注意:如果你需要更深入地了解 Promise 的工作原理,建议你看 Jake Archibald 在 Google 的 web 开发博客上写的文章“ JavaScript Promises:简介”。

现在我们可以使用这些新方法并避免回调链。

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 的函数。

但是 Promise 在所有情况下都能提供帮助吗?

让我们重新评估一下用 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))

代码中的冗余明显减少了,尤其是在错误处理方面,因为我们现在依赖于 catch,但是 Promise 在某种程度上没能提供直接与动作串联相关的清晰代码缩进。

实际上,这是在调用 readFile 之后的第一个 then 语句中实现的。这些代码行之后发生的事情是需要创建一个新的作用域,我们可以在该作用域中先创建目录,然后将结果写入文件中。这会导致缩进节奏的中断,乍一看就不容易确定指令序列。

注意:请注意,这是一个示例程序,我们可以控制某些方法,它们都遵循行业惯例,但并非总是如此。通过更复杂的串联或引入不同的库,我们的代码风格可以轻松被打破。

令人高兴的是,JavaScript 社区再次从其他语言的语法中学到了东西,并增加了一种表示方法,可以在大多数情况下帮助异步任务串联,而不是像同步代码那样能够令人轻松的阅读。

Async 与 Await

Promise 被定义为执行时的未解决的值,创建 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))

在异步方法内部,我们可以用 await 保留字来确定 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')
}

11processLess()

注意:请注意,我们需要将所有代码移至某个方法中,因为我们无法在 异步函数的作用域之外使用 await 。

每当异步方法找到一个 await 语句时,它将停止执行,直到 promise 被解决为止。

尽管是异步执行,但用 async/await 表示会使代码看起来好像是同步的,这是容易被开发人员阅读和理解的东西。

那么错误处理呢?我们可以用在语言中存在了很久的try 和 catch。

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')
}

try {
 processLess()
} catch (e) {
 console.error(e)
}

我们大可放心,在过程中抛出的任何错误都会由 catch 语句中的代码处理。现在我们有了一个易于阅读和规范的代码。

对返回值进行的后续操作无需存储在不会破坏代码节奏的 mkdir 之类的变量中;也无需在以后的步骤中创建新的作用域来访问 result 的值。

可以肯定地说,Promise 是该语言中引入的基本工件,对于在 JavaScript 中启用 async/await 表示法是必需的,你可以在现代浏览器和最新版本的 Node.js 中使用它。

注意:最近在 JSConf 中,Node 的创建者和第一贡献者 Ryan Dahl, 对在其早期开发中没有遵守Promises 表示遗憾,主要是因为 Node 的目标是创建事件驱动服务器和文件管理,而 Observer 模式更适合这样。

结论

将 Promise 引入 Web 开发的目的是改变我们在代码中顺序操作的方式,并改变了我们理解代码的方式以及编写库和包的方式。

但是摆脱回调链更难解决,我认为在多年来习惯于观察者模式和采用的方法之后,必须将方法传递给 then 并不能帮助我们摆脱原有的思路,例如 Node.js。

正如 Nolan Lawson 在他的出色文章“关于 Promise 级联的错误使用“【https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html】 中所述,旧的回调习惯是死硬且顽固的!在文中他解释了如何避免这些陷阱。

我认为 Promise 是中间步骤,它允许以自然的方式生成异步任务,但并没有帮助我们进一步改进更好的代码模式,有时你需要更适应改进的语言语法。

当尝试使用JavaScript解决更复杂的难题时,我们看到了对更成熟语言的需求,并且我们尝试了以前不曾在网上看到的体系结构和模式。

我们仍然不知道 ECMAScript 规范在几年后的样子,因为我们一直在将 JavaScript 治理扩展到 web 之外,并尝试解决更复杂的难题。

现在很难说我们需要从语言中真正地将这些难题转变成更简单的程序,但是我对 Web 和 JavaScript 本身如何推动技术,试图适应挑战和新环境感到满意。与十年前刚刚开始在浏览器中编写代码时相比,我觉得现在 JavaScript 是“异步友好”的。

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

到此这篇关于如何在现代JavaScript中编写异步任务的文章就介绍到这了,更多相关JavaScript编写异步任务内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jquery复选框CHECKBOX全选、反选
Aug 30 Javascript
javascript 延迟加载技术(lazyload)简单实现
Jan 17 Javascript
Javascript继承机制的设计思想分享
Aug 28 Javascript
jQuery+ajax实现鼠标单击修改内容的方法
Jun 27 Javascript
JavaScript中setUTCFullYear()方法的使用简介
Jun 12 Javascript
跟我学习javascript的循环
Nov 18 Javascript
jquery 重写 ajax提交并判断权限后 使用load方法报错解决方法
Jan 19 Javascript
详解JavaScript的AngularJS框架中的表达式与指令
Mar 05 Javascript
Bootstrap 模态框实例插件案例分析
Dec 28 Javascript
bootstrap select下拉搜索插件使用方法详解
Nov 23 Javascript
vue.js高德地图实现热点图代码实例
Apr 18 Javascript
微信小程序点击按钮动态切换input的disabled禁用/启用状态功能
Mar 07 Javascript
ES6的循环与可迭代对象示例详解
Jan 31 #Javascript
Javascript生成器(Generator)的介绍与使用
Jan 31 #Javascript
Node.js中的异步生成器与异步迭代详解
Jan 31 #Javascript
Vite和Vue CLI的优劣
Jan 30 #Vue.js
如何使用RoughViz可视化Vue.js中的草绘图表
Jan 30 #Vue.js
Element-ui 自带的两种远程搜索(模糊查询)用法讲解
Jan 29 #Javascript
小程序实现列表倒计时功能
Jan 29 #Javascript
You might like
PHP判断变量是否为0的方法
2014/02/08 PHP
ThinkPHP2.0读取MSSQL提示Incorrect syntax near the keyword 'AS'的解决方法
2014/06/25 PHP
php生成excel列名超过26列大于Z时的解决方法
2014/12/29 PHP
javascript对象之内置对象Math使用方法
2010/04/16 Javascript
JavaScript作用域与作用域链深入解析
2013/12/06 Javascript
jquery JSON的解析方式示例介绍
2014/07/27 Javascript
js脚本实现数据去重
2014/11/27 Javascript
JavaScript日期类型的一些用法介绍
2015/03/02 Javascript
实例详解Nodejs 保存 payload 发送过来的文件
2016/01/14 NodeJs
BootStrap和jQuery相结合实现可编辑表格
2016/04/21 Javascript
jQuery pagination分页示例详解
2018/10/23 jQuery
vue写h5页面的方法总结
2019/02/12 Javascript
基于Express框架使用POST传递Form数据
2019/08/10 Javascript
浅析webpack-bundle-analyzer在vue-cli3中的使用
2019/10/23 Javascript
vue 指令和过滤器的基本使用(品牌管理案例)
2019/11/04 Javascript
vue 点击其他区域关闭自定义div操作
2020/07/17 Javascript
ant-design表单处理和常用方法及自定义验证操作
2020/10/27 Javascript
python实现得到一个给定类的虚函数
2014/09/28 Python
Python处理JSON时的值报错及编码报错的两则解决实录
2016/06/26 Python
python利用smtplib实现QQ邮箱发送邮件
2020/05/20 Python
matplotlib给子图添加图例的方法
2018/08/03 Python
使用Tensorflow实现可视化中间层和卷积层
2020/01/24 Python
Python中的全局变量如何理解
2020/06/04 Python
Python爬取微信小程序通用方法代码实例详解
2020/09/29 Python
利用CSS3实现单选框动画特效示例代码
2016/09/26 HTML / CSS
在家更换处方镜片:Lensabl
2019/05/01 全球购物
美国艺术和工艺品商店:Hobby Lobby
2020/12/09 全球购物
为什么需要版本控制?
2013/08/08 面试题
关于工资低的辞职信
2014/01/14 职场文书
物业保安岗位职责
2014/07/02 职场文书
债务纠纷委托书范本
2014/10/14 职场文书
仓管员岗位职责范本
2015/04/01 职场文书
三十年同学聚会致辞
2015/07/28 职场文书
宣传部部长竞选稿
2015/11/21 职场文书
如果用一句诗总结你的上半年,你会用哪句呢?
2019/07/16 职场文书
浅谈怎么给Python添加类型标注
2021/06/08 Python