如何在现代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 相关文章推荐
屏蔽鼠标右键、Ctrl+n、shift+F10、F5刷新、退格键 的javascript代码
Apr 01 Javascript
jQuery获取文本节点之 text()/val()/html() 方法区别
Mar 01 Javascript
JavaScript中的数组特性介绍
Dec 30 Javascript
JS之if语句对接事件动作逻辑(详解)
Jun 28 Javascript
node.js的exports、module.exports与ES6的export、export default深入详解
Oct 26 Javascript
Node实战之不同环境下配置文件使用教程
Jan 02 Javascript
ios设备中angularjs无法改变页面title的解决方法
Sep 13 Javascript
Vue 使用beforeEach实现登录状态检查功能
Oct 31 Javascript
JS+CSS+HTML实现“代码雨”类似黑客帝国文字下落效果
Mar 17 Javascript
微信小程序开发打开另一个小程序的实现方法
May 17 Javascript
详解JavaScript之ES5的继承
Jul 08 Javascript
详解如何在vue+element-ui的项目中封装dialog组件
Dec 11 Vue.js
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函数
2010/02/16 PHP
php Session存储到Redis的方法
2013/11/04 PHP
php实现批量上传数据到数据库(.csv格式)的案例
2017/06/18 PHP
基于Jquery的跨域传输数据(JSONP)
2011/03/10 Javascript
Javascript学习笔记 delete运算符
2011/09/13 Javascript
解决bootstrap中modal遇到Esc键无法关闭页面
2015/03/09 Javascript
jquery实现仿新浪微博带动画效果弹出层代码(可关闭、可拖动)
2015/10/12 Javascript
jquery ajax分页插件的简单实现
2016/01/27 Javascript
基于jQuery实现仿QQ空间送礼物功能代码
2016/05/24 Javascript
微信小程序 wx:for的使用实例详解
2017/04/27 Javascript
Angular 2父子组件数据传递之@ViewChild获取子组件详解
2017/07/04 Javascript
打字效果动画的4种实现方法(超简单)
2017/10/18 Javascript
vue.js项目中实用的小技巧汇总
2017/11/29 Javascript
理顺8个版本vue的区别(小结)
2018/09/17 Javascript
react的滑动图片验证码组件的示例代码
2019/02/27 Javascript
vue登录注册实例详解
2019/09/14 Javascript
python缩进区别分析
2014/02/15 Python
使用python实现strcmp函数功能示例
2014/03/25 Python
Python实现字符串格式化输出的方法详解
2017/09/20 Python
django框架自定义用户表操作示例
2018/08/07 Python
Django开发的简易留言板案例详解
2018/12/04 Python
python 从文件夹抽取图片另存的方法
2018/12/04 Python
Python Selenium 之数据驱动测试的实现
2019/08/01 Python
Python打包模块wheel的使用方法与将python包发布到PyPI的方法详解
2020/02/12 Python
python GUI库图形界面开发之PyQt5选项卡控件QTabWidget详细使用方法与实例
2020/03/01 Python
localStorage、sessionStorage使用总结
2017/11/17 HTML / CSS
世界上最大的乐器零售商:Guitar Center
2017/11/07 全球购物
德国香水、化妆品和护理产品网上商店:Parfumdreams
2018/09/26 全球购物
英国最大的在线照明商店:Litecraft
2020/08/31 全球购物
全球性的众包图形设计市场:DesignCrowd
2021/02/02 全球购物
XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式?
2016/01/12 面试题
超市优秀员工获奖感言
2014/08/15 职场文书
民族团结演讲稿范文
2014/08/27 职场文书
一个独生女的故事观后感
2015/06/04 职场文书
python操作xlsx格式文件并读取
2021/06/02 Python
Python闭包的定义和使用方法
2022/04/11 Python