非同期フロー制御

この投稿の内容は、Mixu の Node.js Book に大きく影響を受けています。

JavaScript は、その核心において、ビューがレンダリングされる「メイン」スレッドをブロックしないように設計されています。ブラウザにおけるこの重要性は想像できるでしょう。メインスレッドがブロックされると、エンドユーザーが恐れる悪名高い「フリーズ」が発生し、他のイベントをディスパッチできなくなり、例えばデータ取得の損失につながります。

これにより、関数型プログラミングのスタイルでしか解決できない独特の制約が生まれます。ここでコールバックが登場します。

しかし、コールバックはより複雑な手順では扱いが難しくなることがあります。これはしばしば「コールバック地獄」につながり、複数のネストされた関数とコールバックがコードの読みやすさ、デバッグ、整理などをより困難にします。

async1(function (, ) {
  async2(function () {
    async3(function () {
      async4(function () {
        async5(function () {
          // do something with output
        });
      });
    });
  });
});

もちろん、実際には result1result2 などを処理するための追加のコード行があるでしょうから、この問題の長さと複雑さは、通常、上記の例よりもはるかに乱雑なコードになります。

ここで関数が非常に役立ちます。より複雑な操作は多くの関数で構成されています。

  1. イニシエータースタイル / 入力
  2. ミドルウェア
  3. ターミネーター

「イニシエータースタイル / 入力」は、シーケンスの最初の関数です。この関数は、操作のための元の入力(もしあれば)を受け入れます。操作は実行可能な一連の関数であり、元の入力は主に以下のようになります。

  1. グローバル環境内の変数
  2. 引数ありまたはなしでの直接呼び出し
  3. ファイルシステムまたはネットワークリクエストによって取得された値

ネットワークリクエストは、外部ネットワークによって開始された受信リクエスト、同じネットワーク上の別のアプリケーションによるリクエスト、または同じまたは外部ネットワーク上のアプリケーション自体によるリクエストである可能性があります。

ミドルウェア関数は別の関数を返し、ターミネーター関数はコールバックを呼び出します。以下は、ネットワークまたはファイルシステムリクエストへのフローを示しています。ここでは、これらの値はすべてメモリ内で利用可能であるため、レイテンシは 0 です。

function (, ) {
  (`${} and terminated by executing callback `);
}

function (, ) {
  return (`${} touched by middleware `, );
}

function () {
  const  = 'hello this is a function ';
  (, function () {
    .();
    // requires callback to `return` result
  });
}

();

状態管理

関数は状態に依存する場合としない場合があります。状態依存性は、関数の入力または他の変数が外部の関数に依存する場合に発生します。

このように、状態管理には主に 2 つの戦略があります。

  1. 変数を関数に直接渡すこと、そして
  2. キャッシュ、セッション、ファイル、データベース、ネットワーク、またはその他の外部ソースから変数値を取得すること。

グローバル変数については言及しませんでした。グローバル変数で状態を管理することは、しばしば状態を保証することが困難または不可能になるずさんなアンチパターンです。複雑なプログラムでは、可能な限りグローバル変数を避けるべきです。

制御フロー

オブジェクトがメモリ内で利用可能であれば、反復が可能であり、制御フローに変更はありません。

function () {
  let  = '';
  let  = 100;
  for (;  > 0;  -= 1) {
     += `${} beers on the wall, you take one down and pass it around, ${
       - 1
    } bottles of beer on the wall\n`;
    if ( === 1) {
       += "Hey let's get some more beer";
    }
  }

  return ;
}

function () {
  if (!) {
    throw new ("song is '' empty, FEED ME A SONG!");
  }

  .();
}

const  = ();
// this will work
();

しかし、データがメモリ外に存在する場合、反復は機能しなくなります。

function () {
  let  = '';
  let  = 100;
  for (;  > 0;  -= 1) {
    (function () {
       += `${} beers on the wall, you take one down and pass it around, ${
         - 1
      } bottles of beer on the wall\n`;
      if ( === 1) {
         += "Hey let's get some more beer";
      }
    }, 0);
  }

  return ;
}

function () {
  if (!) {
    throw new ("song is '' empty, FEED ME A SONG!");
  }

  .();
}

const  = ('beer');
// this will not work
();
// Uncaught Error: song is '' empty, FEED ME A SONG!

なぜこうなったのでしょうか? setTimeout は、CPU に命令をバス上の別の場所に保存するよう指示し、そのデータが後で取得されるようにスケジュールされていることを指示します。関数が 0 ミリ秒の時点で再びヒットするまでに何千もの CPU サイクルが経過し、CPU はバスから命令を取得して実行します。唯一の問題は、song ('') が何千サイクルも前に返されていたことです。

ファイルシステムやネットワークリクエストを扱う場合も同様の状況が発生します。メインスレッドは不確定な期間ブロックされることはできません。そのため、コールバックを使用して、制御された方法でコードの実行を時間的にスケジュールします。

次の 3 つのパターンで、ほぼすべての操作を実行できます。

  1. 直列: 関数は厳密な順序で実行されます。これは for ループに最も似ています。
// operations defined elsewhere and ready to execute
const  = [
  { : function1, : args1 },
  { : function2, : args2 },
  { : function3, : args3 },
];

function (, ) {
  // executes function
  const { ,  } = ;
  (, );
}

function () {
  if (!) {
    .(0); // finished
  }

  (, function () {
    // continue AFTER callback
    (.());
  });
}

(.());
  1. 制限付き直列: 関数は厳密な順序で実行されますが、実行回数に制限があります。大きなリストを処理する必要があるが、正常に処理されるアイテムの数に上限がある場合に便利です。
let  = 0;

function () {
  .(`dispatched ${} emails`);
  .('finished');
}

function (, ) {
  // `sendMail` is a hypothetical SMTP client
  sendMail(
    {
      : 'Dinner tonight',
      : 'We have lots of cabbage on the plate. You coming?',
      : .email,
    },
    
  );
}

function () {
  getListOfTenMillionGreatEmails(function (, ) {
    if () {
      throw ;
    }

    function () {
      if (! ||  >= 1000000) {
        return ();
      }

      (, function () {
        if (!) {
           += 1;
        }

        (.pop());
      });
    }

    (.pop());
  });
}

();
  1. 完全並列: 1,000,000 人のメール受信者にメールを送信する場合など、順序が問題にならない場合。
let  = 0;
let  = 0;
const  = [];
const  = [
  { : 'Bart', : 'bart@tld' },
  { : 'Marge', : 'marge@tld' },
  { : 'Homer', : 'homer@tld' },
  { : 'Lisa', : 'lisa@tld' },
  { : 'Maggie', : 'maggie@tld' },
];

function (, ) {
  // `sendMail` is a hypothetical SMTP client
  sendMail(
    {
      : 'Dinner tonight',
      : 'We have lots of cabbage on the plate. You coming?',
      : .email,
    },
    
  );
}

function () {
  .(`Result: ${.count} attempts \
      & ${.success} succeeded emails`);
  if (.failed.length) {
    .(`Failed to send to: \
        \n${.failed.join('\n')}\n`);
  }
}

.(function () {
  (, function () {
    if (!) {
       += 1;
    } else {
      .(.);
    }
     += 1;

    if ( === .) {
      ({
        ,
        ,
        ,
      });
    }
  });
});

それぞれに独自のユースケース、利点、問題があり、より詳細に実験したり読んだりすることができます。最も重要なことは、操作をモジュール化し、コールバックを使用することを忘れないでください!疑問がある場合は、すべてをミドルウェアであるかのように扱ってください!

読了時間
6 分
作成者
コントリビュート
このページを編集
目次
  1. 状態管理
  2. 制御フロー