JavaScript 异步编程指南——你不知道的Promise前世Deferred
The following article is from 五月君 Author 五月君
这是一个系列文章,你可以关注公众号「五月君」订阅话题《JavaScript 异步编程指南》获取最新信息。
Promise 是现代 JavaScript 比较重要的一个核心概念,也许你会疑问为什么会提到 Deferred?这个是什么?也许你之前没听过,其实我们现在的 Promise 就是由 Deferred 逐步演变而来形成了如今的一套规范 PromiseA+。
了解 Promise 前世 Deferred
本节你可以跟随笔者一起来了解下这个 Deferred 是什么?对于你以后学习 Promise 我想是会有帮助的,并且对它的历史也会多一些了解、记忆也会更深刻。当今你不能保证所有系统都是使用 React、Vue 来写的,也许你会遇到一些使用 Jquery 写的系统,总不能不维护吧,当你看到它的 Ajax 请求时也知道这个东西是干嘛的,为什么要这样写。
Promise 曾经以多种形式存在于多种语言中,这个词最早由 C++ 工程师用在 Xanadu 项目中,随后被应用于 E 语言中,这又激发了 Python 人员的灵感,将它实现成为了 Twisted 框架的 Deffered 对象。
2007 年 Promise 赶上了 JavaScript 的流行大潮,当时 Twisted 的 Dojo 框架添加了一个名为 dojo.Deferred 对象。当时,相对成熟的 Dojo 在流行方面可以与初出茅庐的 Jquery 相媲美(争夺人气),虽然 Deferred 模式最早出现于 Dojo 代码中,但被广为所知却来源于 Jquery 1.5 版本,这也是 Jquery 中的一个重要的转折点,在这个版本之后引入了一个新的功能 Deferred,它彻底的改变了在 Jquery 中如何使用 Ajax,几乎重写了 Jquery 的 Ajax 部分。
在 2009 年时 Kris Zyp 有感于 dojo.deferred 的影响力,该模式被抽象为一个提议草案,发布在 CommonJS 规范中,后来又抽象出 Promise/A 规范,同年 Node.js 首次亮相。
Node.js 的早期迭代在非阻塞 API 中使用了 Promise。但是,在 2010 年 2 月,Node.js 早期的作者 Ryan Dahl 决定改为现在大家都熟悉的 callback(err, result)
,理由是 Promise 属于 “用户区” 更高级别构造,所以早期你会看到 Node.js 中的很多 API 都是 callback(err, result) 形式的,包括现在也还有,顺便在说明下 Ryan Dahl 早在 2012 年就已经离开了 Node.js 社区,之后一直由 Node.js 基金会管理,如今已经 2021 年了,Node.js 本身也发生了很多的变化,包括文件操作也为我们提供了基于 Promise 形式的 API,Stream 目前也很好的支持异步迭代,你不用在使用 callback 那种形式嵌套你的程序。
当时 Ryan Dahl 的决定为以 Node.js 为竞争目标的 Promise 实现创建了条件,例如 Q.js 曾一度很流行,是基于 Promise/A 规范相当简单的实现。Futures 是一个更广泛的工具包,其中包含 Async.js 之类的库中提供了许多流程控制功能。
在上一节,我们讲到了在早期我们都是通过使用回调(Callback)的形式向服务器发起网络请求,随后通过注册的回调函数拿到返回的数据,当时我们也提到了基于 Callback 的形式很容易造成回调函数嵌套、错误难以处理,现在我们看下早期 Jquery 中 Deferred 的解决方案是如何做的,与我们后面讲解的 Promise 有什么关联。
Ajax 中的 Deferred 对象
Jquery 1.5 之前的 ajax 书写方式:
// 返回的是 XHR 对象
$.ajax({
url: "http://openapi.xxxxxx.com/api",
success: function(){
console.log("success!");
},
error:function(){
console.log("failed!");
}
});
Jquery 1.5 之后的 ajax 书写方式:
// 返回的是 Deferred 对象
$.ajax("http://openapi.xxxxxx.com/api")
.done(function(){ console.log("success1!"); })
.fail(function(){ console.log("failed1!"); })
.done(function(){ console.log("success2!"); })
.fail(function(){ console.log("failed2!"); })
以链式的方式来写,极大的提高了阅读体验,相比回调嵌套确实解决了回调地狱问题,done() 是之前的 success() 方法,fail() 是之前 error() 方法。
了解 Promise 的应该能看出是不是有点感觉像?让我们在改造下,使用 .then() 的方式:
$.ajax("http://openapi.xxxxxx.com/api")
.then(function(){ console.log("success1!"); }, function(){ console.log("failed1!"); })
.then(function(){ console.log("success2!"); }, function(){ console.log("failed2!"); })
是不是更像 Promise 了?
封装一个自己的 Deferred 对象
deferred 对象的执行将状态分为三个:未完成、已完成、已失败。调用 dtd.resolve() 是将执行状态变为已完成,会调用 done() 方法指定的回调函数。执行 dtd.reject() 是将执行状态变为已失败,会调用 fail() 方法指定的回调函数。
const wait = () => {
const dtd = $.Deferred();
const tasks = () => {
console.log('do something...')
dtd.resolve(); // 调用 Deferred 的执行状态为已完成
// 如果出错也可调用 dtd.reject();
}
setTimeout(tasks,5000);
return dtd;
}
现在 wait 返回的就是一个 Deferred 对象了,可以使用链式操作。下面我们使用 dtd.then() 该方法就已经涵盖了 done() 和 fail() 方法。
const d = wait()
d.then(() => {
console.log('success1');
}, err => {
console.error('failed1')
})
.then(() => {
console.log('success2');
}, err => {
console.error('failed2')
})
运行程序后,大约 5 秒钟我们的程序运行结果如下所示:
do something...
success1
success2
现在还有一个问题,我可以在代码的尾部添加一行 d.resolve();
这会改变程序的运行结果,这是因为我们在外部改变了执行状态。
const d = wait()
d.then(...); // 和上面一样,此处省略
d.resolve();
// 运行结果
success1
success2
do something...
为了避免这种情况,jQuery 1.5 之后提供了 deferred.promise() 方法,作用是在 deferred 对象上返回 deferred 的 promise 对象,仅能使用与执行状态无关的方法,例如 dtd.then() 或 dtd.done()、dtd.fail() 方法。与执行状态有关的方法 dtd.resolve()、dtd.reject() 会被屏蔽。
const wait = () => {
...
return dtd.promise();
}
总结
Deferred 对象有 dtd.resolve()、dtd.reject() 这种与执行状态有关主动触发的函数,也有 dtd.then() 或 dtd.done()、dtd.fail() 这种被动监听的函数,这些函数都在一块,如上面例所示很容易出现在外部被篡改。解决方案是返回一个 dtd.promise() 对象,只能被动监听不能主动修改执行状态。
通过本文你应该会发现这和我们现在使用的 Promise/A+ 这种规范很相似,这也是 Promise/A+ 规范的前世。
Rerefence
https://medium.com/pragmatic-programmers/a-very-brief-history-of-promises-fa6cbbb10855 http://www.ruanyifeng.com/blog/2011/08/a_detailed_explanation_of_jquery_deferred_object.html
推荐阅读
新版犀牛书
《JavaScript权威指南 原书第7版》
“犀牛书”已经成为JavaScript程序员心中公认的权威指南。凭着完整的内容、细致的讲解以及海量针对性的示例而受到读者的一致好评,这本巨著主要讲述的内容涵盖JavaScript语言本身,以及Web浏览器所实现的JavaScriptAPI。初学者读完本书,将会对JS有全面的认识,快速掌握JS最核心的技术。而有经验的开发者读完本书,会让你对JS的理解有从量变到质变的深层次飞跃。
如今,全球畅销25年的JS犀牛书全新升级,新版涵盖了ES2020特性,同时删去了已过时的内容。值得珍藏。
扫码关注【华章计算机】视频号
每天来听华章哥讲书
书讯 | 7月书讯(下)| 读书开启下半年书讯 | 7月书讯(上)| 读书开启下半年资讯 | 《数据安全法》表决通过!最新解读来了书单 | 2021半年盘点,不想你错过的重磅新书干货 | 详解数据资产的8大重要特征收藏 | 一文了解滴滴与蚂蚁金服开源共建的SQLFlow上新 | 【新书速递】打通数据科学三要素——数据科学实战性手册赠书 | 【第64期】豆瓣9.8分,周志明的《凤凰架构》