讲解Java异步编程

大家好!

您可能还记得, 十月份的时候,我们翻译了一篇有趣的文章,介绍如何在Javascript中使用计时器。 它引起了巨大的讨论,根据我们的结果,我们一直希望返回这个主题,并为您提供这种语言的异步编程的详细分析。 我们很高兴我们设法找到了体面的资料并在年底之前出版了。 祝您阅读愉快!

Javascript中的异步编程经历了多阶段的演变:从回调到Promise,再到生成器,再到async/await 。 在每个阶段,对于已经用这种语言屈服的人来说,Java异步编程都得到了一些简化,但是对于初学者来说,它变得更加可怕,因为有必要了解每个范例的细微差别,掌握每个范例的应用,同样重要的是,要理解一切如何运作。

在本文中,我们决定简要回顾一下如何使用回调和Promise,简要介绍了生成器,然后帮助您直观地了解如何精确地安排“内部”使用生成器和async / await进行异步编程。 我们希望通过这种方式,您可以放心地将各种范例正确地应用到适当的位置。

假定读者已经使用了回调,promise和生成器进行异步编程,并且还非常熟悉Javascript中的闭包和循环。

回调地狱

最初,有回调。 Javascript没有同步I / O(以下简称I / O),并且完全不支持阻塞。 因此,为了组织任何I / O或推迟任何操作,选择了这样的策略:将需要异步执行的代码传递给具有推迟执行的功能,该功能在事件循环下面的某个地方启动。 一个回调并不是很糟糕,但是代码会增长,并且回调通常会产生新的回调。 结果是这样的:

 getUserData(function doStuff(e, a) { getMoreUserData(function doMoreStuff(e, b) { getEvenMoreUserData(function doEvenMoreStuff(e, c) { getYetMoreUserData(function doYetMoreStuff(e, c) { console.log('Welcome to callback hell!'); }); }); }); }) 

除了看到这样的分形代码时的颠簸之外,还有一个问题:现在我们将do*Stuff逻辑的控制权委派给其他函数( get*UserData() ),您可能没有源代码,也可能没有确定他们是否正在执行您的回调。 很好,不是吗?

承诺

Promise可以逆转由回调提供的控制权,并有助于在平滑链中解开一堆回调。
现在,前面的示例可以转换为如下形式:

 getUserData() .then(getUserData) .then(doMoreStuff) .then(getEvenMoreUserData) .then(doEvenMoreStuff) .then(getYetMoreUserData) .then(doYetMoreStuff); 

已经不那么丑了吧?

但是,让我! 让我们看一个更重要的(但仍人为设计的)回调示例:

 // ,     fetchJson(),   GET   , //    :         ,     –   // . function fetchJson(url, callback) { ... } fetchJson('/api/user/self', function(e, user) { fetchJson('/api/interests?userId=' + user.id, function(e, interests) { var recommendations = []; interests.forEach(function () { fetchJson('/api/recommendations?topic=' + interest, function(e, recommendation) { recommendations.push(recommendation); if (recommendations.length == interests.length) { render(profile, interests, recommendations); } }); }); }); }); 

因此,我们选择用户的个人资料,然后选择他的兴趣,然后根据他的兴趣选择推荐,最后,在收集了所有推荐后,我们将显示该页面。 这样的一组回调可能很值得骄傲,但是尽管如此,它还是有些毛茸茸的。 没事,在这里应用承诺-一切都会成功。 对不对

让我们更改fetchJson()方法,使其返回promise,而不是接受回调。 承诺由解析为JSON格式的响应主体解析。

 fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (interests) { return Promise.all[interests.map(i => fetchJson('/api/recommendations?topic=' + i))]; }) .then(function (recommendations) { render(user, interests, recommendations); }); 

好吧 现在这段代码有什么问题?

糟糕!
我们无法访问此链的上一个功能的个人资料或兴趣吗? 所以什么都行不通! 怎么办 让我们尝试嵌套的promise:

 fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id) .then(interests => { user: user, interests: interests }); }) .then(function (blob) { return Promise.all[blob.interests.map(i => fetchJson('/api/recommendations?topic=' + i))] .then(recommendations => { user: blob.user, interests: blob.interests, recommendations: recommendations }); }) .then(function (bigBlob) { render(bigBlob.user, bigBlob.interests, bigBlob.recommendations); }); 

是的...现在看起来比我们希望的更加笨拙。 难道是因为如此疯狂的嵌套娃娃,我们最后但并非最不重要的,试图摆脱回调的麻烦吗? 现在该怎么办?

可以依靠闭包对代码进行一些梳理:

 //   ,     var user, interests; fetchJson('/api/user/self') .then(function (fetchedUser) { user = fetchedUser; return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (fetchedInterests) { interests = fetchedInterests; return Promise.all(interests.map(i => fetchJson('/api/recommendations?topic=' + i))); }) .then(function (recomendations) { render(user, interests, recommendations); }) .then(function () { console.log('We are done!'); }); 

是的,现在几乎所有内容都是我们想要的方式,但有一个怪癖。 注意我们如何在fetchedUserfetchedInterests中而不是userinterests回调中调用参数? 如果是这样的话,那么你很观察!

这种方法的缺陷是:您需要非常非常小心,不要以与将在闭包中使用的缓存变量相同的方式命名内部函数中的任何内容。 即使您有避免阴影的诀窍,在闭包中引用如此高的变量仍然看起来很危险,这绝对不是一件好事。

异步发电机

发电机将为您提供帮助! 如果您使用发电机,那么所有的刺激都会消失。 只是魔术。 事实是。 只看一下:

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

仅此而已。 会的。 当您看到生成器多么漂亮时,您不会流泪,您是否后悔自己如此短视并且甚至在生成器出现之前就开始学习Javascript? 我承认,这样的想法曾经造访过我。
但是...这一切如何运作? 真的是魔术吗?

当然!..不 我们转向曝光。

发电机

在我们的示例中,生成器似乎易于使用,但实际上它们中有很多东西。 要了解有关异步生成器的更多信息,您需要更好地了解生成器如何工作以及它们如何提供异步执行(似乎是同步的)。

顾名思义,生成器生成以下值:

 function* counts(start) { yield start + 1; yield start + 2; yield start + 3; return start + 4; } const counter = counts(0); console.log(counter.next()); // {value: 1, done: false} console.log(counter.next()); // {value: 2, done: false} console.log(counter.next()); // {value: 3, done: false} console.log(counter.next()); // {value: 4, done: true} console.log(counter.next()); // {value: undefined, done: true} 

这很简单,但是无论如何,让我们谈谈这里发生的事情:

  1. const counter = counts(); -初始化生成器并将其保存在变量计数器中。 生成器处于混乱状态;生成器主体中的代码尚未执行。
  2. console.log(counter.next()); -解释输出( yield )1,之后将1作为value返回,并且done结果为false ,因为输出未在此处结束
  3. console.log(counter.next()); -现在2!
  4. console.log(counter.next()); -现在3! 完了 没事吧 不行 在步骤yield 3;暂停执行yield 3; 要完成此操作,您需要再次调用next()。
  5. console.log(counter.next()); -现在4,它返回了,但是没有发出,所以现在我们退出该函数,一切就绪。
  6. console.log(counter.next()); -发电机已经完成工作! 除了“一切已完成”,他没有其他要报告的内容。

因此,我们弄清楚了发电机是如何工作的! 但是,等等,这真是一个令人震惊的事实:生成器不仅可以散发出价值,而且还可以吞噬它们!

 function* printer() { console.log("We are starting!"); console.log(yield); console.log(yield); console.log(yield); console.log("We are done!"); } const counter = printer(); counter.next(1); // ! counter.next(2); // 2 counter.next(3); // 3 counter.next(4); // 4\n ! counter.next(5); //    

ew,什么?! 生成器使用值,而不是生成它们。 这怎么可能?

秘密在于next功能。 它不仅从生成器返回值,而且还可以将它们返回到生成器。 如果您告诉next()参数,那么生成器当前正在等待的yield操作实际上会导致参数。 这就是为什么第一个counter.next(1)注册为undefined 。 根本没有引渡可以解决。

无论如何,好像生成器允许调用代码(过程)和生成器代码(过程)彼此合作,以便它们在执行时互相传递值并互相等待。 实际上,情况是一样的,就好像Java生成器有可能考虑实施协作竞争执行的程序一样,它们也是“协程”。 实际上,很像co() ,对吧?

但是我们不要着急,否则我们将胜过自己。 在这种情况下,重要的是读者必须直观地掌握生成器和异步编程的本质,而做到这一点的最佳方法是自己组装生成器。 不要编写生成器函数并且不要使用已完成的函数,而要自己重新创建生成器函数的内部。

发电机的内部设备-我们产生发电机

好的,我真的不知道在不同的JS运行时中,生成器内部的外观如何。 但这不是那么重要。 生成器对应于该接口。 用于实例化生成器的“构造函数”, next(value? : any)方法,用于命令生成器继续工作并为其提供值,如果发生throw(error)而不是值,则使用另一个throw(error)方法,最后是一个方法return() ,它仍然保持沉默。 如果实现了与接口的一致性,那么一切都很好。

因此,让我们尝试在没有关键字function*纯ES5上构建上述counts()生成器。 现在,您可以忽略throw()并将值传递给next() ,因为该方法不接受任何输入。 怎么做?

但是在Javascript中,还有另一种暂停和恢复程序执行的机制:闭包! 看起来熟悉吗?

 function makeCounter() { var count = 1; return function () { return count++; } } var counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 

如果您以前使用过闭包,那么我确定您已经写过类似的东西。 由makeCounter返回的函数可以生成无限的数字序列,就像生成器一样。

但是,此函数与生成器接口不对应,并且不能在我们的示例中直接使用counts()返回4个值并退出。 通用的编写类似生成器函数的方法需要什么?

封盖,状态机和辛苦的工作!

 function counts(start) { let state = 0; let done = false; function go() { let result; switch (state) { case 0: result = start + 1; state = 1; break; case 1: result = start + 2; state = 2; break; case 2: result = start + 3; state = 3; break; case 3: result = start + 4; done = true; state = -1; break; default: break; } return {done: done, value: result}; } return { next: go } } const counter = counts(0); console.log(counter.next()); // {value: 1, done: false} console.log(counter.next()); // {value: 2, done: false} console.log(counter.next()); // {value: 3, done: false} console.log(counter.next()); // {value: 4, done: true} console.log(counter.next()); // {value: undefined, done: true} 

通过运行此代码,您将看到与生成器版本相同的结果。 好吧
因此,我们整理出了发电机的发电侧; 让我们分析一下消费吗?
实际上,并没有太多差异。

 function printer(start) { let state = 0; let done = false; function go(input) { let result; switch (state) { case 0: console.log("We are starting!"); state = 1; break; case 1: console.log(input); state = 2; break; case 2: console.log(input); state = 3; break; case 3: console.log(input); console.log("We are done!"); done = true; state = -1; break; default: break; return {done: done, value: result}; } } return { next: go } } const counter = printer(); counter.next(1); // ! counter.next(2); // 2 counter.next(3); // 3 counter.next(4); // 4 counter.next(5); // ! 

所需要做的只是将input添加为go参数,然后将值输出。 再次看起来像魔术吗? 几乎像发电机吗?

万岁! 因此,我们将发电机重新创建为供应商和消费者。 为什么不尝试在其中组合这些功能? 这是生成器的另一个非常人为的示例:

 function* adder(initialValue) { let sum = initialValue; while (true) { sum += yield sum; } } 

由于我们都是生成器方面的专家,因此我们知道该生成器将next(value)给出的值添加到sum ,然后返回sum。 它完全符合我们的预期:

 const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.next(3)); // 6 

好酷 现在,让我们将此接口编写为普通功能!

 function adder(initialValue) { let state = 'initial'; let done = false; let sum = initialValue; function go(input) { let result; switch (state) { case 'initial': result = initialValue; state = 'loop'; break; case 'loop': sum += input; result = sum; state = 'loop'; break; default: break; } return {done: done, value: result}; } return { next: go } } function runner() { const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.next(3)); // 6 } runner(); 

哇,我们已经实施了完整的协程。

关于发电机的运行,还有一些要讨论的东西。 异常如何工作? 除了生成器内部发生的异常之外,一切都很简单: next()将使异常到达调用者,并且生成器将死亡。 将异常传递给生成器是通过throw()方法完成的,在上面我们省略了。

让我们用一个很酷的新功能丰富我们的终结器。 如果调用方将异常传递给生成器,它将返回到总和的最后一个值。

 function* adder(initialValue) { let sum = initialValue; let lastSum = initialValue; let temp; while (true) { try { temp = sum; sum += yield sum; lastSum = temp; } catch (e) { sum = lastSum; } } } const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.throw(new Error('BOO)!'))); // 1 console.log(add.next(4)); // 5 

编程问题-发电机错误渗透率

同志,我们如何实现throw()?

容易! 错误只是另一个价值。 我们可以将其传递给go()作为下一个参数。 实际上,这里需要谨慎。 调用throw(e)yield将像我们编写了throw e一样工作。 这意味着我们必须在状态机的每个状态下检查错误,如果无法处理错误,则使程序崩溃。

让我们从终止符的先前实现开始,

模式

解决方案

oom! 我们实现了一组协程,它们能够像真正的生成器一样相互传递消息和异常。

但是情况正在恶化,不是吗? 状态机的实现越来越远离生成器的实现。 不仅如此,由于错误处理,代码中乱七八糟。 由于我们这里有很长的while ,因此代码变得更加复杂。 要转换while您需要将其“解缠”为状态。 因此,我们的案例1实际上包括while 2.5次迭代,因为yield会在中间中断。 最后,如果生成器中没有try/catch来处理此异常,则必须添加额外的代码以从调用方推送异常,反之亦然。

你做到了! 我们已经完成了对生成器实施方案的详细分析,希望您已经更好地了解了生成器的工作方式。 在干残渣中:

  • 生成器可以生成值,使用值或同时生成两者。
  • 生成器的状态可以暂停(状态,状态机,捕获?)
  • 调用者和生成器允许您形成一组corutin,彼此交互
  • 异常向任何方向转发。

现在我们对生成器有了更好的了解,我提出了一种潜在的关于生成器的推理方式:这些是语法构造,您可以用它们编写竞争执行的过程,这些过程通过一次一次传递值的通道相互传递值( yield )。 在下一节中,这将派上用场,我们将从协程产生一个co()实现。

Corutin控制反转

现在我们已经熟练使用生成器,让我们考虑一下如何在异步编程中使用它们。 如果我们可以这样编写生成器,这并不意味着生成器中的promise将自动得到解决。 但是,等等,发电机并不是要自己工作。 它们必须与另一个程序(主程序)交互,该程序调用.next().throw()

如果我们将业务逻辑放在主程序中而不是主程序中怎么办? 每当针对业务逻辑发生某个异步值(例如承诺)时,生成器都会说:“我不想弄乱这个废话,在解决时唤醒我”,将暂停并向服务过程发出承诺。 维护程序:“好,我稍后再给您打电话。” 之后,它会使用此承诺注册一个回调,然后退出并等待,直到有可能触发一个事件周期(即,承诺解决时)。 发生这种情况时,该过程将宣布“嘿,轮到您了”,然后通过.next()将值发送.next()休眠的生成器。 她将等待生成器完成其工作,与此同时,它将执行其他异步操作……等等。 您听了一个令人遗憾的故事,该故事讲述了该过程如何在生成器的服务下继续下去。

因此,回到主要主题。 既然我们知道生成器和Promise的工作方式,那么创建这样的“服务程序”将不难。 服务过程本身将按照承诺进行竞争执行,实例化并维护生成器,然后使用.then()回调返回到主过程的最终结果。

接下来,让我们返回co()程序并进行更详细的讨论。 co()是一个服务过程,需要执行从属劳动,因此生成器只能使用同步值。 已经看起来更加合乎逻辑了,对吧?

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

, , co() , .

— co()

太好了! co() , , . co()

  1. ,
  2. .next() , {done: false, value: [a Promise]}
  3. ( ), .next() ,
  4. , 4
  5. - {done: true, value: ...} , , co()

, co(), :



 function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } co(function* asyncAdds(initialValue) { console.log(yield deferred(initialValue + 1)); console.log(yield deferred(initialValue + 2)); console.log(yield deferred(initialValue + 3)); }); function co(generator) { return new Promise((resolve, reject) => { //   }); } 



, ? - 10 co() , . , . ?

– co()

, , , , co() . , .throw() .



 function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } function deferReject(e) { return new Promise((resolve, reject) => reject(e)); } co(function* asyncAdds() { console.log(yield deferred(1)); try { console.log(yield deferredError(new Error('To fail, or to not fail.'))); } catch (e) { console.log('To not fail!'); } console.log(yield deferred(3)); }); function co(generator) { return new Promise((resolve, reject) => { //   }); } 



. , , .next() onResolve() . onReject() , .throw() . try/catch , , try/catch .

, co() ! ! co() , , , . , ?

: async/await

co() . - , async/await? — ! , async await .

async , await , yield . await , async . async - .

, async/await , , - co() async yield await , * , .

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

:

 async function () { var user = await fetchJson('/api/user/self'); var interests = await fetchJson('/api/user/interests?userId=' + self.id); var recommendations = await Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }(); 

, :

  • co() . async , . async co() co.wrap() .
  • co() ( yield ) , , . async ( await ) .



Javascript , , « » co() , , , async/await . ? 对啊

Source: https://habr.com/ru/post/zh-CN434360/


All Articles