java promise和async的区别(异步代码的进化之路)

书接上篇。我们已经介绍了事件循环和Job Queue的机制,现在分享的是原文的后半部分——异步代码的发展历程,并比较三种异步模式的优缺点,希望大家看完能够收获新知识~

java promise和async的区别(异步代码的进化之路)(1)

(图片来自网络)

回调

你已经知道,回调是目前为止在JavaScript程序中表达和管理异步的最常用的方式。实际上,回调是JavaScript语言中最基本的异步模式。无数的JS程序,甚至是非常复杂的程序,都是基于回调编写的,除了回调并没有用到其他的异步基础。

不过回调并不完美。许多开发人员都在尝试寻找更好的异步模式。但是,如果你不理解底层的实际情况,就不可能有效地使用任何抽象。

在接下来的章节中,我们将深入探讨这些抽象概念,以说明为什么更复杂的异步模式(将在后续文章中讨论)是必要的,甚至是推荐的。

嵌套回调

看看下面这段代码

listen('click', function (e){ setTimeout(function(){ ajax('https://api.example.com/endpoint', function (text){ if (text == "hello") { doSomething(); } else if (text == "world") { doSomethingElse(); } }); }, 500);});

我们已经将三个函数的嵌套成了一条链,每一环都表示异步序列中的一个步骤。

这种代码通常被称为“回调地狱”。但是“回调地狱”实际上与缩进/缩进几乎没有任何关系。这是一个更深层次的问题。

首先,我们在等待“click”事件,然后等待计时器触发,然后等待Ajax响应返回,此时可能会再次出现。

乍一看,这段代码似乎可以自然地将它的异步映射为连续的步骤:

listen('click', function (e) { // ..});

然后我们有:

setTimeout(function(){ // ..}, 500);

然后我们有:

ajax('https://api.example.com/endpoint', function (text){ // ..});

最后:

if (text == "hello") { doSomething();}else if (text == "world") { doSomethingElse();}

因此,这样一种顺序的方式来表达您的异步代码似乎更自然,不是吗?一定有这样的方法,对吧?

Promise

看看下面这段代码:

var x = 1;var y = 2;console.log(x y);

这段代码非常简单:它对x和y的值进行求和,并将其打印到控制台。但是,如果x或y的值缺失了,还有待确定呢?比如,我们需要从服务器检索x和y的值,然后才能在表达式中使用它们。假设我们有一个函数loadX和loadY ,分别从服务器加载x和y的值。然后,想象一下,我们有一个函数sum,一旦它们都被加载了,它就将x和y的值相加。 它可能是这样的(很难看,不是吗)

function sum(getX, getY, callback) { var x, y; getX(function(result) { x = result; if (y !== undefined) { callback(x y); } }); getY(function(result) { y = result; if (x !== undefined) { callback(x y); } }); } // A sync or async function that retrieves the value of `x` function fetchX() { // .. } // A sync or async function that retrieves the value of `y` function fetchY() { // .. } sum(fetchX, fetchY, function(result) { console.log(result); });

这里有一些非常重要的东西——在这个片段中,我们将x和y作为未来的值,并表示了一个操作sum(…)(从外部看),它并不关心x和y当前可不可用。

当然,这种粗糙的基于调用的方法还有很多值得期待的。这只是迈向理解未来值的好处的第一步,而不用担心时间的问题。

Promise 值

让我们简要地看看我们如何用Promise来表达x y的例子:

function sum(xPromise, yPromise) { // `Promise.all([ .. ])` takes an array of promises, // and returns a new promise that waits on them // all to finish return Promise.all([xPromise, yPromise]) // when that promise is resolved, let's take the // received `X` and `Y` values and add them together. .then(function(values){ // `values` is an array of the messages from the // previously resolved promises return values[0] values[1]; } ); } // `fetchX()` and `fetchY()` return promises for // their respective values, which may be ready // *now* or *later*. sum(fetchX(), fetchY()) // we get a promise back for the sum of those // two numbers. // now we chain-call `then(...)` to wait for the // resolution of that returned promise. .then(function(sum){ console.log(sum); });

在这段代码中有两层Promise。

fetchX()和fetchY()被直接调用,它们返回的值(Promise!)被传递给sum(...)。这些Promise所代表的潜在值可能在现在或者将来准备好,但是无论如何,每个Promise都将其行为规范化为相同的。我们以一种独立于时间的方式来解释x和y的值。它们在一段时间内,是未来值。

第二层是sum(...) 创建(通过Promise.all([ ... ]))和返回的promise,我们通过调用 then(...)来等待它完成。当sum(...)操作完成,我们的未来值,即求和结果已经准备好了,我们可以打印出来。我们隐藏了在sum(...) 中等待x和y 未来值的逻辑。

注意:在sum(…)内部,Promise.all([ … ])调用创建了一个promise(它等待promiseX和promiseY 完成),链式调用.then(...)来创建另一个promise,返回的values[0] values[1]会立即决议(返回相加的结果)。因此,我们在sum(...)调用后加上的then(...)——在代码段的最后——实际上是在第二个promise的返回后执行,而不是第一个创建的Promise.all([ ... ])。还有,虽然我们还没有在第二个then(...)后面继续添加then,它也创造了另一个promise,我们可以选择观察/使用它。本章后面的内容将在后面详细解释。 使用Promise,then(...) 的调用实际上可以有两个方法,第一个是完成(如上所示),第二个是拒绝:

sum(fetchX(), fetchY()).then( // fullfillment handler function(sum) { console.log( sum ); }, // rejection handler function(err) { console.error( err ); // bummer! });

如果在得到x或y的时候出现了问题,或者在添加的过程中出现了一些失败,那么可以sum(…)返回的promise将被拒绝,传递给then(...)的第二个回调错误处理程序,它将收到来自promise拒绝的值。 因为Promises 封装了依赖于时间的状态——等待内在值的实现或拒绝——从外部来看,Promises 本身是时间独立的,因此可以以可预测的方式组合,而不考虑底层的时间和结果。

而且,一旦一个Promise得到决议,它就会永远保持这种状态——在那个时候它就变成了一个不可改变的值——然后就可以在必要的时候多次被观察。

实际上你可以链式Promise非常有用:

function delay(time) { return new Promise(function(resolve, reject){ setTimeout(resolve, time); }); } delay(1000) .then(function(){ console.log("after 1000ms"); return delay(2000); }) .then(function(){ console.log("after another 2000ms"); }) .then(function(){ console.log("step 4 (next Job)"); return delay(5000); }) // ...

调用delay(2000)创造一个在2000ms完成的Promise,然后我们从第一个then(…)完成回调中返回,导致第二个then(...)的promise再等待2000ms执行。

注意: 因为Promise 一旦决议,从外部就不可改变了,所以现在可以安全地将这个值传递给任何一方,因为它知道它不能被意外或恶意地修改。对于观察该promise的多方来说,这一点尤其正确。任意一方不可能影响另一方观察到的决议结果。不变性可能听起来像是一个学术话题,但它实际上是Promise 设计最基本和最重要的方面之一,不应该被随意地忽略。

如何辨别Promise

关于Promises的一个重要细节是确定是否某些值是真正的Promise。换句话说,它的值会像一个Promise吗?

我们知道Promises是由new Promise(…)语法构造的,你可能会认为 p instanceof Promise 是一个充分的检查。好吧,不完全是。

主要是因为你可以从另一个浏览器窗口(例如iframe)获得一个 Promise的值,它有自己独立的Promise类,不同于当前窗口或框架中的一个,因此该检查将无法识别Promise实例。

而且,一个库或框架可以选择发布它自己的Promise,而不是使用ES6原生的Promise实现。事实上,你很可能会在没有任何Promise的老式浏览器中使用第三方的 Promise。

吞掉异常

如果在创建Promise的任何时候,或者在对其决议的观察中,抛出了一个JavaScript异常错误,比如“TypeError”或“ReferenceError”,那么这个异常就会被捕获,它将迫使这个Promise被拒绝。

例如:

var p = new Promise(function(resolve, reject){ foo.bar(); // `foo` is not defined, so error! resolve(374); // never gets here :( }); p.then( function fulfilled(){ // never gets here :( }, function rejected(err){ // `err` will be a `TypeError` exception object // from the `foo.bar()` line. } );

但是如果一个Promise完成了,却在观察结果时(在 then(…) 注册回调)发生了JS异常会怎样呢?即使这个错误不会被丢失,你可能会对它们处理的方式感到惊讶。除非你进一步挖掘:

var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){ foo.bar(); console.log(message); // never reached }, function rejected(err){ // never reached } );

它看起来像“foo . bar()”真的是被吞没了。其实并不是。不过,一些更深层次的问题出现了,我们没能监听到。“p.then(…)调用本身会返回另一个promise,而这个promise将会被TypeError异常所拒绝。

处理未捕获异常

还有其他“更好”的方法。 常见的一个建议是应该给Promise增加一个done(…),用于标志Promise链的结束。done(…)不会创建并返回一个Promise,所以传递到done(…)的回调显然不会把问题报告给一个不存在的链式Promise。 在未捕获错误的情况下,会按照你期望的方式处理:在done(..)中的拒绝处理函数中如果有任何异常,该异常将被抛出为一个全局未捕获的错误(通常在开发人员控制台能看到):

var p = Promise.resolve(374); p.then(function fulfilled(msg){ // numbers don't have string functions, // so will throw an error console.log(msg.toLowerCase()); }) .done(null, function() { // If an exception is caused here, it will be thrown globally });

在ES8里 Async/await会发生什么

JavaScript ES8引入了async/await,使得Promise更容易使用。我们将简要介绍async/await提供的可能性,以及如何利用它们来编写异步代码。 因此,让我们看看async/await如何工作。

使用async函数声明来定义一个异步函数。这样的函数返回一个AsyncFunction对象,AsyncFunction对象代表了执行代码的异步函数,AsyncFunction包含在该函数中。 当调用async函数时,它返回一个Promise 。当async函数返回一个值时,这不是一个Promise 。而是会自动创建一个Promise ,它将使用函数的返回值来决议。当async 函数抛出异常时,Promise 将使用抛出的值来拒绝。 一个async 函数可以包含一个await表达式,该表达式暂停执行该函数,并等待传递给它的Promise被决议,然后恢复async函数的执行并返回决议值。 你可以把JavaScript的Promise看作是Java的Future或c#的任务。

async/await 的目的是简化使用Promise的行为。 让我们看一下下面的例子:

// Just a standard JavaScript functionfunction getNumber1() { return Promise.resolve('374');}// This function does the same as getNumber1async function getNumber2() { return 374;}

同样,抛出异常的函数等价于返回已被拒绝的promise的函数:

function f1() { return Promise.reject('Some error');}async function f2() { throw 'Some error';}

await关键字只能在async 函数中使用,并允许您同步等待一个Promise。如果我们在async 函数之外使用Promise,我们仍然需要使用then回调:

async function loadData() { // `rp` is a request-promise function. var promise1 = rp('https://api.example.com/endpoint1'); var promise2 = rp('https://api.example.com/endpoint2'); // Currently, both requests are fired, concurrently and // now we'll have to wait for them to finish var response1 = await promise1; var response2 = await promise2; return response1 ' ' response2; } // Since, we're not in an `async function` anymore // we have to use `then`. loadData().then(() => console.log('Done'));

你还可以使用“async函数表达式”来定义async函数。一个async函数表达式非常类似,它的语法和async函数声明差不多。async函数表达式和async函数声明之间的主要区别是函数名,它可以在async函数表达式中省略,以创建匿名函数。一个async函数表达式可以作为一个IIFE(立即执行函数表达式)来使用,当IIFE被定义完就会运行。 它看起来像这样:

var loadData = async function() { // `rp` is a request-promise function. var promise1 = rp('https://api.example.com/endpoint1'); var promise2 = rp('https://api.example.com/endpoint2'); // Currently, both requests are fired, concurrently and // now we'll have to wait for them to finish var response1 = await promise1; var response2 = await promise2; return response1 ' ' response2; }

更重要的是,在所有主流浏览器中都支持async/await:

如果这个兼容性不是你想要的,那么也有几个JS的转换器,比如Babel和TypeScript

在一天结束的时候,重要的是不要盲目地选择“最新”的方法来编写异步代码。理解异步JavaScript的内部原理是很重要的,了解它为什么如此重要,并深入理解您选择的方法的内部原理。每种方法都有利有弊。

编写高度可维护的、健壮的异步代码的5个技巧

1. 干净的代码: 使用async/await允许您编写更少的代码。每次使用async/await你能跳过一些不必要的步骤:写.then,创建一个匿名函数来处理响应,在该回调中命名响应变量,比如:

// `rp` is a request-promise function.rp(‘https://api.example.com/endpoint1').then(function(data) { // …});

对比:

// `rp` is a request-promise function.var response = await rp(‘https://api.example.com/endpoint1');

2. 错误处理: Async/await 使得可以使用相同的代码结构来处理同步和异步错误—著名的try / catch声明。让我们看看用Promise如何实现:

function loadData() { try { // Catches synchronous errors. getJSON().then(function(response) { var parsed = JSON.parse(response); console.log(parsed); }).catch(function(e) { // Catches asynchronous errors console.log(e); }); } catch(e) { console.log(e); }}

对比:

async function loadData() { try { var data = JSON.parse(await getJSON()); console.log(data); } catch(e) { console.log(e); }}

3. 条件语句:用async/await编写条件代码要简单得多:

function loadData() { return getJSON() .then(function(response) { if (response.needsAnotherRequest) { return makeAnotherRequest(response) .then(function(anotherResponse) { console.log(anotherResponse) return anotherResponse }) } else { console.log(response) return response } })}

对比:

async function loadData() { var response = await getJSON(); if (response.needsAnotherRequest) { var anotherResponse = await makeAnotherRequest(response); console.log(anotherResponse) return anotherResponse } else { console.log(response); return response; }}

4. 堆栈帧:与async/await不同的是,从一个Promise链返回的错误堆栈没有给出错误发生的位置。如下:

function loadData() { return callAPromise() .then(callback1) .then(callback2) .then(callback3) .then(() => { throw new Error("boom"); })}loadData() .catch(function(e) { console.log(err);// Error: boom at callAPromise.then.then.then.then (index.js:8:13)});

对比:

async function loadData() { await callAPromise1() await callAPromise2() await callAPromise3() await callAPromise4() await callAPromise5() throw new Error("boom");}loadData() .catch(function(e) { console.log(err); // output // Error: boom at loadData (index.js:7:9)});

5. 调试: 如果你使用了Promise,你知道调试它们是一场噩梦。例如,如果在一个.then中设置了断点。然后,阻塞并使用诸如 “stop-over”之类的调试快捷方式,调试器将不会移动到下一个.then,因为它仅对同步代码提供该功能。通过async/await,您可以逐步调试await调用,就像它们是正常的同步函数一样。 不仅对于应用程序本身,对于那些来说,写异步的JavaScript代码也很重要

例如, SessionStack库记录Web应用程序/网站中的所有内容:所有DOM更改、用户交互、JavaScript异常、堆栈跟踪、失败的网络请求和调试消息。 这一切都必须发生在你的生产环境中,而不影响任何用户体验。我们需要对代码进行大量优化,并尽可能使其异步,这样我们就可以增加被事件循环处理的事件的数量。 而且不仅是库!当您在SessionStack中重播一个用户会话时,我们必须在问题发生时在您的用户的浏览器中呈现所有发生的事情,我们必须重现整个状态,允许您在会话时间轴上来回跳转。为了使这成为可能,我们大量使用了JavaScript提供的异步功能。

有一个免费的计划可以让你开始免费使用。

java promise和async的区别(异步代码的进化之路)(2)

免责声明:转载自网络 不用于商业宣传 版权归原作者所有 侵权删

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页