前言
这篇文章是关于如何指定JavaScript中的并发操作的顺序问题
我们经常不关心并发操作的完成顺序。例如,假设我们有一个Web服务器处理来自客户端的请求。每个请求所花费的时间可能会有所不同,响应发回的顺序也无关紧要
然而,在我们关心订单的情况下出现这种情况并不罕见。有时当我们执行一个异步操作时,我们需要它在完成下一个操作之前运行完成(笔者面试时就遇到过此问题)。这就是这篇文章的内容
现代JavaScript中基本上有三种方法可以做到这一点(使用异步调用的几种方式)
-
最古老的方法是只使用回调。这种方法在概念上可能是最纯粹的,但它也可能导致所谓的回调地狱(至于怎么避免它可以戳****回调地狱链接****):一种意大利式面条代码,难以理解和调试
-
另一种方法是使用承诺(promise),这允许以更程序化的方式指定操作序列(可以让程序代码按照指定的顺序先后执行)
-
最近,JavaScript引入了异步并等待(Aync / Await),这是Es7新增的方法
这些方法不是相互排斥的,而是相辅相成的:异步/等待基于承诺建立,承诺使用回调
我将展示一个以三种方式实现的简单示例,首先是回调,然后是承诺,最后是异步/等待
对于这个例子,我们有一个假设的应用程序,可以自动将一些定制软件同时部署到多台计算机。假设每个部署都有3个步骤
-
安装操作系统
-
部署我们的软件
-
运行测试
对于任何给定的目标,这3个操作需要按顺序运行,但它们可以跨目标同时执行
并发执行
首先让我们看看一些并发运行这些任务的代码,而不用序列化它们(反序列化)
/**
*
* @authors 随笔川迹 (itclanCode@163.com)
* @date 2018-03-19 17:10:23
* @version $Id$
* @weChatPublicId ((itclanCoder))
* @QQGroup ((643468880))
* @PersonWeChatId ((suibichuanji))
* @PersonQQ ((1046678249))
* @link ((https://juejin.im/post/5a005392518825295f5d53c8))
* @describe 反序列化
*/
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
// 安装操作系统
const installOS = () => asyncTask("Install OS:安装操作系统")
// 部署软件
const deploySoftware = () => asyncTask("Deploy Software:部署软件")
// 运行测试
const runTests = () => asyncTask("Run Test:运行测试")
// 任务执行完成异步
const taskDone = (name) => console.log(`Completed async "${name}"`)
// 异步任务
const asyncTask = (name) => {
console.log(`Started async "${name}"...`)
setTimeout(() => taskDone(name), random(1, 3) * 1000)
console.log(`Returning from async "${name}"`)
}
const main = () => {
installOS() // 安装操作系统函数调用
deploySoftware() // 部署软件
runTests() // 运行测试
}
main() // 函数执行
并发执行.gif
我们通过调用asyncTask来模拟我们的操作,它使用setTimeout在完成任务之前等待1到3秒,然后调用taskDone
下面是一个典型的输出(每次运行代码时实际的顺序都会改变)
D:\Front end technology books library\Translation-of-forieign-language-technology\10如何序列化JavaScript中的并发操作:回调,承诺和异步等待\js>node unserialized.
js
Started async "Install OS:安装操作系统"...
Returning from async "Install OS:安装操作系统"
Started async "Deploy Software:部署软件"...
Returning from async "Deploy Software:部署软件"
Started async "Run Test:运行测试"...
Returning from async "Run Test:运行测试"
Completed async "Deploy Software:部署软件"
Completed async "Install OS:安装操作系统"
Completed async "Run Test:运行测试"
正如我们所看到的,这并不是很好:我们在操作系统安装完成之前部署了我们的软件
使用回调
好吧,让我们使用回调来解决这个问题
/**
*
* @authors 随笔川迹 (itclanCode@163.com)
* @date 2018-03-19 17:40:12
* @version $Id$
* @weChatPublicId ((itclanCoder))
* @QQGroup ((643468880))
* @PersonWeChatId ((suibichuanji))
* @PersonQQ ((1046678249))
* @link ((https://juejin.im/post/5a005392518825295f5d53c8))
* @describe 使用回调函数
*/
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
const installOS = (nextTask) => asyncTask("Install OS:安装操作系统", nextTask)
const deploySoftware = (nextTask) => asyncTask("Deploy Software:部署软件", nextTask)
const runTests = () => asyncTask("Run Tests:运行测试")
const taskDone = (name, nextTask) => {
console.log(`Completed async "${name}"`)
if (nextTask) {
nextTask()
}
}
const asyncTask = (name, nextTask) => {
console.log(`Started async "${name}"...`)
setTimeout(() => taskDone(name, nextTask),
random(1, 3) * 1000)
console.log(`Returning from async "${name}"`)
}
const main = () => {
installOS(() => deploySoftware(() => runTests()))
}
main()
callback.gif
我们使用一个回调来调用installOS,一旦installOS完成,它将运行deploySoftware。一旦deploySoftware完成,它将调用它自己的回调函数runTests
每次操作完成时,taskDone函数都会将操作记录为已完成并开始下一个操作
让我们看看它是否有效
D:\Front end technology books library\Translation-of-forieign-language-technology\10如何序列化JavaScript中的并发操作:回调,承诺和异步等待\js>node callback.js
Started async "Install OS:安装操作系统"...
Returning from async "Install OS:安装操作系统"
Completed async "Install OS:安装操作系统"
Started async "Deploy Software:部署软件"...
Returning from async "Deploy Software:部署软件"
Completed async "Deploy Software:部署软件"
Started async "Run Tests:运行测试"...
Returning from async "Run Tests:运行测试"
Completed async "Run Tests:运行测试"
好,我们可以看到每一步都按顺序进行
但是,这个代码仍然有很多问题。即使有这样一个简单的例子,我认为代码有点难以阅读
错误处理也许并不像它可能那样简单。例如,让我们修改deploySoftware函数以引发错误
const deploySoftware = (nextTask) => {
throw new Error('deploying software failed')
asyncTask("Deploy Software",
nextTask)
}
让我们尝试用异常处理程序天真地包装我们的主要调用
const main = ()=> {
try {
installOS(()=>deploySoftware(()=>runTests()))
} catch (error) {
console.log(`*** Error caught: '${error}' ***`)
}
}
修改deploySoftware函数以引发错误.gif
不幸的是,catch块永远不会执行,异常最终会弹出堆栈:
C:\dev\asyncio\callbacks.js:7
throw new Error('deploying software failed')
^
Error: deploying software failed
at deploySoftware (C:\dev\asyncio\callbacks.js:7:8)
at installOS (C:\dev\asyncio\callbacks.js:30:17)
at taskDone (C:\dev\asyncio\callbacks.js:17:3)
at Timeout.setTimeout [as _onTimeout] (C:\dev\asyncio\callbacks.js:23:19)
at ontimeout (timers.js:458:11)
at tryOnTimeout (timers.js:296:5)
at Timer.listOnTimeout (timers.js:259:5)
问题在于,在发生错误时installOS已经返回。显然,一些额外的努力将不得不处理错误。我将把它作为读者的练习。正如我们将看到的,承诺会使错误处理更容易
使用承诺
让我们稍微修改我们的代码以使用
/**
*
* @authors 随笔川迹 (itclanCode@163.com)
* @date 2018-03-19 17:51:10
* @version $Id$
* @weChatPublicId ((itclanCoder))
* @QQGroup ((643468880))
* @PersonWeChatId ((suibichuanji))
* @PersonQQ ((1046678249))
* @link ((https://juejin.im/post/5a005392518825295f5d53c8))
* @describe promises
*/
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
const installOS = () => asyncTask("Install OS:安装操作系统")
const deploySoftware = () => asyncTask("Deploy Software:部署软件")
const runTests = () => asyncTask("Run Tests:运行测试")
const taskDone = (name) => console.log(`Completed async "${name}"`)
const asyncTask = (name) => {
console.log(`Started async "${name}"...`)
// 使用承诺,Promis是一个构造器函数
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve(name), random(1, 3) * 1000)
})
console.log(`Returning from async "${name}"`)
return promise
}
const main = () => {
installOS().then(name => {
taskDone(name)
return deploySoftware()
}).then(name => {
taskDone(name)
return runTests()
}).then(taskDone)
}
main()
我们可以看到我们已经能够从我们的任务中删除nextTask回调。现在每个任务都可以独立运行。将它们连接在一起的工作已经进入主流
为了实现这一点,我们修改了asyncTask来返回一个承诺。
这个怎么用?当异步操作的结果准备就绪时,我们调用promise的resolve回调函数。承诺有一个方法,然后可以提供一个回调作为参数。当我们触发解析函数时,它会运行我们提供给promise的then方法的回调函数
这使我们能够序列化我们的异步操作。当installOS完成时,我们提供一个回调,然后调用deploySoftware。 deploySoftware函数返回另一个承诺,该承诺通过调用runTests来解决。当runTests完成时,我们只提供一个简单的回调函数,只记录完成的工作
通过从我们的任务中返回promise对象,我们可以将我们想要完成的任务依次链接在一起
我认为这个代码比回调示例更容易阅读
这也使得处理错误变得更容易。让我们再次修改deploySoftware以引发错误
const deploySoftware = () => {
throw new Error('"Deploy Software" failed')
return asyncTask("Deploy Software")
}
承诺有一个方便的方法来处理这个问题。我们只需将一个catch方法追加到我们的promise链的末尾:
const main = ()=> {
installOS().then(name=>{
taskDone(name)
return deploySoftware()
}).then(name=>{
taskDone(name)
return runTests()
}).then(taskDone)
.catch((error)=>console.log(`*** Error caught: '${error}' ***`))
}
如果在尝试解析承诺时发生错误,则会调用此catch方法
让我们看看当我们运行这个代码时会发生什么:
C:\dev\asyncio>node serialize_with_promises.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
*** Error caught: 'Error: "Deploy Software" failed' ***
很好,我们发现了我们的错误!我认为这看起来比纯回调示例更直接
使用异步/等待
Aync / Await是我们要看的最后一个例子。该语法与承诺一起使序列化异步操作看起来像普通的同步代码
好吧,不要再等待了 - 让我们修改我们以前的示例以使用async / await
/**
*
* @authors 随笔川迹 (itclanCode@163.com)
* @date 2018-03-19 18:36:25
* @version $Id$
* @weChatPublicId ((itclanCoder))
* @QQGroup ((643468880))
* @PersonWeChatId ((suibichuanji))
* @PersonQQ ((1046678249))
* @link ((https://juejin.im/post/5a005392518825295f5d53c8))
* @describe 使用异步/等待
*/
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
const installOS = () => asyncTask("Install OS:安装操作系统")
const deploySoftware = () => asyncTask("Deploy Software:部署软件")
const runTests = () => asyncTask("Run Tests:运行测试")
const taskDone = (name) => console.log(`Completed async "${name}"`)
const asyncTask = (name) => {
console.log(`Started async "${name}"...`)
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve(name), random(1, 3) * 1000)
})
console.log(`Returning from async "${name}"`)
return promise
}
const main = async() => {
const installOSResult = await installOS()
taskDone(installOSResult)
const deploySoftwareResult = await deploySoftware()
taskDone(deploySoftwareResult)
const runTestsResult = await runTests()
taskDone(runTestsResult)
}
main()
我们做了什么改变?首先,我们将main标记为异步函数。接下来,我们将等待异步操作的结果,而不是承诺链
await会自动等待函数返回的promise来自行解析。它像我们今天看到的所有代码一样是非阻塞的,所以其他的东西可以在等待表达式的同时运行。然而,在promise等待解决之前,下一行代码将不会运行。任何包含await的函数都必须标记为异步
让我们运行这段代码,看看结果
C:\dev\asyncio>node async_await.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
Started async "Deploy Software"...
Returning from async "Deploy Software"
Completed async "Deploy Software"
Started async "Run Tests"...
Returning from async "Run Tests"
Completed async "Run Tests"
async-await.gif
它可以工作我们可以再做一次小改动,导致deploySoftware发出错误
const deploySoftware = () => {
throw new Error('"Deploy Software" failed')
return asyncTask("Deploy Software")
}
让我们看看我们如何处理这个问题
const main = async ()=> {
try {
const installOSResult = await installOS()
taskDone(installOSResult)
const deploySoftwareResult = await deploySoftware()
taskDone(deploySoftwareResult)
const runTestsResult = await runTests()
taskDone(runTestsResult)
} catch(error) {
console.log(`*** Error caught: '${error}' ***`)
}
}
这工作
C:\dev\asyncio>node async_await.js
Started async "Install OS"...
Returning from async "Install OS"
Completed async "Install OS"
*** Error caught: 'Error: "Deploy Software" failed' ***
正如我们所看到的,async / await可以使用标准的同步语法来处理由异步代码产生的任何错误
在结束这篇文章之前,我想我只是添加一些代码来表明真的是非阻塞的。我们添加一个计时器,它将与我们的其他代码同时运行
const timer = () => setInterval(()=>console.log('tick'), 500)
const main = async ()=> {
const t = timer()
const installOSResult = await installOS()
taskDone(installOSResult)
const deploySoftwareResult = await deploySoftware()
taskDone(deploySoftwareResult)
const runTestsResult = await runTests()
taskDone(runTestsResult)
clearInterval(t)
}
结果如下
C:\dev\asyncio>node async_await.js
Started async "Install OS"...
Returning from async "Install OS"
tick
Completed async "Install OS"
Started async "Deploy Software"...
Returning from async "Deploy Software"
tick
tick
tick
tick
tick
tick
Completed async "Deploy Software"
Started async "Run Tests"...
Returning from async "Run Tests"
tick
tick
Completed async "Run Tests"
我们可以确认计时器在我们等待任务时继续运行。大
在使用await时,我认为记住这很有帮助,它大致相当于从异步调用中获得承诺并调用它的then方法
一些疑难问题:你必须在标有异步的功能中使用await。这意味着你无法等待顶级JavaScript代码中的某些内容。编写顶级代码时,可以使用promises的then语法代替,也可以将代码封装在标记为异步的自执行函数中
总结
整篇文章主要是针对如何序列化js中的并发操作,其中序列化也就是编码方式,用什么的方式将要用的方式给存起来,方便日后调用,比如数字转换为二进制(数字对象.toString(2)),数据类型转换,而反过来就是反序列化,对应的就是解码,把先前序列化存起来的数据用起来,例如,将json字符串转换为json对象
data.parseJSON()或者JSON.Parse(data),而并发操作指的是多任务同时进行,但任务的先后,可以通过回调,承诺,异步等待方式控制代码的执行顺序,当然对于序列化与反序列化,文中并没有提及,其实将序列化理解为编码(类似编译),而反序列化理解为解码破译(反编译)就可以了
网友评论