为什么异步代码如此令人困惑(以及如何使其变得简单)
强制同步的肮脏黑客
问题不在于异步代码
从一开始就异步编写
如果没有区别怎么办?
异步函数组合
ESNext 提案:管道操作符
MojiScript
MojiScript 异步示例
我需要你的帮助!
概括
为什么 JavaScript 中的异步代码如此复杂和令人困惑?有很多文章和问题,都是人们试图用 Bean 来解决这个问题的。
从 SO 中精心挑选的一些问题...
关于异步的问题和文章有数百个,其中很多听起来像这样:
// How do I do this in JavaScript?
action1();
sleep(1000);
action2();
这是对 JavaScript 工作原理的一个常见误解。
强制同步的肮脏黑客
甚至有肮脏的黑客可以强制同步
不推荐
问题不在于异步代码
我花了很多时间思考 JavaScript,其中一次我突然冒出一个愚蠢的想法。如果问题不是出在异步代码上呢?如果问题实际上是出在同步代码上呢?
同步代码有问题吗?怎么回事?
我经常先同步写代码,然后再尝试异步处理。这是我的错误。
异步代码无法在同步环境中运行。但是,反之则没有问题。
此限制仅适用于同步代码!
从一开始就异步编写
意识到这一点后,我现在知道我应该异步开始我的代码。
因此,如果我要再次解决异步问题,我会像这样开始:
Promise.resolve()
.then(() => action1())
.then(() => sleep(1000))
.then(() => action2())
或与async
和await
...
const main = async () => {
action1()
await sleep(1000)
action2()
}
这个Promise
解决方案……有点啰嗦。async/await
更好一些,但它只是 Promise 链的语法糖。我还得加上async
和await
,希望我能搞定。
有时async/await
可能会令人困惑。例如:这两行代码的作用完全不同。
// await `action()`
await thing().action()
// await `thing()`
(await thing()).action()
以下是Burke Holland最近发表的一篇文章:
如果没有区别怎么办?
所以我开始重新思考……如果异步代码和同步代码之间没有区别会怎样?如果我可以编写代码而不必担心我编写的代码是否是异步的会怎样?如果异步和同步语法完全相同会怎样?这可能吗?
嗯,这意味着我不能使用标准函数,因为它们只是同步的。async/await
也不行。那段代码完全不一样,而且它本身就很复杂。而且 Promise 要求我到处都写then
, then
, then
……
因此,我又开始思考……
异步函数组合
我超爱函数式编程。所以,我开始思考异步函数组合,以及如何将它应用到这个问题上。
如果您是第一次听说函数组合,这里有一些代码可能会对您有所帮助。这是一个典型的(同步)“hello world”函数组合。如果您想了解更多关于函数组合的知识,请阅读这篇文章:函数式 JavaScript:日常使用的函数组合。
const greet = name => `Hello ${name}`
const exclaim = line => `${line}!`
// Without function composition
const sayHello = name =>
exclaim(greet(name))
// With function composition (Ramda)
const sayHello = pipe(
greet,
exclaim
)
这里我使用和来pipe
组合成一个新的函数。greet
exclaim
sayHello
既然pipe
它只是一个函数,我可以修改它使其也能异步工作。这样一来,代码是同步还是异步就无关紧要了。
我要做的一件事是将任何回调函数转换为 Promise 函数。幸好 Node 的内置功能util.promisify
让这变得简单。
import fs from 'fs'
import { promisify } from 'util'
import pipe from 'mojiscript/core/pipe'
// Synchronous file reader
const readFileSync = fs.readFileSync
// Asynchronous file reader
const readFile = promisify(fs.readFile)
现在,如果我将同步示例与异步示例进行比较,则没有区别。
const syncMain = pipe([
file => readFileSync(file, 'utf8'),
console.log
])
const asyncMain = pipe([
file => readFile(file, 'utf8'),
console.log
])
这正是我想要的!!!
即使readFileSync
是同步和readFile
异步,语法也是完全相同的,输出也完全相同!
我不再需要关心什么是同步,什么是异步。我在两种情况下编写的代码都是一样的。
ESNext 提案:管道操作符
值得一提的是ESNext 提案:管道运算符。
建议的管道操作符将允许您以同样的方式“管道化”功能pipe
。
// pipeline
const result = message =>
message
|> doubleSay
|> capitalize
|> exclaim
// pipe
const result = pipe([
doubleSay,
capitalize,
exclaim
])
Pipeline Operator
和之间的格式pipe
非常相似,我也可以毫无问题地在两者之间切换。
管道提案非常令人兴奋,但也有两个注意事项。
- 它还没有到来,我不知道它是否会到来,或者什么时候到来,或者它会是什么样子。babel 是一个选择。
- 它(目前)还不支持
await
,当它支持时,很可能需要不同的语法来管道同步和异步函数。真恶心。
pipe
与管道运算符语法相比,我仍然更喜欢函数语法。
再次,管道将同步启动代码,我已经将其确定为一个问题。
所以,虽然我对这个功能很兴奋,但我可能永远不会用它,因为我已经有更好的了。这让我百感交集 :|
MojiScript
这就是你问我这到底是什么......
import pipe from 'mojiscript/core/pipe'
// ----------
// /
// WAT?
(好吧,你没有问……但你仍在阅读,而我还在写……)
MojiScript是一种异步优先、固执己见的函数式语言,旨在与 JavaScript 引擎 100% 兼容。
由于 MojiScript 是异步优先的,因此异步代码不会像典型的 JavaScript 那样带来问题。事实上,用 MojiScript 编写异步代码是一种乐趣。
您还可以将 MojiScript 中的函数导入到现有的 JavaScript 应用程序中。了解更多信息:https://github.com/joelnet/MojiScript
MojiScript 异步示例
这是另一个使用 MojiScript 异步的优秀示例pipe
。此函数提示用户输入,然后使用Axios搜索星球大战 API,然后将格式化的结果写入控制台。
const main = ({ axios, askQuestion, log }) => pipe ([
askQuestion ('Search for Star Wars Character: '),
ifEmpty (showNoSearch) (searchForPerson (axios)),
log
])
如果这让你感到好奇,请在此处查看完整的源代码:https://github.com/joelnet/MojiScript/tree/master/examples/star-wars-console
我需要你的帮助!
现在轮到我向你求助了。MojiScript 就像一个超级全新的、处于 pre-alpha 阶段且实验性的项目,我正在寻找贡献者。你能做些什么贡献呢?你可以试用一下,提交 Pull Request,给我反馈,或者问我问题,任何方式都可以!快去https://github.com/joelnet/MojiScript查看吧。
概括
- 异步代码无法在同步环境中运行。
- 同步代码在异步环境中也能正常运行。
- 从一开始就异步编写代码。
for
循环是同步的。删除它们。- 尝试使用类似这样的异步函数组合
pipe
。 pipe
具有与 ESNext Pipeline Proposal 类似的功能,但现已可用。- 使用MojiScript玩转:)
- MojiScript 目前处于实验阶段,因此请不要将其投入生产!
MojiScript 入门:FizzBuzz(第 1 部分)
在 Twitter 上关注我@joelnet
文章来源:https://dev.to/joelnet/why-async-code-is-so-damn-confusing-and-a-how-to-make-it-easy-3441