ES 6-async+await同期/非同期方案を読み返す

6387 ワード

非同期プログラミングは常にJavaScriptプログラミングの重要な事項である.非同期方式については、ES6は、まず状態管理に基づくPromiseが現れ、次いでGenerator + co が現れ、次いでES7async + await案が現れた.
本文力は最も簡明な方法でasync + awaitを通じさせることを求める.
非同期プログラミングのいくつかのシーン
まず、一般的な問題から開始します.1つのforサイクルでは、どのように非同期的に繰り返し印刷されますか?
私たちはこの問題について、クローズドまたはES6に規定されたletブロック級のスコープで答えたいと思います.
for (let val of [1, 2, 3, 4]) {
    setTimeout(() => console.log(val),100);
}
// =>        :1, 2, 3, 4
ここで述べたのは、非同期キューにおいて所定の順序で並べられた均一に起こる非同期ステップである.
非同期が均一に発生しない場合、非同期キューに登録されている順序は乱れている.
for (let val of [1, 2, 3, 4]) {
    setTimeout(() => console.log(val), 100 * Math.random());
}
// =>         ,   :4, 2, 3, 1
戻ってきた結果は乱序で制御できないことです.これは元々最も真実な非同期です.もう一つの場合は、サイクルの中で、前の非同期を実行してほしいなら、後の非同期を実行してください.どうすればいいですか?
for (let val of ['a', 'b', 'c', 'd']) {
    // a     ,       
    //    b,    
}
これは複数の非同期の「シリアル」ではないですか?
変调callbackに非同期の操作を入れ込んで、やり直しの方式で、この问题を解决したのではないですか?あるいは、Promise + then()積層ネストを使っても問題は解決されます.しかし、あえてこの入れ子をループの中に書くには、まだ一週間はかかります.すみません、もっといい方法がありますか?
非同期化案
考えてみてください.もしいくつかのデータをサーバに送信するなら、前のバッチが成功した(すなわちサーバが成功した応答を返した)だけが次のデータの送信を開始します.そうでなければ送信を終了します.これは典型的な「forサイクルにおける相互依存非同期動作」の例である.
このような「シリアル」の非同期は、本質的には同期として扱うことができることは明らかである.乱序の非同期と比べて、より多くの時間がかかった.道理から言えば、プログラムは非同期で実行したいです.つまり、ブロックをスキップするために、より少ない時間で費用がかかります.逆に、一連の非同期の「シリアル」が必要なら、どうやってプログラミングすればいいですか?
この「シリアル」非同期については、ES6があると非常に簡単にこの問題を解決します.
async function task () {
    for (let val of [1, 2, 3, 4]) {
        // await        
        let result = await send(val);
        if (!result) {
            break;
        }
    }
}
task();
文面から見れば、今回の循環は結果が出たら、また次の循環を行います.そのため、サイクルは一回実行するごとに停止されます.サイクルが終わるまで一回停止されます.このコードが実現し、幾重にもネストされた「逆転地獄」の問題がよく解消され、認知度が低下しました.
これは非同期問題の同期化案である.このスキームに関しては、Promiseが主に非同期の問題を解決しているとしたら、async + awaitが主に解決しているのは非同期の問題を同期化し、非同期プログラミングの認知負担を低減することである.
async+await「外異内同」
最初にこのAPIに接触した時、煩雑な文書を見ていて、主に非同期化を解決するためにasync + awaitが使われているということが分かりました.
実はそうではないです.上記の例から、asyncキーワードは、 文を表しています.この には、行await文があり、動作の同期実行が告示されています.そして、上下に隣接するコードは順次実行されます.
この形式化されたものをもうちょっと訳してください.
1、async関数を実行した後、いつもpromiseオブジェクトに戻りました.2、awaitがある行の文は同期です.
1は、外部から見てtask方法が実行された後にPromiseオブジェクトに戻ることを示しており、Promiseが戻ってくるので、taskは非同期的な方法であることが理解できる.間違いなくこのように使います.
task().then((val) => {alert(val)})
      .then((val) => {alert(val)})
2は、task関数の内部において、非同期が「削る」ことによって同期されていることを示している.全体としては、ちょっと時間がかかる機能を実行するだけです.
総合1、2は、形式的に見ると、「task全体は非同期関数であり、内部全体は同期している」ということで、「異内同」と略称します. は分かりやすいです.実装では、私たちは逆方向にしてもいいです.言語レベルでasyncのキーワードを起動させると、関数実行の最後にpromiseのバックを強制的に追加します.
async fn () {
    let result;
    // ...
    //     promise
    return isPromise(result)? 
            result : Promise.resolve(undefined);
}
はどうやってできますか?実際のawait呼び出しは、結果を取得し、 を変更させるまで、後のステートメント(関数)に再帰的に実行させ、resolveを落とします.resolveだけが落ちて、await行のコードが実行されたと見なされます.したがって、外部は大きなforサイクルであるが、for全体のサイクルは順次連続している.
したがって、上記のフレームの外観だけからすれば、async + awaitの意味は分かりにくい.使うのも簡単ですが、逆にPromiseは身につけなければならない基礎です.
今回の「ES 6を読み返す」シリーズの原則を受けていますが、細部の理解と具体的な実現過程を追求しています.私たちは引き続きこの「形式化」の理解を強化します.
async+awaitのさらなる理解
このような非同期動作longTimeTaskがあり、すでにPromiseで包装されている.この関数を用いて一連の検証を行った.
const longTimeTask = function (time) {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      console.log(`   ${time||'xx'}  ,     `);
      resolve({'msg': 'task done'});
    }, time||1000)
  })
}
async関数の実行状況async exec1関数の返却結果とawaitコマンドの実行結果を参照したい場合:
const exec1 = async function () {
  let result = await longTimeTask();
  console.log('result after long time ===>', result);
}
//           
exec1();
// =>    xx  ,     
// => result after long time ===> Object {msg: "task done"}

//         
console.log(exec1());
// => Promise {[[PromiseStatus]]: "pending",...}
// =>   
以上の2ステップが実行され、関数体内ではexec1が同期し、プログレッシブに実行されること、すなわち非同期動作が先に実行され、その後にconsole.log()が印刷されることが明確に証明された.exec1()の実行結果は、Promiseの最初のフレームPromise ...が飛び出すので、exec1の関数の内部実行ログです.
したがって,すべての検証は,完全に一致した全体が非同期関数であり,内部全体が同期の総括である.
awaitはどうやってその後の文を実行しますか?awaitに戻って、後の文をどうやって実行するかを確認してください.longTimeTask()の後ろに直接then()を戻してください.2つの場合:1)then()の中でもう何も戻りません.then()の中で引き続き手動で他のpromiseに戻ります.
const exec2 = async function () {
  let result = await longTimeTask().then((res) => {
    console.log('then ===>', res.msg);
    res.msg = `${res.msg} then refrash message`;
    //       return          promise
    return Promise.resolve(res);
  });
  console.log('result after await ===>', result.msg);
}
exec2();
// =>     TypeError: Cannot read property 'msg' of undefined
// =>       
まず、longTimeTask()then()回付が追加されました.その回調列列のqueueに置いただけです.つまり、awaitコマンドの後はいつも一つの です.ただし、上記のコードの書き方は迷惑です.(比較的良い実践提案は、longTimeTask方法の後ろのthen()longTimeTask関数体に移してカプセル化することである)
第二に、他のpromiseに手動で戻ってきても、longTimeTask()方法の最終的なresolveに関連する内容は異なる.言い換えれば、awaitコマンドは、後のpromiseresolveの結果を抽出し、resultの違いを直接に引き起こす.
特に、awaitコマンドは、resolveの結果だけを認識し、rejectの結果に対してエラーを報告する.上記returnを以下のreturn文で置き換えて検証しても良い.
return Promise.reject(res);
最後に
実は、非同期プログラミングについては、モジュール間の非同期プログラミング、非同期ユニットテスト、非同期エラー処理、何が良い実践ですか?All in allは、紙面に限られています.ここではまとめません.最後に、async + awaitは本当に優雅なスキームである.