JavaScript 中的 Promise - 过去、现在和未来
一篇关于 JavaScript Promise 的历史、当前趋势和未来的文章
引言
今天,对于任何体面的 Web 应用程序,我们都无法想象编写非异步的 JavaScript 代码。无论是服务调用、动画,甚至是像在网页上显示时间这样简单的事情,我们都必须利用 JavaScript 的异步元素。那么,同步编程和异步编程究竟是什么?最简单的区别是,同步代码必须等待前一个操作完成后才能执行下一个操作。
同步代码通常在单个线程中运行。异步代码必须在单独的线程中执行。当异步 JavaScript 代码运行时,浏览器会为我们处理简单场景下的线程管理。当我们必须等待结果才能继续执行任何任务时,我们总是需要编写异步代码;例如,当我们向服务器发出 Get
请求时,我们就必须等到结果返回给我们并在页面上显示它们。这种等待的需求导致了回调的出现。
回调只是在任何异步请求的结果检索后被调用的代码片段。回调可以有两种类型:成功回调和失败回调。当成功接收到结果时,将执行成功回调。当异步代码执行出现任何问题时,将执行失败回调。在 JavaScript 中,成功回调和失败回调可以是相同或不同的函数,这取决于我们的偏好。
当我们处理异步代码和回调时,Promise 就出现了。Promise 是我们将来会收到并且可能处于多种状态的东西;最常见的状态是 Promise 已完成 (Promise Fulfilled) 和 Promise 被拒绝 (Promise Rejected)。
根据 github/kriskowal
引用如果一个函数无法在不阻塞的情况下返回值或抛出异常,那么它可以返回一个 Promise。Promise 是一个对象,它表示该函数可能最终提供的返回值或抛出的异常。Promise 还可以用作远程对象的代理,以克服延迟。
您可以在这里阅读有关 Promise 的 Commonjs 规范
过去
XML Http Request
在拥有 jQuery 的 Promise 之前,为异步服务调用分配回调的常规方法是使用 XHR 回调。可以使用指定的状态码将 Xhr 回调分配给服务器的成功和失败响应。
以下是 xhr
对象的基本实现。在最简单的情况下,我们将创建一个新的 xhr
对象,然后发送请求。在发送请求之前,我们可以根据请求的就绪状态和状态分配回调。
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp = new XMLHttpRequest();
}
else
{// code for IE6, IE5
xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
}
xmlhttp.onreadystatechange = function ()
{
if (xmlhttp.readyState == 4 && xmlhttp.status == 200)
{
successCallback();
}
}
xmlhttp.open('GET', url, true);
xmlhttp.send();
就绪状态可以具有以下值
0
= 未发送1
= 已打开2
= 已接收标头3
= 正在加载4
= 已完成
XHR 状态可以具有以下值
200
= 确定404
= 页面未找到
在当前版本中,我们可以为 xhr
事件分配事件监听器。
var xhr = new XMLHttpRequest();
xhr.addEventListener("progress", progress);
xhr.addEventListener("load", complete);
xhr.addEventListener("error", failed);
xhr.addEventListener("abort", cancelled);
回调可以是普通函数
function completed(data) {
document.getElementById('container').innerHTML = data.Text;
}
jQuery 中的回调
jQuery 的 $.ajax()
具有成功和错误回调,这些回调被广泛用于管理服务器返回数据的延迟问题。它的实现很简单;我们分配函数的两个 JavaScript 属性,这些函数稍后在成功获取结果或发生错误时被调用。
$.ajax({
method: "POST",
url: "some.php",
data: { name: "John", location: "Boston" },
error: function (xhr, error) {
//handle the error
},
success: function (data) {
//handle the returned data
}
});
现在
.then(fn)
快进到今天,我们在 JS 代码中到处都能看到 .then()
。我敢打赌,在每个中到大型应用程序中,只要使用了不错的 JS 框架,你都会看到这种实现。顾名思义,.then()
用于在异步代码执行返回响应后执行回调代码。
以下是使用 Deferred Object 的 .then()
函数的 jQuery 实现
$.when($.ajax("test.aspx")).then(function (data, textStatus, jqXHR) {
alert(jqXHR.status);
});
链式回调
在基于一个异步代码块执行的结果,我们需要发起另一个异步请求的情况下,可能会有很多种情况。在这种情况下,我们必须将回调一个接一个地链接起来。要实现回调链,我们需要一种特殊的对象,它可以存储回调信息,然后排队执行。
$.when($.ajax("someeUrl.aspx"))
.then(function (data, textStatus, jqXHR) {
$.ajax("someOtherUrl.aspx");
})
.then(function (data, textStatus, jqXHR) {
$.ajax("someOtherUrl.aspx");
})
.then(function (data, textStatus, jqXHR) {
$.ajax("yetAnotherUrl.aspx");
}
);
Deferred Object
这个对象可以促进回调链及其排队执行。Deferred 对象公开了几个函数,可用于链接成功和失败回调。其他功能可能包括获取 deferred 对象的当前状态以及在特定上下文中拒绝和解析。
jQuery 中的 Promise 实现
jQuery 从多个 API 返回 Deferred 对象,这些 API 涉及某种异步代码执行。例如,$.ajax()
和许多动画函数会返回 Deferred 对象,我们可以将更多的回调链接到这些对象。
$("#container").css("color", "red").slideUp(2000).slideDown(2000);
其他 JS 代码库
如果我们不想使用任何框架固有的 Promise 代码结构,还有其他代码库可供我们使用。这些库的工作方式或多或少相同,即为成功和失败响应将回调一个接一个地链接起来。其中最突出的一个是 $q
。
AngularJS 实现了 $q
来处理异步代码执行,并提供 $q
依赖项作为内置服务,我们可以在自定义服务工厂中使用它。
$q
或任何其他此类 Deferred 对象库的目的是修复横向增长的回调链,并将它们转换为线性代码语句。
function callback1 (data) {
function callback2(data) {
function callback3(data) {
function callback4(data) {
//and so on
}
}
}
}
上面的代码可以使用 Deferred 对象将它们转换为线性链,以链接 Promise。
someFunctionReturningPromise()
.then(callBack1)
.then(callBack2)
.then(callBack3)
.then(callBack4);
未来
当我们谈论 JavaScript 中 Promise 的未来时,可以说未来已经到来。阻止最新 Promise 实现投入使用的唯一因素是开发者社区的广泛使用。随着越来越多的公司和团队转向 ES6/ES7 编码标准,这种情况也在发生变化。
ES6 Promises
ES6 为 Promise
对象提供了标准规范,可用于处理操作的未来结果。Promise
构造函数的语法如下
new Promise( /* executor */ function(resolve, reject) { ... } );
在上面,我们必须提供一个执行器函数。当我们调用 Promise
对象的 then()
方法时,将执行执行器函数。resolve
和 reject
参数是我们传递给执行器的函数,用于处理任务的未来结果。
下面是一个非常简单的示例,展示了如何使用它
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="container"></div>
<script>
var myPromise = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('Hello');
}, 5000);
});
myPromise.then(function (message) {
document.getElementById('container').innerHTML = message;
});
</script>
</body>
</html>
上面的代码将在 5 秒后在容器 <div>
元素中打印我们选择的消息。这种语法仍然与大多数浏览器不兼容,因此如果您决定在应用程序中使用它,则必须为旧版本使用 polyfill。
async 函数
您可能已经在 C# 语言中编写过带有 async
关键字的方法。带有 async
关键字的方法/函数在其主体中使用 await
关键字来等待异步代码执行的结果。如果收到结果,则执行下一组代码,如果未收到,则错误会被 catch
块捕获。这种实现消除了为异步请求/响应任务执行编写单独的回调函数以进行链接的需要。
<script>
async function GET(url) {
var result = await getResuts(url);
return result;
}
</script>
Async
函数在浏览器中尚无法使用,除非使用合适的转换器对 JS 代码进行转换。JavaScript 中 async
-await
的合适替代品是生成器函数。
<script>
function* someGennerator(url){
yield get(url);
}
console.log(someGenerator('account/1').next().value);
</script>
不幸的是,生成器的支持仍然非常有限,在我们看到它在浏览器中广泛使用之前还需要一段时间。
JavaScript 中 Promise 的高级实现还有很长的路要走,直到事情才能变得稳定。但好消息是,事情正在为我们快速发展。Node.js 环境中已经支持这些功能。对于浏览器,有许多不错的转换器,如 Babel,我们可以使用它们来编写和利用新的语法。
在过去几年里,我看到许多开发者对于 Promise 是什么以及如何利用它们,不仅仅是在异步服务器调用中,而是在许多不同的领域,仍然不是很清楚。
自定义实现
如果您雄心勃勃,那么您甚至可以尝试实现自己的 promise
对象。我以前创建了一个自定义的 Deferred 对象,它支持链接多个回调
我建议使用一个好的 promise
库,而不是编写自己的实现,因为编写自定义代码来处理如此复杂的规范是风险的,而且最好使用经过高度测试、并且在开发者社区中得到广泛使用和支持的代码模块。
各位,今天就到这里。关于 Promise 还有很多可以涵盖的内容;如果您是新手,请将本文视为开始使用 JavaScript 异步代码的起点。如果您是专家,这可能对您来说是一次复习。欢迎提出有意义的评论或提问。
历史
- 2016 年 6 月 26 日:初始版本