站点工具

用户工具


Promise简史(译)

Promise 比你想象的要古老——它们源于太空竞赛开始时的一个想法。让我来告诉你一个关于 1960 年代的概念是如何进入你 JS代码的故事。

在开始之前,我们需要知道同步操作和异步操作之间的区别。在讨论代码和实现细节之前,举一个例子更适合理解。

如果你把世界和我们在与异步实体交互时所做的一切关联起来,区别就变得更容易理解了。想象一下自己在自己的车道上开车:你正在听收音机,并与旁边的乘客交谈。现在你有打喷嚏的冲动。大家都知道,打喷嚏是开车时最不想干的事。尽你所能,你也无法阻止自己打喷嚏,于是你开始打喷嚏并闭上眼睛。在那一刻你所做的一切都不会“停止”,你会继续以每小时 65 英里的速度行驶,但同时你会陷入打喷嚏的状态:冻结、闭上眼睛。

打喷嚏就是我所说的同步现实世界动作:当你打喷嚏时,其他一切都不重要,因为这就是你所做的一切。

让我用代码来说明这一点。在这里你可以看到我们打印“hello”,sleep一段时间,然后打印“bye”。在代码sleep时,程序中没有其他任何事情发生。当这个过程完成后,代码继续执行。这里要强调的一点是,代码sleep时,我们的程序中没有发生任何事情。这是一个重要的概念,当我们徘徊在异步演进这条曲折而模糊的路径上时,需要牢记这一点。

如今,打开我们的浏览器控制台并输入 Promise,我们会看到一个 Promise 构造函数。我们可以resolve一个Promise,也能reject一个Promise。

这是一个支持度非常好的的 API,你现在可以在浏览器和 Node.js 中使用,无需 polyfill 也不需要引入额外的js来实现。

你肯定会说:别废话了,告诉我一些我还不知道的事情吧。但我想说的是,能够使用这个 API 并不是什么新奇的事情,新奇的是问自己为什么我今天可以用Promise?

这 7 个字母(“Promise”)是如何如此不稳定地结合在一起却能产生如此有用和强大的价值?

Ryan Dahl 于 2009 年首次提出 Node.js,这是自 Brendan Eich 编写 JavaScript 的初始实现以来 JavaScript 开发的最大转变之一。在他在 JSConf 的演讲中,这个早期版本的 Node 有 Promises…,至少他们称做“Promises”……当然,我想知道这个实现到底是什么样子,所以我克隆了 Node.js 存储库来获取看看日志(温馨警告 - 这个 repo 有点大,花了 30 分钟 :( 所以我真的不建议你这样做)

查看 repo,我们可以看到在 2009 年 6 月,初始提交里出现了 Promises …。让我们看看它是如何实现的。

哦,在这里。等等……这不是一个 Promise 实现,这只是一个封装在类似 Promise 接口中的Event Emitter。但这个想法的意图是什么?这个想法是从哪里来的?

起这个名字的渊源是什么?

要翻旧账非常棘手,但是通过广泛使用回溯历史、电子邮件和个人采访,我能够拼凑出从 Promises 的早期起源到 ES2015 规范的合理历史。

这个关于promise 的故事开始于 1961 年。1961 年,NASA忙于水星太空计划,发射了人造卫星 1 号后他们飞向太空赶上了俄罗斯。 黑猩猩哈姆刚刚完成一次成功的太空之旅,并登上了 LIFE 的封面。

好吧……不提黑猩猩了。回到我们要讲的promise.....

61 年,出现了一篇关于 Algol(算法语言的缩写)的论文,并发表在 ACM 上。本文描述了一种编译程序的语句。我不提细节了,因为我知道很多人已经对算法非常熟悉,但本文讨论的重要概念归结为“Tunk”这个想法

thunk 是提供地址的编译时优化。当这个 thunk 被执行时,一些值最终将在某个标准位置(或在给定地址)可用。现在为什么这很重要,这意味着什么?这个“thunk”的东西是一个值引用最终将在未来某个时间存储在给定位置的概念。这听起来对 Promise 的基本概念有些熟悉——这个值最终将代表未来的某个值。

如果我们跳进德洛里安并快进到 1977 年,那么第一部星球大战刚刚发布,并且引入了一个同样重要的概念。发表了描述“future”概念的论文。

虽然这篇论文是关于处理垃圾收集的方法,但它有一些新颖的想法,这些想法为未来在最终值和远程执行函数的概念上的迭代提供了灵感。

在论文摘要中,作者立即提出了对函数参数进行并行赋值的概念。这个想法在 JavaScript 世界中并不是什么新鲜事,这实际上是一个非常容易识别的概念。我们希望同时并行地eval多个东西。就像网络工作者允许我们做的那样……值得注意的是,这是在 1977 年,而不是 2016 年。

该论文更进一步,概述了获取每个参数并将它们绑定到单独进程的方法。这里有一个基本概念,只要我们允许不同的进程处理各个参数,就没有什么可以阻止每个参数值的赋值和运行“并行且不会相互阻塞。现在,你在想我为什么要关心 params,那些只是一些值......但是,如果你把思维从值转变为不同的东西 - 在这里将 params 认为不仅仅是简单的值,也可能是其他函数和其他“终"值。

1995 年,引入了Joule语言,旨在成为构建分布式系统的新模型。整个模型是使构建分布式系统变得简单。Joule 中的每个动作都包括从服务器发送和接收消息。“通道”是传递消息的抽象或消息管道。Joule正是通过这种方式为正式的消息模式奠定了基础。这种负责委托异步通信而不是基于事件的模式的中间人或中继实体的想法意味着数据及其流是可预测的,这使我们可以将这些实体视为传递数据的另一种模式,即使它们是异步的。

些想法的顶点真正分水岭是在 E语言中实现的 Future/Promise 。虽然 E 引入了许多概念,但我们只看语言的一个特定子集 - Promise 实现。E 是第一个真正非阻塞的 Promise 实现,因为“远程”完成的工作永远不会阻塞未来的执行。

E还建立了一个词典,如上图所示。通过一些细微的调整,该图以 1:1 的比例映射到今天的 JavaScript promise。该图来自原始 E 语言论文和 Mark Miller 的描述。注意到,它将本地promise和远程promise视为同一件事,就像 Joule 将所有消息视为“通道”一样,future 可能会启用跨多个进程的完全并行执行。E 建立在这些想法的基础上,并将 Promise 的想法视为与实际完成工作的位置无关。关于这个原始设计的最后一个有趣的注释是,虽然在失败状态下的 Promise 的原始名称是“broken”,这在其他语言中是没问题的,但这在 JavaScript 中不能用……。为什么呢?"broken"=> break。 break是JS里的一个保留字,于是改为 .catch 来取代 broken

Python 的 Twisted 框架直接从 E 语言派生了它们自己的 future/deferred 实现。这个实现虽然没有吸纳到 E 的promise的全部思想,但带来了足够多的概念,成为 JS 实现成长的种子。

从这一点开始,我们将跟随这个故事的 JavaScript 轨迹——需要注意的是,其他语言的实现路径是并行运行的,并且在某些方面在这段时间内更“正确”。

我能找到的第一个遵循 E promise 精髓的 JS 实现是在 MochiKit (从MochiBot 抽离))中找到的。 当我联系Bob Ippolito 询问他从哪里得到这个实现想法的灵感时,他说他直接从 Python 的 Twisted 库中移植的。

他跟我解释移植此功能的合理理由(因为它确实经受住了时间的考验),我们的交流很开心。

MochiBot 使用了很多 AJAX 风格的调用,而 Deferred 比直接使用 XMLHttpRequest 更容易使用

正如他指出的那样……

因为今天的 fetch API 提供了一个返回 Promise 的 HTTP 接口,所以这不是一个坏主意。

请记住,这是在 2005 年……在我开始认真编程之前,这个直到今天仍然很麻烦的异步问题在当时就已经被感觉到和处理了。

2006 年(或者可能更早,因为我认为提交日期与从 SVN 批量导入的日期有关),Dojo Toolkit 团队checked 了deferredRequest。所以我猜在这种情况下,Dojo 不一定是第一个这样做,......但他们确实很早就这样做了。

对互联网进一步搜索,关于 Dojo 有了更多有趣的发现。2007 年,Alex Russell 将Deferred 的想法与 deferredRequest分离。在 2006 年的listserve 帖子中,Alex 还提到了延迟请求的灵感来自 Mochikit 和 Python 的 Twisted 实现。

2009 年初是promise使用激增的时刻,从小众走向主流。虽然你可能对Q有点熟悉,但有一个名为Waterken Q的早期版本,它对现代 Q 和 when.js 等较新的库有很多语法相似性。实现自我标识为“用于与 JSON 资源交互的简洁且富有表现力的 API”

2009 年,Kris Zyp 提议在 CommonJS 邮件中添加 Promises 作为官方 API。对许多人来说,这是 Promises 在规范中落地的开始。在他的帖子中,他指出了 Waterken Q 的影响

2009年9 月,Kris Kowal(也在 Promise API 组)将 waterken Q 的想法与 E 的设计和目标一起创建了流行的 Q 库。

8 天后,Dojo开始了讨论,从 deferred 的想法转变为 Promise 的想法。

2010年12 月,deferreds 被引入到 jQuery 核心代码,但还没有作为 API 对外公开。Promise 接口最终在 \$.ajax API 中公开——当时任何编写 JavaScript 的人对此肯定记忆深刻。

这个简单的 API 把XMLHttpRequest的让人诟病的难用之处包装了起来,同时引入了“done”的概念作为一个有趣的副作用。

请记住,这段时间网络正在快速发展,HTML5 即将到来,并且越来越多的新网络 API 正在发布:Webcrypto、fetch、IndexedDB、localStorage。这些 API 中的每一个都必须弄清楚如何处理异步操作。有些人选择使用类似 Promise 的接口,有些人选择使用回调,有些人选择使用事件发射器。当将不同的 Promise 实现添加到组合中时,这种脱节变得特别复杂

Promise 规范很简单,但缺乏统一的测试套件使得不同的实现不能一起工作。Paul Chavard率先提出了为 Promise 规范实现引入test suite 的想法。Paul向 Ember 创建了一个pull request,以引入 Promise test suite。

Domenic Denicola 认识到实现的缺陷,在 Ember PR 登陆的那天继续创建了 Promise A+ 文档和测试套件。

他的测试套件和规范导致了 50 多个兼容的 Promise 实现,这对社区来说是一个巨大的胜利,因为这意味着你不必担心不同的 async / deferred / promise 实现。只要实现是 A+,就知道它们将在 .then 级别兼容。这里的胜利是向 TC39 表明,社区围绕 Promises 的想法达成了共识…。而几年前,这个想法被认为过于深奥而无法普及。

由于社区的支持和最终需要其他异步操作,例如 ES2015(当时称为 ES6)中的模块加载,Promises 被快速跟踪并落地,这要感谢 Domenic Denicola。

所以我们在 2020 年使用的 API 是始于1961 年的想法和未来工作的最终结合,跨越多个编程语言和时代。

特别感谢 Kris Kowal、Mark Miller、Rebecca Murphey、Alex Russell 和 Domenic Denicola 帮助重建此时间线。还要感谢 Karl Horky 的编辑帮助。

作者 Sam Saccone,译者 若愚,原文:https://samsaccone.com/posts/history-of-promises.html

> 饥人谷一直致力于培养有灵魂的编程者,打造专业有爱的国内前端技术圈子。如造梦师一般帮助近千名不甘寂寞的追梦人把编程梦变为现实,他们以饥人谷为起点,足迹遍布包括facebook、阿里巴巴、百度、网易、京东、今日头条、大众美团、饿了么、ofo在内的国内外大小企业。 了解培训课程:加微信 xiedaimala03,官网:https://jirengu.com

若愚 · 2023/02/08 18:55 · promise简史_译.txt