JavaScriptと非同期プログラミング


何が非同期ですか?
ウィキペディアによると、主なコントロールフローから独立して発生した事件を非同期といいます.例えば、ある順序で実行されるコードがあります.
void function main() {
  fA();
  fB();
}();
fA=>fBは順番に実行されます.いつまでもfAはfBの前で実行されます.彼らは同期の関係です.加入時にsetTimeoutを使用してfAを遅延させる.
void function main() {
  setTimeout(fA, 1000);
  fB();
}();
このとき、fAはfBに対して非同期である.main関数は、一秒後に一回fAを実行すると宣言しただけで、すぐに実行しませんでした.このとき、fAの制御フローはメインから独立している.
JavaScript-生まれつきの非同期の言語setTimeoutの存在のため、少なくともECMAによって標準化された時点から、JavaScriptは非同期プログラミングをサポートしている.他の言語のsleepと違って、setTimeoutは非同期であり、現在のプログラムの継続を妨げることはない.
しかし、非同期プログラムの真の発展は壮大で、Ajaxの流行は不可欠です.Ajaxの中のA(Aynchronous)は本当に非同期の概念に達しました.これはまだIE 5、IE 6の時代です.
コールバック関数——非同期プログラミングの痛み
非同期タスクを実行した後、開発者にどうやって通知しますか?コールバック関数は最もシンプルで、考えやすい実現方法です.そこで、非同期プログラムが生まれた時から、それはコールバック関数と一緒に縛られました.
例えばsetTimeout.この関数は指定された時間を超えて指定された関数を実行するタイマーを開きます.例えば、一秒後に数字1を出力します.コードは以下の通りです.
setTimeout(() => {
  console.log(1);
}, 1000);
常規用法もし需要が変わったら、毎秒1つの数字を出力する必要があります.(もちろんsetIntervalではありません.)JavaScriptの初心者はこのようなコードを書くかもしれません.
for (let i = 1; i < 10; ++i) {
  setTimeout(() => { //   !
    console.log(i);
  }, 1000);
}
実行結果は1秒待ちで、すべての結果を一度に出力しました.ここのサイクルは10のタイマーを同時に起動しているので、タイマーごとに1秒を待っています.もちろん、すべてのタイマーが1秒後にタイムアウトし、コールバック関数が起動されます.
解法も簡単です.前のタイマーがタイムアウトしたらもう一つのタイマーを起動してください.コードは以下の通りです.
setTimeout(() => {
  console.log(1);
  setTimeout(() => {
    console.log(2);
    setTimeout(() => {
      console.log(3);
      setTimeout(() => {
        console.log(4);
        setTimeout(() => {
          console.log(5);
          setTimeout(() => {
            // ...
          }, 1000);
        }, 1000);
      }, 1000)
    }, 1000)
  }, 1000)
}, 1000);
層の入れ子、結果はこのような漏斗型コードです.新しい基準のPromiseを思いつき、次のように書き換えられるかもしれません.
function timeout(delay) {
  return new Promise(resolve => {
    setTimeout(resolve, delay);
  });
}

timeout(1000).then(() => {
  console.log(1);
  return timeout(1000);
}).then(() => {
  console.log(2);
  return timeout(1000);
}).then(() => {
  console.log(3);
  return timeout(1000);
}).then(() => {
  console.log(4);
  return timeout(1000);
}).then(() => {
  console.log(5);
  return timeout(1000);
}).then(() => {
  // ..
});
漏斗形コードはなくなりましたが、コード量自体はそれほど減っていません.Promiseは、コールバック関数をドライできませんでした.
コールバック関数が存在するため、ループは使えません.循環できないなら、再帰を考慮するしかないです.
let i = 1;
function next() {
  console.log(i);
  if (++i < 10) {
    setTimeout(next, 1000);
  }
}
setTimeout(next, 1000);
再帰的な書き方であるが、next関数はすべてブラウザで呼び出されるので、実際には再帰関数のコールスタック構造はない.
ジェネナート——JavaScriptの中の半協程
多くの言語が協働プログラムを導入して、非同期プログラムを簡略化します.JavaScriptにも似たような概念があります.Generatorといいます.
MDN上の説明:Generatorは途中で退出した後に再び入ることができる関数です.彼らの関数の文脈は再入力するたびに維持されます.簡単に言えば、Generatorと一般Functionとの最大の違いは、Generator自身が前回の呼び出しの状態を保持していることである.
簡単な例を挙げます.
function *gen() {
  yield 1;
  yield 2;
  return 3;
}

void function main() {
  var iter = gen();
  console.log(iter.next().value);
  console.log(iter.next().value);
  console.log(iter.next().value);
}();
コードの実行順序はこうです.
  • は、genを要求し、1つのローズマリーiterを得る.注意このときは、genの関数体が本当に実行されていません.
  • iter.next()を呼び出し、genの関数を実行する.
  • yield 1に出会い、1を返します.iter.next()の戻り値は{done:false、value:1}で、1
  • を出力します.
  • 呼び出しiter.next().前回yieldが出たところから引き続きgenが実行される.
  • yield 2に出会い、2を返します.iter.next()の戻り値は{done:false、value:2}で、2
  • を出力します.
  • 呼び出しiter.next().前回yieldが出たところから引き続きgenが実行される.
  • return 3に遭遇し、3を返し、returnは関数全体が実行済みであることを示している.iter.next()の戻り値は{done:true,value:3}、出力3
  • です.
    Generator関数を呼び出したら、1つのサブジェネレータに戻ります.ユーザーが自動的にiter.next()を呼び出すと、このGenerator関数は本当に実行されます.
    例えば、for ... ofを使って、iteratorを巡回してもいいです.
    for (var i of gen()) {
      console.log(i);
    }
    出力1 2、最後のreturn 3の結果は含まれていません.Generatorの各項目で1つの配列を生成するのも簡単で、Array.from(gen())または[...gen()]を直接使用すればよく、[1, 2]を生成するのも同様に最後のreturn 3を含まない.
    Generatorは非同期ですか?
    Generatorも半協程と言っています.自然と非同期の関係は浅からずです.Generatorは非同期ですか?
    でもないです.前に述べたように、非同期は相対的で、例えば上記の例である.
    function *gen() {
      yield 1;
      yield 2;
      return 3;
    }
    
    void function main() {
      var iter = gen();
      console.log(iter.next().value);
      console.log(iter.next().value);
      console.log(iter.next().value);
    }();
    私たちは直感的に見ることができます.genのメソッドはmainのメソッドと交互に実行されていますので、genはmainに対して非同期的に実行されています.しかし、このプロセスでは、全体の制御フローはブラウザに渡されていないので、genとmainはブラウザに対して同期して実行されるということです.
    Generatorで非同期コードを簡略化する
    最初の問題に戻ります.
    for (let i = 0; i < 10; ++i) {
      setTimeout(() => {
        console.log(i);
      }, 1000);
      //      setTimeout     
    }
    キーは、前のsetTimeoutがフィードバックをトリガしてから次のループを実行する方法です.Generatorを使用すると、setTimeoutの後にyieldが外に出て(制御ストリームをブラウザに返す)、setTimeoutによってトリガされるコールバック関数nextを考慮して、制御ストリームをコードに戻して、次のループを実行します.
    let iter;
    
    function* run() {
      for (let i = 1; i < 10; ++i) {
        setTimeout(() => iter.next(), 1000);
        yield; //      setTimeout     
        console.log(i);
      }
    }
    
    iter = run();
    iter.next();
    コードの実行順序はこうです.
  • は、runを要求し、1つのローズマリーiterを得る.注意このときは、runの関数体が本当に実行されていません.
  • iter.next()を呼び出し、runの関数を実行する.
  • サイクルが開始され、iは1に初期化される.
  • setTimeoutを実行し、タイマーを起動し、コールバック関数は1秒後に実行される.
  • yieldに遭遇し、制御ストリームは最後のyield undefinedに戻る.後に他のコードがないので、ブラウザはコントロールを得て、ユーザイベントに応答して、他の非同期コードなどを実行します.
  • 1秒後、iter.next()がタイムアウトし、コールバック関数setTimeoutが実行される.
  • 呼び出し() => iter.next().前回iter.next()が出たところから引き続き、すなわちyieldが、iの値を出力する.
  • 回のサイクルが終了し、iは2に増加し、第4ステップに戻って
  • を実行し続ける.
  • これにより同期sleepのような要求が実現される.
    async、await――同期文法で非同期コードを書きます.
    上のコードは結局手動でディエゼル変数を定義する必要があります.また、ハンドメイドconsole.log(i)も必要です.もっと重要なのはnextと連結されていて、通用しません.
    私たちはsetTimeoutが非同期プログラミングの未来であることを知っています.PromisePromiseを組み合わせて使ってもいいですか?このように考えた結果がasync関数です.Generatorでコードを取得すると以下の通りです.
    function timeout(delay) {
      return new Promise(resolve => {
        setTimeout(resolve, delay);
      });
    }
    
    async function run() {
      for (let i = 1; i < 10; ++i) {
        await timeout(1000);
        console.log(i);
      }
    }
    run();
    Chromeの設計文書によれば、async関数内部はasyncによって実行される.Generator関数自体は、run関数がいつ実行されたかを知るためにPromiseを返します.したがって、runの後ろにrun()があり、直接.then(xxx)があります.
    注意時には、いくつかの非同期イベントが並行して実行される必要があります.(例えば、2つのインターフェースを呼び出して、2つのインターフェースが戻ったら後続コードを実行します.)
    const a = await queryA(); //    queryA      
    const b = await queryB(); //    queryB
    doSomething(a, b);
    このときawait run()およびawaitはシリアルで実行される.少し修正できます.
    const promiseA = queryA(); //    queryA
    const b = await queryB(); //    queryB         。     queryA     。
    const a = await promiseA(); //    queryB       。     queryA     
    doSomething(a, b);
    個人的には次のような書き方が好きです.
    const [ a, b ] = await Promise.all([ queryA(), queryB() ]);
    doSomething(a, b);
    queryAqueryBを組み合わせて使うと、より効果的です.
    結尾語
    今はawaitの関数が主要なブラウザによって実装されました.古いバージョンのブラウザに対応する場合、Promiseを使用してasyncにコンパイルすることができる.まだ互換性があるなら、ES 5のみをサポートするブラウザも、babelGeneratorにコンパイルし続けることができる.コンパイルしたコードの量が大きいので、コードの膨張に注意してください.
    nodeでServerを書くなら、気にしないで使ってもいいです.koaはGeneratorを使うのがあなたの良いヘルパーです.
    終了