ES 6約束のベストプラクティス


ES 6の約束は素晴らしいです!それらはJavaScriptで非同期プログラミングのための不可欠な構成です.そして、最終的に、深くネストしたコード(「コールバック地獄」)をもたらすために最も有名に知られていた古いコールバックベースのパターンを取り替えます.
残念ながら、約束は正確に把握する最も簡単なコンセプトではありません.本稿では、私が長年にわたって学んだベストプラクティスについて議論します.

ハンドル約束拒否
何も、イライラしない約束拒絶をよりイライラさせます.これは、約束がエラーをスローするが、Promise#catch ハンドラは、それを優雅に扱うために存在します.
非常に並行したアプリケーションをデバッグするとき、攻撃的な約束は、以下のような潜在的な(そして、むしろ脅迫的な)エラーメッセージのために見つけるのが非常に難しいです.しかし、一旦それが見つけられて、再現可能であるとみなされるならば、アプリケーションの状態はしばしばアプリケーション自体のすべての同時性のために決定するのが難しいのです.全体的に、それは楽しい経験ではありません.
解決策は、単純ですPromise#catch 拒否する可能性のある約束のハンドラー、どのようにしても.
また、将来のノードのバージョンでは.JS、未処理の約束拒否は、ノードプロセスをクラッシュさせます.今よりも優れたエラーを習慣を処理するより良い時間です.

キープイットリニア


最近の記事では、なぜネスティング約束を避けるのが重要かを説明しました.要するに、入れ子約束は「コールバック地獄」の領域に迷い込んでしまう.約束のゴールは非同期プログラミングのための慣用的な標準意味論を提供することです.約束を入れ子にすることによって、私たちは漠然とノードによって一般化された冗長でかなり面倒なエラーの最初のコールバックに戻ります.JS API .
非同期アクティビティを「線形」に保つには、どちらかを使うことができますasynchronous functions または適切に連鎖約束.
import { promises as fs } from 'fs';

// Nested Promises
fs.readFile('file.txt')
  .then(text1 => fs.readFile(text1)
    .then(text2 => fs.readFile(text2)
      .then(console.log)));

// Linear Chain of Promises
const readOptions = { encoding: 'utf8' };
const readNextFile = fname => fs.readFile(fname, readOptions);
fs.readFile('file.txt', readOptions)
  .then(readNextFile)
  .then(readNextFile)
  .then(console.log);

// Asynchronous Functions
async function readChainOfFiles() {
  const file1 = await readNextFile('file.txt');
  const file2 = await readNextFile(file1);
  console.log(file2);
}
util.promisify あなたの親友
エラーの最初のコールバックからES 6の約束への移行として、我々はすべての“promisifying”の習慣を開発する傾向がある.
ほとんどの場合、古いコールバックベースAPIPromise コンストラクタは十分です.典型的な例は globalThis.setTimeout としてsleep 関数.
const sleep = ms => new Promise(
  resolve => setTimeout(resolve, ms)
);
await sleep(1000);
しかし、他の外部ライブラリは、ボックスからの約束で必然的に「遊びません」.我々が注意しないならば、メモリリークのような予期しない副作用は起こるかもしれません.ノードで.環境変数 util.promisify ユーティリティ機能がこの問題に取り組むために存在します.
その名の通り、util.promisify コールバックベースのAPIのラッピングを修正し、簡素化します.指定された関数は、エラーの最初のコールバックをほとんどのノードとして最後の引数として受け付けます.エビス・ドゥ.特別な実施詳細があるならば1 , ライブラリ著者も提供することができます"custom promisifier" .
import { promisify } from 'util';
const sleep = promisify(setTimeout);
await sleep(1000);

シーケンシャルトラップを避ける


このシリーズの前記事では、複数の独立した約束をスケジューリングする権限を広範囲に議論しました.それが効率的にそのシーケンシャルな性質のために来るとき、約束チェーンは今まで私たちを得ることができます.したがって、プログラムの「アイドル時間」を最小にするキーは同時性です.
import { promisify } from 'util';
const sleep = promisify(setTimeout);

// Sequential Code (~3.0s)
sleep(1000)
  .then(() => sleep(1000));
  .then(() => sleep(1000));

// Concurrent Code (~1.0s)
Promise.all([ sleep(1000), sleep(1000), sleep(1000) ]);

注意:約束もイベントループをブロックすることができます
おそらく約束について最も人気の誤解は、“マルチスレッド”JavaScriptの実行を許可するという信念です.イベントループは「平行」の幻想を与えますが、それはそれだけです.フードの下では、JavaScriptはまだシングルスレッドです.
イベントループは、ランタイムが同時にプログラムを通してイベントをスケジュール、調整、および処理することを可能にするだけです.緩やかに言えば、これらの「出来事」は本当に並行して起こりますが、時が来たときにはまだ処理されます.
次の例では、指定したExecutor関数を使用して新しいスレッドを生成しません.実際には、Executor関数は常に約束の構成によって実行され、イベントループをブロックします.Executor関数が戻ると、トップレベルの実行が再開されます.解決された値の消費Promise#then 現在の呼び出しスタックが残りのトップレベルコードを実行するまで終了します.2
console.log('Before the Executor');

// Blocking the event loop...
const p1 = new Promise(resolve => {
  // Very expensive CPU operation here...
  for (let i = 0; i < 1e9; ++i)
    continue;
  console.log('During the Executor');
  resolve('Resolved');
});

console.log('After the Executor');
p1.then(console.log);
console.log('End of Top-level Code');

// Result:
// 'Before the Executor'
// 'During the Executor'
// 'After the Executor'
// 'End of Top-level Code'
// 'Resolved'
約束は自動的に新しいスレッドを生成しないのでPromise#then ハンドラもイベントループをブロックします.
Promise.resolve()
//.then(...)
//.then(...)
  .then(() => {
    for (let i = 0; i < 1e9; ++i)
      continue;
  });

記憶使用を考慮に入れる
いくつかの残念なことに、約束は比較的重要なメモリの足跡と計算コストを示す傾向がある.
についての情報を格納するに加えてPromise JavaScriptランタイムでは、各約束に関連付けられた非同期アクティビティを追跡するために、さらにメモリを動的に割り当てます.
さらに、約束APIのクロージャとコールバック関数の両方の広範な使用を考えると、両方の約束は驚くほど、メモリのかなりの量を伴います.約束の配列は、ホットコードパスでは非常に複雑であることを証明することができます.
親指の一般的な規則としてPromise プロパティ、メソッド、クロージャ、および非同期状態を格納するための独自の高品質ヒープ割り当てが必要です.我々が使用するより少ない約束、より良いオフ我々は長期的になります.

同期的に約束された約束は冗長で不必要である
前述のように、約束は魔法のように新しい糸を産まない.したがって、完全に同期したExecutor関数Promise 不要な層を間接的に導入する効果があります.3
const promise1 = new Promise(resolve => {
  // Do some synchronous stuff here...
  resolve('Presto');
});
同様にPromise#then 同期的に解決される約束に対するハンドラは、コードの実行をわずかに延期する効果しかありません.4 このユースケースでは、使用する方が良いでしょう global.setImmediate 代わりに.
promise1.then(name => {
  // This handler has been deferred. If this
  // is intentional, one would be better off
  // using `setImmediate`.
});
場合には、Executor関数が非同期I/O操作を含まない場合、それは、前述のメモリと計算オーバーヘッドを持つ間接的な不要な層として機能します.
この理由から、私は個人的に自分を使うことを思いとどまらせる Promise.resolve and Promise.reject 私のプロジェクトで.これらの静的メソッドの主な目的は、値を約束で最適にラップすることです.結果として生じる約束がすぐに解決されるならば、1つの場所で約束の必要がないと主張することができます.
// Chain of Immediately Settled Promises
const resolveSync = Promise.resolve.bind(Promise);
Promise.resolve('Presto')
  .then(resolveSync)  // Each invocation of `resolveSync` (which is an alias
  .then(resolveSync)  // for `Promise.resolve`) constructs a new promise
  .then(resolveSync); // in addition to that returned by `Promise#then`.

長い約束鎖は、若干の眉を上げるべきです
複数の非同期操作を直列に実行する必要がある場合があります.そのような場合、有望なチェーンは仕事のための理想的な抽象です.
しかし、約束APIがそうであるので、注意しなければなりませんPromise#then を返しますPromise インスタンス(以前の状態のいくつかを持ちます).中間ハンドラによって構築された追加の約束を考慮すると、ロングチェーンはメモリとCPUの使用に重要な料金を取る可能性があります.
const p1 = Promise.resolve('Presto');
const p2 = p1.then(x => x);

// The two `Promise` instances are different.
p1 === p2; // false
可能であるときはいつでも、見込みチェーンは短く保たれなければなりません.この規則を実施する効果的な戦略は完全同期を禁止することですPromise#then チェーン内の最終ハンドラを除くハンドラ.
言い換えれば、すべての中間ハンドラは厳密には、つまり、彼らは約束を返す非同期にする必要があります.最終的なハンドラだけが完全に同期したコードを実行する権利を保有します.
import { promises as fs } from 'fs';

// This is **not** an optimal chain of promises
// based on the criteria above.
const readOptions = { encoding: 'utf8' };
fs.readFile('file.txt', readOptions)
  .then(text => {
    // Intermediate handlers must return promises.
    const filename = `${text}.docx`;
    return fs.readFile(filename, readOptions);
  })
  .then(contents => {
    // This handler is fully synchronous. It does not
    // schedule any asynchronous operations. It simply
    // processes the result of the preceding promise
    // only to be wrapped (as a new promise) and later
    // unwrapped (by the succeeding handler).
    const parsedInteger = parseInt(contents);
    return parsedInteger;
  })
  .then(parsed => {
    // Do some synchronous tasks with the parsed contents...
  });
上記の例で示されるように、完全に同期している中間のハンドラーは、約束の冗長ラッピングと包み込みをもたらします.これは、最適な連鎖戦略を実施することが重要である理由です.冗長性をなくすために、我々は、単に後続するハンドラーへの有害な中間ハンドラの仕事を統合することができます.
import { promises as fs } from 'fs';

const readOptions = { encoding: 'utf8' };
fs.readFile('file.txt', readOptions)
  .then(text => {
    // Intermediate handlers must return promises.
    const filename = `${text}.docx`;
    return fs.readFile(filename, readOptions);
  })
  .then(contents => {
    // This no longer requires the intermediate handler.
    const parsed = parseInt(contents);
    // Do some synchronous tasks with the parsed contents...
  });

簡単にしてください!
あなたがそれらを必要としないならば、彼らを使わないでください.それは簡単です.約束なしで抽象化を実装することが可能であるなら、我々は常にそのルートを好むべきです.
約束は「自由」ではない.JavaScriptの「平行」を自分で容易にしない.それらは単に非同期操作をスケジューリングして、扱うための標準化された抽象化です.我々が書くコードが本質的に非同期でないならば、約束の必要はありません.
残念ながら、より頻繁に、我々は強力なアプリケーションの約束が必要です.これが我々がすべてのベストプラクティス、トレードオフ、落とし穴、誤解を認識しなければならない理由です.この点で、それは約束が「悪」であるので、使用を最小にする問題だけです、しかし、彼らが誤用するのがとても簡単であるので.
しかし、これは物語が終わるところでありません.このシリーズでは、ベストプラクティスについてのディスカッションをES2017 asynchronous functions ( async / await ) .
これは、特定の引数形式、初期化操作、クリーンアップ操作などを含むことができる.畝
本質的には、これは「マイクロタスクキュー」に「マイクロタスク」をスケジュールすることを意味する.現在の最上位コードが実行を終了すると、“MicroTask Queue”はすべての予定の約束を解決するのを待ちます.時間をかけて、それぞれの解決された約束のためにPromise#then 解決された値を持つハンドラresolve コールバック).畝
つの約束の追加オーバーヘッドで.畝
各連鎖ハンドラーのための新しい約束を構築する追加のオーバーヘッドで.畝