序文
このセクションでは、主な対象は非同期性を学んだばかりの方であるため、著者はこの側面の内容をより一般的な言語で簡単に整理するつもりです。
同期と非同期
非同期の進化過程について話したいので、もちろん、同期と非同期の概念についても言及する必要があります。
同期とは
同期とは、次のタスクが前のタスクの実行を待ってから実行することであり、実行順序はタスクの配置順序と一致します。
次は同期です。
console.log('hello 0')
console.log('hello 1')
console.log('hello 2')
// hello 0
// hello 1
// hello 2
非同期とは
非同期処理では呼び出し側は止まらずに次の処理に進みます。
setTimeout(() => {
console.log('hello 0')
}, 1000)
console.log('hello 1')
// hello 1
// hello 0
なぜ非同期の概念を導入するのですか?
もちろん、新しい概念の導入は、特定の問題を解決するためのものです。
たとえば、ページをロードするときに、ページにレンダリングするためのバックグラウンドのデータを取得する必要があるが、ネットワーク遅延の可能性があるため、その以降で実行する必要がある処理がまだある場合、これを取得できず、次の処理を実行することはできなく、画面がホワイトスクリーンになるので、ユーザー体験が非常に悪いです。 しかし、非同期処理を導入すれば、非同期タスクを待たず、非同期タスクの次にタスクを実行し続けます。
コールバック関数 Callback
コールバック関数とは
MDN ドキュメントでは、 callback()
の定義は次のようになります。
コールバック関数とは、引数として他の関数に渡され、外側の関数の中で呼び出されて、何らかのルーチンやアクションを完了させる関数のことです。
簡単に説明すると:
関数a
のパラメータが関数b
であり、関数a
の実行後に 関数a
を実行する
例えば:
console.log('a');
console.log('b');
console.log('c');
setTimeout(() => {
console.log('届きました!')
},2000)// 非同期
console.log('d');
console.log('e');
コールバック関数が 2000 ミリ秒後に実行されることを設定します。
非同期処理にコールバック関数のメソッドを使用する利点は、理解しやすいことですね。
コールバック関数の欠点を小さな例で説明します:
setTimeout(function () { //レベル1
console.log(111);
setTimeout(function () { //レベル2
console.log(222);
setTimeout(function () { //レベル3
console.log(333);
}, 1000)
}, 2000)
}, 3000)
この書き方は非同期処理のコード実行を順番に解決しますが、このコールバック関数の入れ子はコールバック地獄を引き起こします。
このコードは、ネストされたリクエストの3つのレイヤーを使用しているため、コードが既に乱雑になっているため、このネストされた呼び出しの後で、乱雑なコード構造を解決する必要があります。
このコードが乱雑に見える主な理由は次のとおりです。
- 非同期コールバックのネストはコードの可読性を著しく低下させ、エラー処理は不便となる。
- 各レイヤーの実行は、前のレイヤーの結果に依存する。このネストされた関数のカップリングは高すぎる。変更したい時は、全ての処理を変更する必要がある。
- try catch でエラーする際に、エラーは何処なのか分からない。
Promise
日本語の翻訳は約束
であり、将来のある時点でデータを返すことを約束することを意味します。 これはJavaScriptのネイティブ オブジェクトであり、従来のコールバック関数ソリューションを置き換えることができる非同期プログラミングのソリューションです。
簡単に紹介します:
Promise
オブジェクトには 3つの状態しかありません。
-
pendding
: 初期状態、成功でも失敗でもない状態。 -
fulfilled
: 操作が正常に完了した。 -
rejected
: 操作が失敗した
Promise
は、チェーンで呼び出すことができるthen
メソッドを提供し、前の処理を実行した後にthen
メソッドを呼び出すことができます (Promise
が保留状態から実行状態になったとき)。
次に、Promise
はどのようにコールバック地獄の問題を解決するか readFile
を例として見てみましょう (最初にA
のテキストコンテンツを読み取り、次にA
のテキストコンテンツに従って B
を読み取り、次に B
のコンテンツに従って C
を読み取ります)。
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
Promise
コードから、promise.then
の使用も非常に複雑であることがわかります. リクエスト プロセス全体は線形化されていますが、コードには多数の then 関数が含まれているため、コードはまだ非常に読みにくい。
async/await
async/await
は ES7
で導入されました。このアプローチにより、より簡潔なコードが実現可能です。
async/await
のメリットは、コードが明確であり、コールバック地獄の問題に対処するために Promise
のようなチェーンを多数記述する必要がないです。 また、エラーは try catch
する可能になる。
上記の readFile (最初に A のテキスト コンテンツを読み取り、次に A のテキスト コンテンツに従って B を読み取り、次に B のコンテンツに従って C を読み取る) を例として、async/await
を使用して次のように実現する。
const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);
async function read() {
await readFile(A, 'utf-8');
await readFile(B, 'utf-8');
await readFile(C, 'utf-8');
//code
}
read().then((data) => {
//code
}).catch(err => {
//code
});
まとめ
コールバック関数 ---> Promise ---> Generator ---> async/await.