如何在现代JavaScript中编写异步任务
前言
在本文中,我们将探讨过去异步执行的JavaScript的演变,以及它是怎样改变我们编写代码的方式的。我们将从最早的Web开发开始,一直到现代异步模式。
作为编程语言,JavaScript有两个主要特征,这两个特征对于理解我们的代码如何工作非常重要。首先是它的同步特性,这意味着代码将逐行运行,其次是单线程,任何时候都仅执行一个命令。
随着语言的发展,允许异步执行的新工件出现在场景中。开发人员在解决更复杂的算法和数据流时尝试了不同的方法,从而导致新的接口和模式出现。
同步执行和观察者模式
如简介中所述,JavaScript通常会逐行运行你编写的代码。即使在最初的几年中,该语言也有这种规则的例外,尽管很少,你可能已经知道了它们:HTTP请求,DOM事件和timeinterval。
如果我们通过添加事件侦听器去响应用户对元素的单击,则无论语言解释器在运行什么,它都会停止,然后运行在侦听器回调中编写的代码,之后再返回正常的流程。
与interval或网络请求相同,addEventListener,setTimeout和XMLHttpRequest是Web开发人员访问异步执行的第一批工件。
尽管这些是JavaScript中同步执行的例外情况,但重要的是你要了解该语言仍然是单线程的。我们可以打破这种同步性,但是解释器仍然每次运行一行代码。
例如检查一个网络请求。
varrequest=newXMLHttpRequest(); request.open('GET','//some.api.at/server',true); //observeforserverresponse request.onreadystatechange=function(){ if(request.readyState===4&&xhr.status===200){ console.log(request.responseText); } } 11request.send();
不管发生什么情况,当服务器恢复运行时,分配给onreadystatechange的方法都会在取回程序的代码序列之前被调用。
对用户交互做出反应时,也会发生类似的情况。
constbutton=document.querySelector('button'); //observeforuserinteraction button.addEventListener('click',function(e){ console.log('userclickjusthappened!'); })
你可能会注意到,我们正在连接一个外部事件并传递一个回调,告诉代码当事件发生时应该怎么做。十多年前,“什么是回调?”是一个非常受期待的面试问题,因为在很多代码库中到处都有这种模式。
在上述每种情况下,我们都在响应外部事件。不管是达到一定的时间间隔、用户操作还是服务器响应。我们本身无法创建异步任务,我们总是观察发生在我们力所能及范围之外的事件。
这就是为什么这种方式的代码被称为观察者模式的原因,在这种情况下,它最好由addEventListener接口来表示。很快,暴露这种模式的事件发送器库或框架开始蓬勃发展。
NODE.JS和事件发送器
Node.js是一个很好的例子,它的官网把自己描述为“异步事件驱动的JavaScript运行时”,所以事件发送器和回调是一等公民。它甚至已经实现了一个EventEmitter构造函数。
constEventEmitter=require('events'); constemitter=newEventEmitter(); //respondtoevents emitter.on('greeting',(message)=>console.log(message)); //sendevents emitter.emit('greeting','Hithere!');
这不仅是通用的异步执行方法,而且是其生态系统的核心模式和惯例。Node.js开辟了一个在不同环境中甚至在web之外编写JavaScript的新时代。当然异步的情况也是可能的,例如创建新目录或写文件。
const{mkdir,writeFile}=require('fs'); conststyles='body{background:#ffdead;}'; mkdir('./assets/',(error)=>{ if(!error){ writeFile('assets/main.css',styles,'utf-8',(error)=>{ if(!error)console.log('stylesheetcreated'); }) } })
你可能会注意到,回调函数将第一个参数接作为error,如果得到了预期的响应数据,则将其作为第二个参数。这就是所谓的错误优先回调模式,它成为作者和贡献者为包和库所做的约定。
Promise和没完没了的回调链
随着Web开发面临的更复杂的问题,出现了对更好的异步工件的需求。如果我们查看最后一个代码段,则会看到重复的回调链,随着任务数量的增加,回调链的扩展效果不佳。
例如,我们仅添加两个步骤,即文件读取和样式预处理。
const{mkdir,writeFile,readFile}=require('fs'); constless=require('less') readFile('./main.less','utf-8',(error,data)=>{ if(error)throwerror less.render(data,(lessError,output)=>{ if(lessError)throwlessError mkdir('./assets/',(dirError)=>{ if(dirError)throwdirError writeFile('assets/main.css',output.css,'utf-8',(writeError)=>{ if(writeError)throwwriteError console.log('stylesheetcreated'); }) }) }) 16})
我们可以看到,由于多个回调链和重复的错误处理,编写程序变得越来越复杂,代码变得更加难以理解。
Promise、包装和链模式
当Promises最初被宣布为JavaScript语言的新成员时,并没有引起太多关注,它们并不是一个新概念,因为其他语言在几十年前就已经实现了类似的实现。事实上自从它出现以来,他们就改变了我从事的大多数项目的语义和结构。
Promises不仅为开发人员引入了用于编写异步代码的内置解决方案,,而且还开辟了Web开发的新阶段,成为Web规范后来的新功能(如fetch)的构建基础。
从回调方法迁移到基于promise的方法在项目(例如库和浏览器)中变得越来越普遍,甚至Node.js也开始缓慢地迁移到它上面。
例如,包装Node的readFile方法:
const{readFile}=require('fs'); constasyncReadFile=(path,options)=>{ returnnewPromise((resolve,reject)=>{ readFile(path,options,(error,data)=>{ if(error)reject(error); elseresolve(data); }) }); }
在这里,我们通过在Promise构造函数内部执行来隐藏回调,方法成功后调用resolve,定义错误对象时调用reject。
当一个方法返回一个 Promise 对象时,我们可以通过将一个函数传递给then来遵循其成功的解析,它的参数是Promise 被解析的值,在这里是data。
如果在方法运行期间抛出错误,则将调用catch函数(如果存在)。
注意:如果你需要更深入地了解Promise的工作原理,建议你看JakeArchibald在Google的web开发博客上写的文章“JavaScriptPromises:简介”。
现在我们可以使用这些新方法并避免回调链。
asyncRead('./main.less','utf-8') .then(data=>console.log('filecontent',data)) .catch(error=>console.error('somethingwentwrong',error))
它具有创建异步任务的原生方法,并以清晰的接口跟踪其可能的结果,这摆脱了观察者模式。基于Promise的代码似乎可以解决可读性差且容易出错的代码。
在更好的语法突出显示和更清晰的错误提示信息对编码过程中提供的帮助下,对于开发人员来说,编写更容易理解的代码变得更具可预测性,并且执行的情况更好,更容易发现可能的陷阱。
Promises的采用在社区中非常普遍,以至于Node.js迅速发布其I/O方法的内置版本以返回Promise对象,例如从fs.promises中导入文件操作。
它甚至提供了一个promisify工具来包装遵循错误优先回调模式的函数,并将其转换为基于Promise的函数。
但是Promise在所有情况下都能提供帮助吗?
让我们重新评估一下用Promise编写的样式预处理任务。
const{mkdir,writeFile,readFile}=require('fs').promises; constless=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; constless=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; constless=require('less') asyncfunctionprocessLess(){ constcontent=awaitreadFile('./main.less','utf-8') constresult=awaitless.render(content) awaitmkdir('./assets') awaitwriteFile('assets/main.css',result.css,'utf-8') } 11processLess()
注意:请注意,我们需要将所有代码移至某个方法中,因为我们无法在异步函数的作用域之外使用await。
每当异步方法找到一个await语句时,它将停止执行,直到promise被解决为止。
尽管是异步执行,但用async/await表示会使代码看起来好像是同步的,这是容易被开发人员阅读和理解的东西。
那么错误处理呢?我们可以用在语言中存在了很久的try和catch。
const{mkdir,writeFile,readFile}=require('fs').promises; constless=require('less') asyncfunctionprocessLess(){ constcontent=awaitreadFile('./main.less','utf-8') constresult=awaitless.render(content) awaitmkdir('./assets') awaitwriteFile('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的创建者和第一贡献者RyanDahl,对在其早期开发中没有遵守Promises表示遗憾,主要是因为Node的目标是创建事件驱动服务器和文件管理,而Observer模式更适合这样。
结论
将Promise引入Web开发的目的是改变我们在代码中顺序操作的方式,并改变了我们理解代码的方式以及编写库和包的方式。
但是摆脱回调链更难解决,我认为在多年来习惯于观察者模式和采用的方法之后,必须将方法传递给then并不能帮助我们摆脱原有的思路,例如Node.js。
正如NolanLawson在他的出色文章“关于Promise级联的错误使用“【https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html】中所述,旧的回调习惯是死硬且顽固的!在文中他解释了如何避免这些陷阱。
我认为Promise是中间步骤,它允许以自然的方式生成异步任务,但并没有帮助我们进一步改进更好的代码模式,有时你需要更适应改进的语言语法。
当尝试使用JavaScript解决更复杂的难题时,我们看到了对更成熟语言的需求,并且我们尝试了以前不曾在网上看到的体系结构和模式。
我们仍然不知道ECMAScript规范在几年后的样子,因为我们一直在将JavaScript治理扩展到web之外,并尝试解决更复杂的难题。
现在很难说我们需要从语言中真正地将这些难题转变成更简单的程序,但是我对Web和JavaScript本身如何推动技术,试图适应挑战和新环境感到满意。与十年前刚刚开始在浏览器中编写代码时相比,我觉得现在JavaScript是“异步友好”的。
到此这篇关于如何在现代JavaScript中编写异步任务的文章就介绍到这了,更多相关JavaScript编写异步任务内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!