node.js里冗长的回调嵌套回调不好如何破除

原标题:为什么我这个 Java 死忠倒向了 上有过一个博客(现在荒废了),连续六年坚持每周写一到两篇文章,讨论Java生态系统中发生的一切。最常见的话题就是反驳那些唱衰Java的论调。

那么,说好的靠着Java字节码生存和呼吸呢?我这篇文章的目的就是想解释下一个Java字节码的忠实粉丝是如何变成了Node.js/Java的传道者。

其实并不是说我和Java完全不相干了。

过去三年里我也写了许多Java/Spring/Hibernate代码。虽然我很喜欢我的工作——我在Solar Industry工作,做一些实现梦想的事情,如写数据库查询语句查询用电量等,但用Java编程已经是昨日黄花了。

两年的Spring编程让我清楚地意识到一件事:掩盖复杂性并不会让其变简单,只会欲盖弥彰。

Java包含了大量样板代码,扰乱了程序员的意图。

Spring和Spring Boot的教训:掩盖复杂性只会让事情更复杂性。

Java EE是个“由委员会设计”的项目,覆盖了企业应用开发所需的一切,导致过度复杂。

Spring的编程体验非常好,但是一旦在子系统深处出现模糊难懂、从未见过的异常信息,就需要花掉三天以上才能找出问题是什么。

如果框架允许程序员完全不写代码,那产生的额外开销会有多少?

虽然像Eclipse之类的IDE很强大,但都是Java复杂度的症状。

Node.js是一个人磨砺并精炼轻量级事件驱动架构的结果,直到Node.js自己揭露了真相。

Java社区似乎很感谢去掉样板代码,可以让程序员专注做有意义的事。

回调陷阱的解决方案async/await函数就是移除样板代码的例子。

用Node.js写程序很愉快。

Java缺少Java那种严格类型检查,但这是个双刃剑。编程变得容易了许多,但需要更多测试才能保证正确。

npm/yarn包管理器非常优秀,也非常好用,相对的就是令人生厌的Maven。

Java和Node.js都提供优秀的性能,这与“Java很慢因此Node.js的性能必然不好”的传说正相反。

浏览器之间的激烈竞争使得Java变得越来越强大,反过来帮助了Node.js。

Java已成为负担,用Node.js编程很愉快

一些工具或对象是设计师多年精心磨砺并提炼的结果。他们会尝试不同的想法,去掉不需要的特性,最后得到为某个目的量身打造的对象。因此这些对象都有强大的简单性,所以非常吸引人。而Java并不是这种系统。

Spring是个流行的Java Web应用程序开发框架。Spring,特别是Spring Boot,其核心目标是个预配置的、易用的Java EE栈。Spring程序员不需要考虑所有servlets、数据持久、应用服务器,以及构成系统的其他不知所云的东西。Spring会处理这一切,而你只需要专注写代码即可。例如,JPA Repository类会将数据库查询合成为方法,名字类似于“findUserByFirstName”,这样你无需写任何代码,只需要调用方法,Spring就会处理剩余的一切。

——这个想法很不错,而且的确很好用,直到某种情况发生。

persist”,也就是说到达REST访问点的JSON有ID值,而这层含义往往需要几天时间才能理解。这就是过度简化的代价。Hibernate也在过度简化,它希望控制ID值,于是抛出了这个不知所云的异常。在Spring的栈中,子系统一个接一个,它就像复仇女神一样耐心地等待你犯哪怕最微小的错误,然后用应用程序崩溃的异常来惩罚你。

紧接着你就会看到巨大的栈跟踪信息。它们有好几个屏幕那么长,充满了一个又一个抽象方法。Spring显然做了许多工作来实现代码的功能。这种级别的抽象显然需要大量的逻辑,来找出所有信息并执行请求。长长的栈跟踪不一定是坏事,它指出了一个症状:这需要多少内存和性能上的额外开销?

既然程序员不需要写任何代码,那么调用“findUserByFirstName”时,它是怎么执行的?框架需要解析方法名、猜测程序员的意图、构建类似于抽象语法树的东西、生成SQL等等。这些事情的额外开销有多大?一切都只为了让程序员不需要写代码?

在经历了多次折磨,浪费了许多天的时间学习本来不需要学习的东西后,你也许会产生与我同样的迷惑:掩盖复杂性不会产生简单性,只会让系统更复杂。

而使用Node.js的关键就是这一点。

“兼容性很重要”是句很好的口号,它的意思是Java平台的主要价值体现在它完全后向兼容。我们很看中这一点,连T恤衫上都印了这句口号。当然,维持这种程度的兼容性非常痛苦,有时候还可以用来避免那些已经没用的老方法。

Dahl在开发Node.js平台核心时采用的设计美学。Dahl的经验是,线程会导致重量级的复杂系统。他想找一些不同的东西,花了很长时间打磨并提炼了一系列核心思想放到了Node.js里。结果就是一个轻量级、单线程的系统,巧妙地利用了Java的匿名函数进行异步回调,还有一个巧妙的运行时库用来实现异步。最初的基调是通过回调函数的事件发布来实现高吞吐量的事件处理。

然后就是Java语言本身了。Java程序员似乎很喜欢移除样板代码,使得程序员可以专注于有用的事情。

另一个用来对比Java和Java的例子就是事件处理函数的实现。在Java中,事件处理函数需要创建一个实际的抽象接口类。这就需要许多冗长的代码,使得代码本身的意图含混不清。程序员的意图埋在那一大堆样板代码后面,谁能看得清呢?

而在Java中,你只需要简单地使用匿名函数,也就是闭包。不需要搜索正确的抽象接口,只需要写下必须的代码,没有任何冗余。于是有了另一个教训:大多数编程语言只会掩盖程序员的意图,使得代码更难理解。

这就是使用Node.js的最大好处。不过我们还得解决一个问题:回调陷阱。

有时解决方案就蕴藏在问题里

Java的异步编程一直有两个问题。一个就是Node.js中所谓的“回调陷阱”。很容易就陷入嵌套回调函数的陷阱中,每层嵌套都会让代码更复杂,使得错误处理和结果处理更困难。一个相关的问题就是Java语言不会帮助程序员恰当地表达异步执行。

一些库使用Promise来简化异步执行。这就是另一个掩盖复杂度使之更复杂的例子。

这段代码实现了Unix的cat命令。async库用来简化异步执行序列很不错,但它用了许多样板代码,混淆了程序员的真实意图。

我们实际想写的是个循环。但不能写成循环,而且也不是自然的循环结构。进一步,错误处理和结果处理不在最自然的地方,而是违反常规地写到了回调函数内。在Node.js支持ES之前,这是人们能做到的最好方法。

这段代码用async/await函数重写了前面的例子。还是同样的异步结构,但使用了正常的循环结构来书写。错误和结果处理的位置也很自然,代码更易于理解,更容易编写,而且也可以很容易地理解程序员的意图。

回调陷阱并不是用掩盖复杂性的方式解决的。相反,语言和范式的改变解决了回调陷阱的问题,同时还解决了过多样板代码的问题。有了async函数,代码就更漂亮了。

尽管最初这是Node.js的缺点,但优美的解决方案将缺点变成了Node.js和Java的优点。

自定义良好的类型和接口

我之所以是Java的死忠,原因之一就是严格的类型检查使得Java可以用于编写巨型应用。当时的风向是编写宏系统(没有什么微服务、Docker之类的),由于Java有严格的类型检查,Java编译器可以帮你避免许多类型的bug,因为不好的代码无法通过编译。

相反,Java的类型很松散。理论也很明显:程序员无法确定他们收到的对象的类型,那他们怎么知道该做什么呢?

Java的强类型的缺点就是太多样板代码。程序员要不断进行类型转换,否则就得努力保证一切都分毫不差。程序员要花掉很多时间写极其精确的代码,使用更多的样板代码,以图早期发现错误并改正。

这个问题十分严重,因此人们必须使用大型、复杂的IDE。简单的编辑器是不够的。让Java程序员保持正常的唯一方式就是提供下拉菜单供他选择对象中可用的字段,描述方法的参数,帮助他创建类,协助他做重构,以及其他一切Eclipse、NetBeans和IntelliJ能提供的功能。

还有……别逼我说Maven。那个工具太垃圾了。

在Java中,许多类型不需要定义,通常也不需要用类型转换。因此代码更清晰易读,但存在漏掉编码错误的风险。

在这一点上Java是好是坏取决于你的观点。我十年前认为,用这些额外开销获得更多确定性是值得的。但我现在认为,我靠怎么要写这么多代码,还是Java简单。

用容易测试的小模块来对抗bug

Node.js鼓励程序员将程序分割成小单元,即模块。看似是一件小事,但却部分地解决了刚才提到的问题。

  • 自我包含:顾名思义,模块把相关的代码包装成一个单位;
  • 强边界:模块内部的代码不会被其他地方的代码侵入;
  • 显式导出:默认情况下模块中的代码和数据不会被导出,只有选中的函数和代码才能被别人使用;
  • 显式导入:模块需要定义它依赖哪些模块;
  • 潜在的独立性:很容易将模块公开发布到npm代码仓库中,或者发布到其他私有仓库中供其他应用使用;
  • 易于理解:需要阅读的代码量小,因此更容易理解代码的意图;
  • 易于测试:如果实现正确,那么小模块可以很容易进行单元测试。

所有这些特性一起,使得Node.js模块更容易测试,并且有定义良好的范围。

人们对JavaScripot的恐惧一般集中在它缺乏严格的类型检查,因此代码很容易出错。对于小型、目的明确并且有着清晰边界的模块来说,受影响的范围通常会限制在模块内部。因此大多数情况下需要考虑的范围很小,而且都安全地保护在模块的边界内部。

解决弱类型问题的另一个方案就是增加测试。

通过书写简单的Java代码而节省下的时间,必须花一部分在增加测试上。测试用例必须捕获那些本应被编译器捕获的错误。你肯定会测试代码的,对吧?

如果想在Java中享受静态类型检查,可以试试Type。我没用过Type,但听说它很不错。它增加了包括类型检查在内的许多有用的功能,并且可以直接编译成兼容的Java。

因此在这一点上,Node.js和Java完胜。

Maven想想就觉得可怕,完全不知道该写什么。我觉得,肯定有人非常喜欢Maven,也肯定有人很讨厌Maven,两者之间没有中间地带。

Java生态环境的问题之一就是它没有统一的包管理系统。Maven包还算可以,而且理论上应该能在Gradle中使用。但不论是用途、易用性还是功能上,Maven与Node.js的包管理系统相比简直是天壤之别。

在Node.js的世界里有两个非常优秀的包管理系统,他们能合作得很好。最初只有npm和npm的代码仓库。

npm用一种非常好的格式描述包的依赖关系。依赖可以是严格的(精确的版本1.2.3),或者可以逐渐增加较松散的条件,直到使用“*”表示任何最新版本。Node.js社区已经向npm代码仓库发布了几十万个包。在npm代码仓库之外使用这些包也同样容易。

最好的地方是npm代码库不仅供Node.js使用,也可以让前端工程师使用。以前他们使用类似于Bower之类的包管理工具。现在,Bower已经过时,所有的前端Java库都以npm包的形式存在。许多前端工具链如Vue.js CLI和Webpack都是用Node.js编写的。

Node.js的另一个包管理器yarn从npm代码仓库下载包,而且使用与npm相同的配置文件。yarn与npm相比的主要优势就是运行得更快。

不论是用npm还是yarn,npm代码仓库都是使得Node.js如此易用和愉快的重要因素。

在创建了java.awt.Robot之后,我想出了这张图。官方的Duke吉祥物完全由曲线组成,而RoboDuke则都是直线,除了肘关节处的齿轮之外。

Java和Java都被批评过太慢。

两者都由编译器将源代码转换成字节码,再由虚拟机执行。VM通常会将字节码再次编译成原生代码,并使用各种优化技术。

Java和Java在性能方面都有巨大的需求。Java和Node.js需要快速的服务器端代码,在浏览器中的Java则需要更好的客户端应用性能。

Sun/Oracle JDK使用HotSpot这个超级虚拟机,它采用了多字节编译策略。它的名字表示,它会检测经常执行的代码,一段代码执行次数越多,就会应用越多的优化。因此HotSopt可以产生非常快的代码。

而对于Java,我们曾一度迷惑:谁能期待浏览器中运行的Java能实现任何复杂应用程序呢?办公文档套件肯定没办法用Java在浏览器中实现吧?但今日,这一切都实现了。本文就是用Google Docs写的,它的性能还不错,浏览器上运行的Java性能每年都有大幅度增长。

这个增长趋势使得Node.js越来越好,因为它用的就是Chrome的V8引擎。

机器学习领域涉及到大量数学计算,因此数据科学家通常使用R或Python。包括机器学习在内的几个领域都需要快速数值计算。许多原因导致Java很不擅长数值计算,但人们已经在努力开发一个标准库,使得Java也可以进行数值计算。

Java还可以使用Tensorflow中的一个新的库:TensorFlow.js。它的API类似于Python的TensorFlow,可以导入训练好的模型,用来做比如分析动态视频以识别训练过的物体等工作,而且可以完全在浏览器中运行。

此前IBM的Chris Bailey在介绍Node.js的性能和扩展性问题时,就介绍了关于Docker/Kubernetes部署方面的问题。他从一系列性能评测开始谈起,证明Node.js在I/O吞吐量、应用程序启动时间和内存足迹方面的性能已远远超过了Spring Boot。而且,由于V8引擎的改进,Node.js的每次发布都会带来巨大的性能提升。

Bailey还表示,人们不应该在Node.js运行计算类的代码。理解其原因非常重要。因为Node.js是单线程模型,长时间运行的计算会阻塞事件的执行。在我的《Node.js Web开发》一书中,我谈到了这个问题,并介绍了三种方法:

  • 算法重构:找出算法中慢的部分并进行重构以获得更快的速度;
  • 将计算代码用事件分发机制分成小块,这样Node.js可以经常返回到执行线程上;
  • 将计算交给后台服务器。

如果Java的进步还达不到应用程序的要求,那么还有两种方法可以直接在Node.js中集成原生代码。Node.js的工具链包括node-gyp,它能处理链接原生代码模块的工作。WebAssembly能将其他语言编译成一个执行速度很快的Java子集。WebAssembly是一种可执行代码的便携式格式,可以在Java引擎中运行。

富互联网应用(RIA)

十年前软件行业谈论的话题就是,用快速的Java引擎运行富互联网应用,从而使得桌面应用失去存在的必要。

实际上,这件事情20年前就开始了。Sun和Netscape达成了一项协议,在Netscape浏览器中运行Java Applets。当时Java语言是作为编写Java Applets的脚本语言的一部分出现的。当时的希望是在服务器端运行Java Servlets,在客户端运行Java Applets,从而达到前后端使用同一种语言的目的。但由于许多原因,这个目标并没有实现。

十年前,Java开始变得足够强大,可以用来实现复杂的应用程序了。因此出现了RIA这个词,而且RIA据称将在客户端应用的平台上干掉Java。

今天我们可以看到,RIA的想法已经实现了。通过服务器端的Node.js,我们终于可以实现了当年的目标,但两侧的语言却都是Java。

  • Google Docs(这篇文章的协作工具),类似于传统的办公套件,但完全在浏览器中运行。
  • 强大的框架,如React、Angular、Vue.js,它们HTML/CSS进行渲染,极大地简化了基于浏览器的应用开发。

Java在桌面应用平台上失败的原因并不是Java的RIA,主要原因是Sun微系统对于客户端技术的无视。Sun专注于要求快速服务器端性能的企业客户。当时我就在Sun,对此亲眼目睹。真正杀死Applets的是几年前在Java插件和Java Web Start中的一个严重的安全漏洞。那个漏洞造成了全世界的恐慌,于是人们都不再使用Java

其他Java桌面应用依然能够开发,而且NetBeans和Eclipse IDE之间的竞争现在依然火热。但这个领域的Java开发已经是一潭死水了,而且除了一些开发者工具之外,已经很少见到基于Java的应用程序了。

JavaFX是十年前Sun为对抗iPhone而提出的方案。它计划支持在手机中的Java平台上开发富界面的应用程序,从而将Flash和iOS应用程序驱逐出市场。结果没有发生。JavaFX依然有人使用,但并不像它宣称的那么火热。

而这个领域的一切狂热都来自于React、Vue.js和类似框架的出现。

因此,在这一点上Java和Node.js获得了碾压式的胜利。

Java戒指,是早期的一次Java ONE会议的东西。这些戒指上包含芯片,内部完全是用Java实现的。在JavaONE上的主要用途是解锁大厅里的电脑。

Java戒指的说明书。

如今,开发服务器端代码有许多选择。我们不必再被局限在“P语言”(Perl,PHP,Python)和Java中,因为我们有Node.js、Ruby、Haskell、Go、Rust以及其他很多语言。现在的开发者能享受到许多快乐。

至于为什么我这个Java死忠倒向了Node.js,显然是因为我喜欢使用Node.js编程时的自由感。Java已经成为负担,而使用Node.js却没有这种负担。如果有人雇我写Java当然我还会接受,因为我想赚钱。

每个应用程序都有真实的需求。只因为喜欢Node.js就一直使用Node.js的态度显然不可取,选择一种语言或框架必然有技术上的原因。例如,我之前的一些工作涉及了XBRL文档。由于最好的XBRL库是用Python实现的,因此要完成项目,就必须学习Python。

所以,要诚实评价真实需求,然后根据结果进行选择。

作者:David Herron,软件工程师,技术作价,喜欢Node.js和清洁能源技术。《Node.js Web 开发》一书的作者。

译者:弯月,责编:郭芮

本章我们将会学习Promise提供的各种方法以及如何进行错误处理。



如果像下面那样使用一个计时器来计算一下程序执行时间的话,那么就可以非常清楚的知道传递给  的promise数组是同时开始执行的。

也就是说,这个promise对象数组中所有promise都变为resolve状态的话,至少需要128ms。实际我们计算一下 的执行时间的话,它确实是消耗了128ms的时间。

从上述结果可以看出,传递给  的promise并不是一个个的顺序执行的,而是同时开始、并行执行的。

如果这些promise全部串行处理的话,那么需要 等待1ms → 等待32ms → 等待64ms → 等待128ms ,全部执行完毕需要225ms的时间。

要想了解更多关于如何使用Promise进行串行处理的内容,可以参考第4章的中的介绍。

它的使用方法和Promise.all一样,接收一个promise对象数组为参数。

下面我们再来看看在第一个promise对象变为确定(FulFilled)状态后,它之后的promise对象是否还在继续运行。

reject状态之一。也就是说Promise并不适用于  可能会固定不变的处理。也有一些类库提供了对promise进行取消的操作。

此外我们也会学习一下,在 .then 里同时指定处理对错误进行处理的函数相比,和使用 catch 又有什么异同。

我们看看下面的这段代码。

在上面的代码中, badMain 是一个不太好的实现方式(但也不是说它有多坏), goodMain 则是一个能非常好的进行错误处理的版本。

这里我们又学习到了如下一些内容。

我们需要注意如果代码类似 badMain 那样的话,就可能出现程序不会按预期运行的情况,从而不能正确的进行错误处理。

我要回帖

更多关于 嵌套回调不好 的文章

 

随机推荐