ホームページ  >  記事  >  ウェブフロントエンド  >  Promiseの非同期問題_JavaScriptスキルの詳しい説明

Promiseの非同期問題_JavaScriptスキルの詳しい説明

WBOY
WBOYオリジナル
2016-05-16 15:32:281299ブラウズ

今では、おそらくすべての JavaScript 開発者とその祖母が Promise について聞いたことがあるでしょう。まだそうしていないなら、これからそうするでしょう。 Promise の概念は、CommonJS チームのメンバーによって Promises/A 仕様で提案されました。 Promise は、非同期操作のコールバックを管理する方法としてますます使用されていますが、その設計により、Promise はそれよりもはるかに便利です。実際、Promise はさまざまに使用されるため、Promise について何か書いた後、数え切れないほどの人が、Promise の「要点を見逃している」と言いました。では、約束には何の意味があるのでしょうか?

約束について

Promise の「核心」に入る前に、Promise がどのように機能するのかについて少し内部を見てもらいたいと思いました。 Promise は、Promise/A 仕様によれば、then という 1 つのメソッドのみを必要とするオブジェクトです。 then メソッドは、成功コールバック、失敗コールバック、および前方コールバックの 3 つのパラメータを取ります (仕様では前方コールバックの実装を含めることは要求されていませんが、多くの場合は含める必要があります)。新しい Promise オブジェクトが各 then 呼び出しから返されます。
Promise は、未履行、完了、失敗の 3 つの状態のいずれかになります。 Promise は未履行状態で開始され、成功すると完了し、失敗すると失敗します。 Promise が完了状態に移行すると、それに登録されているすべての成功コールバックが呼び出され、成功の結果値が渡されます。さらに、Promise に登録された成功コールバックは、完了後すぐに呼び出されます。

Promise が失敗状態に移行したときも、成功コールバックの代わりに失敗コールバックを呼び出すことを除いて、同じことが起こります。進行状況機能を含む実装の場合、Promise は未履行状態を離れる前にいつでも進行状況を更新できます。進行状況が更新されると、すべての進行状況コールバックに進行状況の値が渡され、すぐに呼び出されます。進行状況コールバックは、成功および失敗コールバックとは異なる方法で処理されます。進行状況の更新が発生した後に進行状況コールバックを登録した場合、新しい進行状況コールバックは、登録後に更新された進行状況に対してのみ呼び出されます。
Promise 状態の管理方法についてはこれ以上説明しません。これは仕様の範囲外であり、実装ごとに異なるためです。後の例でそれがどのように行われるかを示しますが、今のところ知っておく必要があるのはこれだけです。

コールバックの処理

前述した非同期操作のコールバックの処理は、Promise の最も基本的で一般的な使用法です。標準のコールバックと Promise を使用したコールバックを比較してみましょう。

// Normal callback usage
asyncOperation(function() {
  // Here's your callback
});
// Now `asyncOperation` returns a promise
asyncOperation().then(function(){
  // Here's your callback
});

この例を見ただけで、実際に Promise を使おうと思う人はいないでしょう。 「then」により、非同期操作の完了後にコールバック関数が呼び出されることがより明確になることを除いて、利点はないようです。しかし、この利点があっても、現在はより多くのコードがあり (抽象化によりコードは短くなりますよね?)、Promise のパフォーマンスは標準のコールバックよりわずかに劣ります。

しかし、これで諦めないでください。これが約束でできる最善のことであれば、この記事は存在しないでしょう

破滅のピラミッド

// Normal callback usage => PYRAMID OF DOOOOOOOOM
asyncOperation(function(data){
  // Do some processing with `data`
  anotherAsync(function(data2){
    // Some more processing with `data2`
    yetAnotherAsync(function(){
      // Yay we're finished!
    });
  });
});
// Let's look at using promises
asyncOperation()
.then(function(data){
  // Do some processing with `data`
  return anotherAsync();
})
.then(function(data2){
  // Some more processing with `data2`
  return yetAnotherAsync();
})
.then(function(){
  // Yay we're finished!
});

正如你所见,promises的使用使得事情变扁平而且更可读了。这能起作用是因为——像早先提到的——then返回了一个promise,所以你可以将then的调用不停的串连起来。由then返回的promise装载了由调用返回的值。如果调用返回了一个promise(像这个例子中的情形一样),then返回的 promise装载了与你的回调返回的promise所装载的相同值。这内容很多,因此我将帮助你一步一步的理解它

异步操作返回一个promise对象。因此我们在那个promise对象中调用then,并且传给它一个回调函数;then也会返回一个promise。当异步操作结束,它将给promise装上数据。然后(第一次)回调被调用,数据被作为参数传递进去。如果回调不含有返回值,then返回的promise将会立即不带值组装。如果回调返回的不是一个promise,那么then返回的 promise将会立即装载那个数值。如果回调返回一个promise(像例子中的),那么then返回的 promise将等待直到我们回调返回的promise被完全装载。一旦我们回调的 promise被装载,它装载的值(本例中就是data2)将会被提交给then的promise。然后then中的promise装载了data2。等等。听起来有点复杂,但事实上很简单,如果我说的你不能理解,我非常抱歉。我猜我可能不是谈论它的最佳人选。

用命名的回调替代

但显然 promises 不是使这个结构扁平化的唯一方法。在写了一篇提到promises解决了厄运的金字塔问题的帖子之后,有个人对该帖评论说……

我想promises有时是有用的,但是“嵌套”的回调的问题(圣诞树综合症)可以仅用一个命名的函数作为一个参数替代匿名函数的方法平常的处理:

asyncCall( param1, param2, HandlerCallback );
function HandlerCallback(err, res){
// do stuff
}

它的例子只是给出了一层深的例子,但它仍是正确的。我们来扩展我前面的例子,使这个看起来容易些。

命名回调

// Normal callback usage => PYRAMID OF DOOOOOOOOM
asyncOperation(handler1);
function handler1(data) {
  // Do some processing with `data`
  anotherAsync(handler2);
}
function handler2(data2) {
  // Some more processing with `data2`
  yetAnotherAsync(handler3);
}
function handler3() {
  // Yay we're finished!
}

看看上面的代码!他们绝对是对的!它就是一个扁平的结构,但是这里有个问题同样也存在于 我以前从来没有注意过的老的回调例子中:依赖性和复用性。依赖性和复用性是相互关联的可逆类型。一样东西依赖的越少,那么它的复用性就越大。在以上的例子中,handler1依赖handler2,handler2依赖handler3.这就意味着handler1无论出于任何目的都不可在被用除非handler2也呈现出来。假如你不打算重用他们,那么给你的函数命名又有什么意义呢?

最糟糕的的是handler1都不关心在handler2里面发生了什么事情。它压根就不需要handler2除了和它异步工作。因此,让我们消除这些依赖性然后通过用promise使函数更具复用性。

链式回调

asyncOperation().then(handler1).then(handler2).then(handler3);
function handler1(data) {
  // Do some processing with `data`
  return anotherAsync();
}
function handler2(data2) {
  // Some more processing with `data2`
  return yetAnotherAsync();
}
function handler3() {
  // Yay we're finished!
}

这样看起来是不是好多了?假如另外的函数存在的话,现在handler1和handler2都互不相关了。想看看他们是否真的很棒呢?现在handler1可以被用在不需要handler2的情况下了。相反,handler1被操作以后,我们将可以用另一个handler。

复用函数

asyncOperation().then(handler1).then(anotherHandler);
function handler1(data) {
  // Do some processing with `data`
  return anotherAsync();
}
function anotherHandler(data2) {
  // Do some really awesome stuff that you've never seen before. It'll impress you
}

现在handler1已经从handler2脱离而且可以被用在了更多的情形中,特别是那些由handler2提供的功能而我们又不想用的。这就是复用性!评论家解决代码易读性的唯一方法就是通过消除缩进。我们不想消除缩进仅仅是为了缩进。多层次的缩进仅仅是某些事情错误的标志,问题不一定在它本身。他就像是由脱水引起的头痛。真正的问题是脱水,不是头痛。解决的方法是获得水合物,而不是用一些止痛药。

并行异步操作

在前面我提到的文章里,我将promises与events在处理异步操作方面做了比较。遗憾的是,按照那些曾提到过的人在评论里给的说法,我比较的不是很成功。我描述出了promises的力量,接着转到events来描述它们的力量,就像在我的特别项目里用到的那样。没有比较和对比。一位评论者写道(修改了一点语法错误):

我想用帖子中的例子是一个坏的对照。有篇论文证明了promises的值将会怎样,如果按下虚构的“启动服务器按钮”,将不仅仅是启动一个web服务器,还有一个数据库服务器,当它们都在运行的时候只是更新了UI。

使用promise的.when方法将会使这种“多个异步操作”例子变得普通,然而响应多个异步事件需要一个并不普通的代码量。
他完全正确。事实上我没有比较那两种情况。那篇文章的要点实际在于说明promises不是异步操作的唯一机制,而且在一些情况下,它们也不一定是最好的。在这个评论者指出的情况下,promises当然是最佳的解决办法。我们来看看他说的是什么

jQuery 具有 一个名为when的方法 ,可以带上任意数量的promise参数,并返回一个单一的promise。如果任何一个promise传入失败,when返回的promise也会失败。如果所有的promises被装载,那么每个值都将会按照promises被定义的顺序传递给附加的回调。

以并行的方式执行无数的异步操作非常有用,然后只要在它们之中的每一个结束之后继续执行回调。我们看一个简单的例子。

jQuery.when

// Each of these async functions return a promise
var promise1 = asyncOperation1();
var promise2 = asyncOperation2();
var promise3 = asyncOperation3();
// The $ refers to jQuery
$.when(promise1, promise2, promise3).then(
  function(value1, value2, value3){
    // Do something with each of the returned values
  }
);

これは約束の最も良い点の 1 つであり、それが約束を重要なものにする理由の 1 つであるとよく言われます。また、これは多くの操作を簡素化する良い機能だと思いますが、この when メソッドのメカニズムはどの Promises 仕様にもまったく記載されていないため、それが Promise のポイントではないと思います。 when メソッドについて言及した仕様がありますが、上記とはまったく異なります。私の知る限り、この when メソッドを実装するライブラリは jQuery だけです。 Q、Dojo、when などの他の Promise ライブラリは、Promises/B 仕様に従って when メソッドを実装しますが、コメンテーターが言及した when メソッドは実装しません。ただし、Q ライブラリには all メソッドがあり、when.js にもParallel メソッドがあります。これは、任意の数のパラメータではなく配列型パラメータを受け入れることを除いて、上記の jQuery.when メソッドと同じように機能します。

値の表現

Promise は、次のシナリオを処理するためのより良い方法です:

「このデータベースでユーザーを検索したいのですが、検索メソッドは非同期です。」

つまり、すぐに値を返せない find メソッドがここにあります。しかし、最終的には (コールバック経由で) 値を「返す」ので、その値を何らかの方法で処理する必要があります。ここで、コールバックを使用することで、継続、つまり「後でその値を処理するコード」を定義できます

Promise は、「戻り値を処理するために使用するコードをいくつか示します」を変更します。これらは、「find」メソッドに「おい、探している情報を探すのに忙しくなるけど、それまでの間、結果が返されるのを待ち続けることができる」ということを可能にするメソッドです。本物のように、同時に好きなように処理できます。」

約束は実際の価値を表します。それが罠です。 Promise を本物のように扱うと機能します。 Promise の JavaScript 実装では、これにコールバック関数を渡すことが期待されています。これは単なる「偶然」であり、重要なことではありません。

これがまさに約束の要点だと思います。なぜ? Promise/A 仕様の最初の文「A Promise は、操作の完了によって返される値を表します。」を読むと、それが少し分かりますね。まあ、それが重要な点だったとしても、この記事の後半で他の人の洞察を紹介することをやめるわけではありません。とにかく、このアイデアについてもう少し詳しく話しましょう。

結論

Promise の重要な点は、それが操作によって返される最終結果の値を表すことですが、Promise を使用する理由は、同期操作の並列性を高めるためです。非同期プログラミングが登場して以来、コールバックがあらゆる場所に出現し、奇妙な方法でコードを曖昧にしてきました。約束はそれを変える方法です。 Promise を使用すると、コードを非同期に実行しながら、同期的にコードを書くことができます。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。