当前位置: 首页 > Web前端 > vue.js

【技术翻译】用现代JavaScript编写异步任务

时间:2023-04-01 01:22:51 vue.js

这周我会翻译一些技术文章。这次打算翻译三篇文章如下:04.翻译:用Nuxt生成静态网站(GenerateStaticWebsiteswithNuxt)05.翻译:网页内容如何影响电池电量(HowWebContentCanAffectPowerUsage)06.翻译:WritingasynchronoustasksinmodernJavaScript(https://web.dev/off-main-thread/)我翻译的技术文章都放在一个github仓库里,如果觉得有用请点star收藏。我为什么要创建这个git存储库?目的是通过翻译国外与Web相关的技术文章,学习和跟进Web开发的新思想、新技术。git仓库地址:https://github.com/yzsunlei/javascript-article-translate在这篇文章中,我们将探讨JavaScript在过去围绕异步执行的演变,以及它如何改变了我们编写和阅读代码的方式。我们将从Web开发的开端开始,一直到现代异步模式的示例。JavaScript作为一种编程语言有两个主要特征,这两个特征对于理解我们的代码是如何工作的都很重要。首先是它的同步特性,这意味着代码将在您阅读时几乎逐行运行,其次,它是单线程的,任何时候只有一个命令在执行。随着语言的发展,出现了新的模块以允许异步执行。开发人员尝试使用不同的方法来解决更复杂的算法和数据流,从而导致围绕它们出现新的接口和模式。同步执行和观察者模式正如简介中提到的,JavaScript通常会逐行运行您编写的代码。即使在最初几年,该语言也有例外,尽管它们很少,而且您可能已经知道它们:HTTP请求、DOM事件和时间间隔。constbutton=document.querySelector('button');//观察用户交互button.addEventListener('click',function(e){console.log('userclickjusthappened!');})ifaddingeventlistenerlistener(例如,单击一个元素并触发用户交互),JavaScript引擎会将任务排队等待事件侦听器回调,但会继续执行当前在其堆栈中的任务。在那里完成调用后,它现在将运行侦听器的回调。这种行为类似于网络请求和计时器发生的情况,它们是Web开发人员访问异步执行的第一个模块。虽然这些是JavaScript中通常同步执行的例外情况,但了解该语言仍然是单线程的至关重要,虽然它可以将任务排队,异步运行它们然后返回主线程,但它只能一次执行它们一段代码。我们的工具书,其中AllaKholmatova探讨了如何创建有效且可维护的设计系统来设计出色的数字产品。认识DesignSystems,了解常见的陷阱、陷阱和Alla多年来吸取的经验教训。例如,让我们发送一个网络请求。varrequest=newXMLHttpRequest();request.open('GET','//some.api.at/server',true);//观察服务器响应request.onreadystatechange=function(){if(request.readyState===4&&xhr.status===200){console.log(request.responseText);}}request.send();当服务器返回时,分配给该方法的任务将onreadystatechange放入队列中(代码在主线程继续执行)。注意:解释JavaScript引擎如何排队任务和处理执行线程是一个复杂的主题,可能值得一读。尽管如此,我还是建议您查看“事件循环到底是什么?”PhillipRoberts的帮助可以帮助您更好地理解它。在上述每种情况下,我们都在响应外部事件。达到某个时间间隔、用户操作或服务器响应。我们本身不能创建异步任务,我们总是观察发生在我们范围之外的事件。这就是为什么这种样板代码被称为“观察者模式”,在这种情况下,addEventListener可以更好地由接口表示。很快,暴露这种模式的事件库或框架蓬勃发展。Node.JS和事件触发器的一个很好的例子是Node.js,该页面将自己描述为“异步事件驱动的JavaScript运行时”,因此事件触发器和回调是一等公民。它甚至已经实现了带有EventEmitter的构造函数。constEventEmitter=require('事件');constemitter=newEventEmitter();//响应eventsemitter.on('greeting',(message)=>console.log(message));//发送eventsemitter.emit('greeting','Hithere!');这不仅是异步执行的通用方法,也是其生态系统的核心模式和约定。Node.js开创了在不同环境中编写JavaScript的新时代,甚至是在网络之外。因此,其他异步情况也是可能的,例如创建新目录或写入文件。const{mkdir,writeFile}=require('fs');conststyles='body{背景:#ffdead;}';mkdir('./assets/',(error)=>{if(!error){writeFile('assets/main.css',styles,'utf-8',(error)=>{if(!error)console.log('stylesheetcreated');})}})你可能注意到了,回调错误函数的第一个参数是,如果需要响应数据,它被用作第二个参数。这被称为“错误优先回调模式”,它成为作者和贡献者为他们自己的包和库采用的约定。Promises和无尽的回调链随着Web开发面临更复杂的问题需要解决,对更好的异步工件的需求就出现了。如果我们查看最后一个代码片段,我们会看到重复的回调链随着任务数量的增加而扩展性很差。例如,让我们只添加两个步骤,文件读取和样式预处理。const{mkdir,writeFile,readFile}=require('fs');constless=require('less')readFile('./main.less','utf-8',(error,data)=>{if(error)throwerrorless.render(data,(lessError,output)=>{if(lessError)throwlessErrormkdir('./assets/',(dirError)=>{if(dirError)throwdirErrorwriteFile('assets/main.css',output.css,'utf-8',(writeError)=>{if(writeError)throwwriteErrorconsole.log('stylesheetcreated');})})})})我们可以看到,随着编写的程序变得越来越复杂,代码变得越来越晦涩回调链和重复的错误处理。Promises、Wrappers和ChainingPatterns当Promise作为JavaScript语言的一项新特性首次宣布时并没有引起太多关注,它们并不是一个新概念,因为其他语言在几十年前就已经实现了类似的特性。事实是,他们发现我参与的大多数项目的语义和结构自发布以来发生了很大变化。Promises不仅为开发人员引入了编写异步代码的内置解决方案,而且还开辟了Web开发的新阶段,作为构建Web开发新功能(例如Web规范)的基础。从回调方法迁移到基于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,定义错误对象时在Called中reject,屏蔽回调。当一个方法返回一个Promise对象时,我们可以通过将一个函数传递给then来跟踪它的成功解析,该函数的参数是Promise解析到的值,在本例中为数据。如果在catch方法期间出现错误,将调用该函数(如果存在)。注意:如果您需要更深入地了解Promises的工作原理,我推荐JakeArchibald在Google的Web开发博客上发表的文章“JavaScriptPromises:简介”。现在我们可以使用这些新方法并避免回调链。asyncRead('./main.less','utf-8').then(data=>console.log('文件内容',data)).catch(error=>console.error('出错了',error))有一种创建异步任务的方法和一个清晰的界面来跟踪它们的可能结果,使行业远离观察者模式。基于Promise的代码似乎是解决不可读和容易出错的代码的方法。由于更好的语法或更清晰的错误消息突出显示有助于编码,更容易推理的代码变得更可预测,并且执行路径更好,更容易让开发人员捕获可能的代码陷阱。由于Promises在社区中的流行,Node.js很快发布了其I/O方法的内置版本以返回Promise对象,例如从fs.promises导入文件。它甚至提供了一个promisify实用程序,用于包装所有遵循错误优先回调模式的函数,并将它们转换为基于Promise的函数。但是Promises可以在所有情况下提供帮助吗?让我们重新想象用Promises编写的样式预处理任务。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))代码中的冗余显着减少,特别是在我们现在依赖的错误处理方面在catches上,但Promises以某种方式无法提供与操作串联直接相关的清晰代码缩进。这其实就是在调用then之后的第一条语句上实现的readFile。在这些行之后发生的事情是需要创建一个新的范围,我们可以在其中首先创建目录,然后将结果写入文件。这会导致缩进的节奏中断,让人很难一眼就确定指令的顺序。解决这个问题的一种方法是预先处理这个问题的自定义方法,并允许方法正确连接,但是我们会在代码中引入更多的复杂性,这些代码似乎已经具有完成任务所需的功能。注意:这是一个示例程序,我们可以控制一些方法,它们都遵循行业惯例,但并非总是如此。通过更复杂的串联或引入不同类型的库,我们可以轻松打破代码风格。令人高兴的是,JavaScript社区再次从其他语言语法中学习并添加了一种符号,在许多情况下可以帮助串联异步任务,而不像同步代码那样令人愉快或直接。async和awaitPromise在执行时被定义为未解析的值,创建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))在async方法内部,我们可以使用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')}processLess()注意:请注意,所有代码都需要移动到await方法中,因为我们不能使用它超出了今天异步函数的范围。每次异步方法找到await语句时,它都会停止执行,直到处理中的值或Promise得到解决。尽管是异步执行,但使用async/await符号的后果很明显,代码看起来好像是异步的,这是我们开发人员更习惯看到和推理的。错误处理呢?为此,我们使用语言中长期存在的语句,tryandcatch。const{mkdir,writeFile,readFile}=require('fs').promises;constless=require('less');asyncfunctionprocessLess(){try{constcontent=awaitreadFile('./main.less','utf-8')constresult=awaitless.render(content)awaitmkdir('./assets')awaitwriteFile('assets/main.css',result.css,'utf-8')}catch(e){console.error(e)}}processLess()请放心,在此过程中出现的任何错误都将由代码处理在此catch语句中。我们在中央位置处理错误,但现在我们有一个易于阅读和遵循的代码。后续有返回值的操作不需要存放在mkdir不打乱代码节奏的变量中;您也不需要创建新的范围来访问后续步骤中结果的值。可以肯定地说,Promises是语言中引入的一个基本模块,是在JavaScript中启用异步/等待符号所必需的,您可以在现代浏览器和最新版本的Node.js中使用它。注意:最近在JSConf上,Node的创建者和第一贡献者RyanDahl后悔没有坚持Promises的早期开发,主要是因为Node的目标是创建事件驱动的服务器和文件管理,而Observer模式更适合这一点。结论将Promises引入Web开发世界的目的是改变我们在代码中对操作进行排队的方式,改变我们推理代码执行的方式以及我们编写库和包的方式。但是摆脱回调链是很难解决的,我不认为在多年习惯观察者模式之后必须通过一种方法,而主要供应商采用的方法并不能帮助我们摆脱困境方式。像Node.js这样的社区。正如NolanLawson在他关于滥用Promise连接的优秀文章中所说,旧的回调惯用语死了!后来,他解释了如何避免这些陷阱。我将Promises视为一个中间步骤,它允许异步任务以自然的方式产生,但不会帮助我们进一步进入更好的代码模式,有时您实际上需要一种更具适应性和改进的语言语法。当我们尝试使用JavaScript来解决更复杂的难题时,我们看到了对更成熟语言的需求,并尝试了我们以前在web上从未见过的架构和模式。我们仍然不知道ECMAScript规范会有多好,因为我们不断将JavaScript治理扩展到网络之外,并试图解决更复杂的难题。现在很难说我们需要从语言中得到什么来真正将这些难题转化为更简单的程序,但我很高兴web和JavaScript本身正在推动事物发展,努力适应挑战和新环境。我觉得JavaScript现在比我十年前开始在浏览器中编写代码时更加异步友好。