非同期トラバーサー

同期トラバーサーに関する問題

「イテレータ」の章で述べたように、イテレータ インターフェイスはデータ トラバーサル用のプロトコルです。イテレータ オブジェクトの「next」メソッドを呼び出す限り、現在のトラバーサル ポインタの位置に関する情報を表すオブジェクトが得られます。 。 next メソッドによって返されるオブジェクトの構造は {value, Done} です。ここで、value は現在のデータの値を表し、done はトラバーサルが終了したかどうかを示すブール値です。

関数 idMaker() {
  インデックス = 0 とします。

  戻る {
    次へ: function() {
      return { 値:index++、完了:false };
    }
  };
}

const it = idMaker();

it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...

上記のコードでは、変数 it はイテレータです。 it.next() メソッドが呼び出されるたびに、現在の移動位置の情報を表すオブジェクトが返されます。

ここには、「it.next()」メソッドは同期的である必要があり、呼び出されるとすぐに値を返さなければならないという暗黙の規定があります。つまり、it.next() メソッドが実行されると、2 つのプロパティ valuedone を同期的に取得する必要があります。トラバーサル ポインタがたまたま同期操作を指している場合はもちろん問題ありませんが、非同期操作の場合は適していません。

関数 idMaker() {
  インデックス = 0 とします。

  戻る {
    次へ: function() {
      return new Promise(function (解決、拒否) {
        setTimeout(() => {
          solve({ 値: Index++、完了: false });
        }, 1000);
      });
    }
  };
}

上記のコードでは、「next()」メソッドは Promise オブジェクトを返しますが、コードに非同期操作が含まれている限り、Iterator プロトコルに準拠していないため、これは受け入れられません。言い換えれば、Iterator プロトコルの next() メソッドには同期操作のみを含めることができます。

現在の解決策は、非同期操作を Thunk 関数または Promise オブジェクトにラップすることです。つまり、next() メソッドの戻り値の value 属性は Thunk 関数または Promise オブジェクトであり、実際の値は後で返されます。 「done」プロパティは引き続き同期的に生成されます。

関数 idMaker() {
  インデックス = 0 とします。

  戻る {
    次へ: function() {
      戻る {
        値: 新しい Promise(resolve => setTimeout(() =>solve(index++), 1000)),
        完了: false
      };
    }
  };
}

const it = idMaker();

it.next().value.then(o => console.log(o)) // 0
it.next().value.then(o => console.log(o)) // 1
it.next().value.then(o => console.log(o)) // 2
// ...

上記のコードでは、「value」属性の戻り値は、非同期操作を配置するために使用される Promise オブジェクトです。しかし、この方法で書くのは面倒で、あまり直観的ではなく、意味論も複雑です。

ES2018 導入 「Async Iterator」(Async Iterator)、非同期操作用のネイティブ イテレータ インターフェイス、つまり valuedone を提供します 両方のプロパティが生成されます非同期的に。

非同期トラバーサル インターフェイス

非同期トラバーサーの最大の文法上の特徴は、トラバーサーの「next」メソッドを呼び出すと Promise オブジェクトが返されることです。

asyncIterator
  。次()
  。それから(
    ({ 値, 完了 }) => /* ... */
  );

上記のコードでは、「asyncIterator」は非同期トラバーサーであり、「next」メソッドを呼び出した後、Promise オブジェクトを返します。したがって、この Promise オブジェクトの状態が resolve に変わった後、then メソッドを使用してコールバック関数を指定できます。コールバック関数のパラメータは、valuedone という 2 つのプロパティを持つオブジェクトであり、同期トラバーサーと同じです。

オブジェクトの同期イテレータのインターフェイスが Symbol.iterator プロパティにデプロイされていることはわかっています。同様に、オブジェクトの非同期イテレータ インターフェイスは Symbol.asyncIterator プロパティにデプロイされます。オブジェクトの種類に関係なく、その Symbol.asyncIterator プロパティに値がある限り、それは非同期に走査される必要があることを意味します。

以下は非同期トラバーサーの例です。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();

asyncIterator
。次()
.then(iterResult1 => {
  console.log(iterResult1); // { 値: 'a'、完了: false }
  asyncIterator.next() を返します。
})
.then(iterResult2 => {
  console.log(iterResult2); // { 値: 'b'、完了: false }
  asyncIterator.next() を返します。
})
.then(iterResult3 => {
  console.log(iterResult3); // { 値: 未定義、完了: true }
});

上記のコードでは、非同期トラバーサーは実際に値を 2 回返します。初めて呼び出されたときは Promise オブジェクトが返され、Promise オブジェクトの「resolve」が返されたときは、現在のデータ メンバー情報を表すオブジェクトが返されます。これは、Promise オブジェクトが仲介者として最初に返されることを除いて、非同期トラバーサーと同期トラバーサーの最終的な動作が同じであることを意味します。

非同期トラバーサーの「next」メソッドにより、Promise オブジェクトが返されます。したがって、「await」コマンドの後に置くことができます。

非同期関数 f() {
  const asyncIterable = createAsyncIterable(['a', 'b']);
  const asyncIterator = asyncIterable[Symbol.asyncIterator]();
  console.log(await asyncIterator.next());
  // { 値: 'a'、完了: false }
  console.log(await asyncIterator.next());
  // { 値: 'b'、完了: false }
  console.log(await asyncIterator.next());
  // { 値: 未定義、完了: true }
}

上記のコードでは、next メソッドを await で処理した後、then メソッドを使用する必要はありません。プロセス全体はほぼ同期されています。

非同期トラバーサーの「next」メソッドは継続的に呼び出すことができ、前のステップで生成された Promise オブジェクト「resolve」が呼び出されるまで待つ必要がないことに注意してください。この場合、「next」メソッドは蓄積され、各ステップの順序で自動的に実行されます。以下は、すべての next メソッドを Promise.all メソッド内に配置する例です。

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{値: v1}, {値: v2}] = await Promise.all([
  asyncIterator.next()、asyncIterator.next()
]);

console.log(v1, v2); // a b

別の使用法は、すべての「next」メソッドを一度に呼び出してから、最後のステップを「待つ」ことです。

非同期関数ランナー() {
  const Writer = openFile('someFile.txt');
  Writer.next('こんにちは');
  Writer.next('ワールド');
  ライターを待つ.return();
}

ランナー();

待ってください...の

前に紹介したように、「for...of」ループは、同期された Iterator インターフェイスをトラバースするために使用されます。新しく導入された for await...of ループは、非同期トラバーサル用の Iterator インターフェイスです。

非同期関数 f() {
  for await (const x of createAsyncIterable(['a', 'b'])) {
    コンソール.ログ(x);
  }
}
//
//b

上記のコードでは、createAsyncIterable() は非同期トラバーサー インターフェイスを持つオブジェクトを返します。for...of ループはこのオブジェクトの非同期トラバーサーの next メソッドを自動的に呼び出し、Promise オブジェクトを取得します。 await は、この Promise オブジェクトの処理に使用されます。 resolve が完了すると、取得された値 (x) が for...of ループ本体に渡されます。

「for await...of」ループの使用法の 1 つは、このループに直接配置できる asyncIterable 操作用の非同期インターフェースをデプロイすることです。

本体 = '';

非同期関数 f() {
  for await(req の const データ) body += データ;
  const 解析 = JSON.parse(body);
  console.log('取得'、解析済み);
}

上記のコードでは、「req」は asyncIterable オブジェクトであり、データを非同期的に読み取るために使用されます。ご覧のとおり、「for await...of」ループを使用すると、コードは非常に簡潔になります。

next メソッドによって返された Promise オブジェクトが reject である場合、for await...of はエラーを報告するため、try...catch でキャッチする必要があります。

非同期関数 () {
  試す {
    for await (const x of createRejectingIterable()) {
      コンソール.ログ(x);
    }
  } キャッチ (e) {
    コンソール.エラー(e);
  }
}

「for await...of」ループは同期トラバーサーでも使用できることに注意してください。

(非同期関数() {
  for await (const x of ['a', 'b']) {
    コンソール.ログ(x);
  }
})();
//
//b

Node v10 は非同期トラバーサーをサポートしており、Stream はこのインターフェイスをデプロイします。従来のファイルの読み取り方法と非同期トラバーサーの作成方法の違いを次に示します。

//伝統的な書き方
関数 main(inputFilePath) {
  const readStream = fs.createReadStream(
    入力ファイルパス、
    { エンコーディング: 'utf8'highWaterMark: 1024 }
  );
  readStream.on('データ', (チャンク) => {
    console.log('>>>> '+チャンク);
  });
  readStream.on('end', () => {
    console.log('### 完了 ###');
  });
}

//非同期トラバーサーの書き方
非同期関数 main(inputFilePath) {
  const readStream = fs.createReadStream(
    入力ファイルパス、
    { エンコーディング: 'utf8'highWaterMark: 1024 }
  );

  for await (readStream の const チャンク) {
    console.log('>>>> '+チャンク);
  }
  console.log('### 完了 ###');
}

非同期ジェネレーター関数

Generator 関数が同期反復子オブジェクトを返すのと同様に、非同期 Generator 関数は非同期反復子オブジェクトを返します。

構文的には、非同期ジェネレーター関数は、「async」関数とジェネレーター関数を組み合わせたものです。

非同期関数* gen() {
  「こんにちは」を返します。
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { 値: 'こんにちは'、完了: false }

上記のコードでは、「gen」は非同期 Generator 関数であり、実行後に非同期 Iterator オブジェクトを返します。オブジェクトの「next」メソッドを呼び出し、Promise オブジェクトを返します。

非同期トラバーサーの設計目標の 1 つは、ジェネレーター関数が同期操作と非同期操作を処理するときに同じインターフェイスのセットを使用できることです。

// 同期ジェネレーター関数
function*map(iterable, func) {
  const iter = iterable[Symbol.iterator]();
  while (true) {
    const {値、完了} = iter.next();
    (終わったら)休憩する。
    利回り関数(値);
  }
}

// 非同期ジェネレーター関数
async function*map(iterable, func) {
  const iter = iterable[Symbol.asyncIterator]();
  while (true) {
    const {値, 完了} = await iter.next();
    (終わったら)休憩する。
    利回り関数(値);
  }
}

上記のコードでは、map はジェネレーター関数、最初のパラメーターはトラバース可能オブジェクト iterable、2 番目のパラメーターはコールバック関数 func です。 map の機能は、func を使用して、iterable の各ステップで返された値を処理することです。上記の map には 2 つのバージョンがあり、前者は同期トラバーサーを処理し、後者は非同期トラバーサーを処理します。この 2 つのバージョンの記述方法は基本的に同じであることがわかります。

次に、非同期ジェネレーター関数の別の例を示します。

非同期関数* readLines(path) {
  let file = await fileOpen(path);

  試す {
    while (!file.EOF) {
      yield await file.readLine();
    }
  } ついに {
    file.close() を待ちます。
  }
}

上記のコードでは、「await」キーワードが非同期操作の前で使用されています。つまり、「await」の後の操作は Promise オブジェクトを返す必要があります。 yield キーワードが使用される場合は常に、next メソッドが停止する場所で、それに続く式の値 (つまり、await file.readLine() の値) が next() として返されます。 ) オブジェクト。value 属性は同期ジェネレーター関数と一致します。

非同期ジェネレーター関数内では、「await」コマンドと「yield」コマンドを同時に使用できます。 「await」コマンドは外部演算により生成された値を関数に入力するために使用され、「yield」コマンドは関数内部の値を出力するために使用されることが理解できます。

上記のコードで定義された非同期ジェネレーター関数は次のように使用されます。

(非同期関数() {
  for await (readLines(filePath) の定数行) {
    コンソール.ログ(行);
  }
})()

非同期ジェネレーター関数は、「for await...of」ループと組み合わせて使用​​できます。

async 関数* prefixLines(asyncIterable) {
  for await (asyncIterable の const 行) {
    yield '> ' + line;
  }
}

非同期ジェネレーター関数の戻り値は非同期イテレーターです。つまり、その「next」メソッドが呼び出されるたびに、Promise オブジェクトが返されます。つまり、「yield」コマンドに続くものは Promise オブジェクトである必要があります。 。上の例のように、「yield」コマンドの後に文字列が続く場合、そのコマンドは自動的に Promise オブジェクトにラップされます。

関数 fetchRandom() {
  const url = 'https://www.random.org/decimal-fractions/'
    + '?num=1&dec=10&col=1&format=plain&rnd=new';
  フェッチ(url)を返します;
}

非同期関数* asyncGenerator() {
  console.log('開始');
  const result = await fetchRandom(); // (A)
  yield '結果: ' + await result.text(); // (B)
  console.log('完了');
}

const ag = asyncGenerator();
ag.next().then(({値, 完了}) => {
  console.log(値);
})

上記のコードでは、「ag」は「asyncGenerator」関数によって返される非同期トラバーサー オブジェクトです。 ag.next() を呼び出した後の上記のコードの実行シーケンスは次のようになります。

  1. ag.next() はすぐに Promise オブジェクトを返します。
  2. asyncGenerator 関数が実行を開始し、Start を出力します。
  3. await コマンドは Promise オブジェクトを返し、asyncGenerator 関数はここで停止します。
  4. ポイント A が満たされ、生成された値が result 変数に入れられ、asyncGenerator 関数が実行を続けます。
  5. 関数は B の yield で実行を一時停止します。 yield コマンドが値を取得すると、ag.next() によって返された Promise オブジェクトが満たされます。
  6. ag.next() の実行開始後に、then メソッドで指定されたコールバック関数が実行されます。このコールバック関数のパラメータはオブジェクト {value, Done} です。ここで、value の値は yield コマンドに続く式の値であり、done の値は false です。

行 A と B は、以下のコードと同様の処理を実行します。

return new Promise((解決、拒否) => {
  fetchRandom()
  .then(result => result.text())
  .then(結果 => {
     解決する({
       値: '結果: ' + 結果、
       完了: false、
     });
  });
});

非同期ジェネレーター関数がエラーをスローすると、Promise オブジェクトの状態が reject に変更され、スローされたエラーは catch メソッドによってキャッチされます。

非同期関数* asyncGenerator() {
  throw new Error('問題!');
}

asyncGenerator()
。次()
.catch(err => console.log(err)); // エラー: 問題があります。

通常の非同期関数は Promise オブジェクトを返すのに対し、非同期 Generator 関数は非同期 Iterator オブジェクトを返すことに注意してください。 async 関数と非同期ジェネレーター関数は、非同期操作をカプセル化する 2 つの方法であり、両方とも同じ目的を達成するために使用されることが理解できます。違いは、前者には独自のエグゼキュータが付属しているのに対し、後者は「for await...of」を通じて実行されるか、自分でエグゼキュータを作成できることです。以下は、非同期ジェネレーター関数の実行者です。

非同期関数 takeAsync(asyncIterable, count = Infinity) {
  const 結果 = [];
  const iterator = asyncIterable[Symbol.asyncIterator]();
  while (result.length < count) {
    const {値, 完了} = await iterator.next();
    (終わったら)休憩する。
    結果.push(値);
  }
  結果を返します。
}

上記のコードでは、非同期ジェネレーター関数によって生成された非同期反復子は、「await iterator.next()」が完了するたびに「while」ループを通じて自動的に実行されます。 「done」属性が「true」になると、ループが終了し、非同期トラバーサの実行が終了します。

以下は、この自動実行プログラムの使用例です。

非同期関数 f() {
  非同期関数* gen() {
    'a' を生成します。
    'b' を生成します。
    'c' を生成します。
  }

  return await takeAsync(gen());
}

f().then(関数 (結果) {
  console.log(結果); // ['a', 'b', 'c']
})

非同期ジェネレーター関数の出現後、JavaScript には通常の関数、非同期関数、ジェネレーター関数、および非同期ジェネレーター関数の 4 つの関数形式があります。各機能を区別する違いに注意してください。基本的に、連続して実行される一連の非同期操作 (ファイルの読み取り、新しいコンテンツの書き込み、ハード ディスクへの保存など) の場合は、async 関数を使用できます。同じデータ構造を生成するファイル (ファイルを 1 行で読み込む行など) を生成する場合、非同期ジェネレーター機能を使用できます。

非同期ジェネレーター関数は、「next」メソッドのパラメーターを通じて外部からの受信データを受信することもできます。

const Writer = openFile('someFile.txt');
Writer.next('hello'); // すぐに実行します。
Writer.next('world'); // すぐに実行します。
await Writer.return(); // 書き込みが終了するまで待ちます。

上記のコードでは、「openFile」は非同期ジェネレーター関数です。 「next」メソッドのパラメータは、関数内の操作にデータを渡します。各「next」メソッドは同期的に実行され、最後の「await」コマンドは書き込み操作全体が終了するのを待つために使用されます。

最後に、同期データ構造でも非同期ジェネレーター関数を使用できます。

非同期関数* createAsyncIterable(syncIterable) {
  for (syncIterable の const 要素) {
    要素を生成します。
  }
}

上記のコードでは、非同期操作がないため、「await」キーワードは使用されていません。

yield* ステートメント

yield* ステートメントの後に非同期トラバーサを続けることもできます。

非同期関数* gen1() {
  'a' を生成します。
  'b' を生成します。
  2を返します。
}

非同期関数* gen2() {
  // 結果は最終的に 2 になります
  const 結果 = yield* gen1();
}

上記のコードでは、「gen2」関数の「result」変数の最終値は「2」です。

同期ジェネレーター関数と同様に、「for await...of」ループは「yield*」を展開します。

(非同期関数() {
  for await (const x of gen2()) {
    コンソール.ログ(x);
  }
})();
//
//b

作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0