最新のJavaScriptで非同期タスクを作成する

公開: 2022-03-10
簡単なまとめ↬この記事では、過去の時代における非同期実行を中心としたJavaScriptの進化と、それがコードの記述と読み取りの方法をどのように変えたかを探ります。 Web開発の始まりから始めて、最新の非同期パターンの例に至るまで進みます。

JavaScriptには、プログラミング言語として2つの主要な特徴があり、どちらもコードがどのように機能するかを理解するために重要です。 1つは同期の性質です。つまり、コードはほとんど読んだときに行ごとに実行され、2つ目はシングルスレッドであり、常に1つのコマンドのみが実行されます。

言語が進化するにつれて、非同期実行を可能にする新しいアーティファクトがシーンに登場しました。 開発者は、より複雑なアルゴリズムとデータフローを解決しながらさまざまなアプローチを試しました。その結果、新しいインターフェイスとパターンが出現しました。

同期実行とオブザーバーパターン

はじめに述べたように、JavaScriptは、ほとんどの場合、記述したコードを1行ずつ実行します。 最初の数年間でさえ、言語にはこのルールの例外がありましたが、それらは少数であり、すでに知っているかもしれません:HTTPリクエスト、DOMイベント、時間間隔。

 const button = document.querySelector('button'); // observe for user interaction button.addEventListener('click', function(e) { console.log('user click just happened!'); })

たとえば要素のクリックなどのイベントリスナーを追加し、ユーザーがこのインタラクションをトリガーすると、JavaScriptエンジンはイベントリスナーコールバックのタスクをキューに入れますが、現在のスタックに存在するものを実行し続けます。 そこに存在する呼び出しが完了すると、リスナーのコールバックが実行されます。

この動作は、Web開発者が非同期実行にアクセスする最初のアーティファクトであるネットワーク要求とタイマーで発生する動作と似ています。

これらはJavaScriptでの一般的な同期実行の例外でしたが、言語は依然としてシングルスレッドであり、takをキューに入れ、非同期で実行してからメインスレッドに戻ることができますが、実行できるコードは1つだけであることを理解することが重要です。一度に。

ジャンプした後もっと! 以下を読み続けてください↓

たとえば、ネットワークリクエストを確認してみましょう。

 var request = new XMLHttpRequest(); request.open('GET', '//some.api.at/server', true); // observe for server response request.onreadystatechange = function() { if (request.readyState === 4 && request.status === 200) { console.log(request.responseText); } } request.send();

サーバーが復帰すると、 onreadystatechangeに割り当てられたメソッドのタスクがキューに入れられます(コードの実行はメインスレッドで続行されます)。

JavaScriptエンジンがタスクをキューに入れ、実行スレッドを処理する方法を説明することは、カバーするのが複雑なトピックであり、おそらくそれ自体の記事に値します。 それでも、「とにかくイベントループとは何ですか?」を視聴することをお勧めします。 あなたがより良い理解を得るのを助けるためにフィリップロバーツによって。

いずれの場合も、外部のイベントに対応しています。 一定の時間間隔に達した、ユーザーアクションまたはサーバー応答。 非同期タスク自体を作成することはできませんでした。私たちは常に、手の届かないところに発生が発生しているのを観察していました。

このため、このように形成されたコードはオブザーバーパターンと呼ばれ、この場合はaddEventListenerインターフェイスでより適切に表現されます。 すぐに、このパターンを公開するイベントエミッタライブラリまたはフレームワークが繁栄しました。

Node.jsとイベントエミッター

良い例はNode.jsで、このページはそれ自体を「非同期のイベント駆動型JavaScriptランタイム」と表現しているため、イベントエミッターとコールバックは第一級市民でした。 EventEmitterコンストラクターもすでに実装されていました。

 const EventEmitter = require('events'); const emitter = new EventEmitter(); // respond to events emitter.on('greeting', (message) => console.log(message)); // send events emitter.emit('greeting', 'Hi there!');

これは、非同期実行の継続的なアプローチであるだけでなく、そのエコシステムのコアパターンと慣習でもありました。 Node.jsは、Webの外でも、別の環境でJavaScriptを作成する新時代を切り開きました。 その結果、新しいディレクトリの作成やファイルの書き込みなど、他の非同期の状況が発生する可能性がありました。

 const { mkdir, writeFile } = require('fs'); const styles = 'body { background: #ffdead; }'; mkdir('./assets/', (error) => { if (!error) { writeFile('assets/main.css', styles, 'utf-8', (error) => { if (!error) console.log('stylesheet created'); }) } })

コールバックが最初の引数としてerrorを受け取ることに気付くかもしれません。応答データが期待される場合、それは2番目の引数として行われます。 これはエラーファーストコールバックパターンと呼ばれ、作成者と寄稿者が独自のパッケージとライブラリに採用する規則になりました。

約束と無限のコールバックチェーン

Web開発が解決すべきより複雑な問題に直面するにつれて、より優れた非同期アーティファクトの必要性が現れました。 最後のコードスニペットを見ると、タスクの数が増えるにつれてスケーリングがうまくいかないコールバックチェーンが繰り返されていることがわかります。

たとえば、ファイルの読み取りとスタイルの前処理の2つのステップだけを追加しましょう。

 const { mkdir, writeFile, readFile } = require('fs'); const less = require('less') readFile('./main.less', 'utf-8', (error, data) => { if (error) throw error less.render(data, (lessError, output) => { if (lessError) throw lessError mkdir('./assets/', (dirError) => { if (dirError) throw dirError writeFile('assets/main.css', output.css, 'utf-8', (writeError) => { if (writeError) throw writeError console.log('stylesheet created'); }) }) }) })

私たちが書いているプログラムがより複雑になるにつれて、複数のコールバックチェーンと繰り返されるエラー処理のために、コードが人間の目にはわかりにくくなることがわかります。

約束、ラッパー、チェーンパターン

Promisesは、JavaScript言語への新しい追加として最初に発表されたときはあまり注目されていませんでした。他の言語が数十年前に同様の実装を行っていたため、新しい概念ではありません。 真実は、彼らは私がその出現以来取り組んだほとんどのプロジェクトのセマンティクスと構造を大きく変えることが判明したということです。

Promisesは、開発者が非同期コードを作成するための組み込みソリューションを導入しただけでなく、 fetchなどのWeb仕様の後の新機能の構築ベースとして機能するWeb開発の新しい段階を開きました。

コールバックアプローチからpromiseベースのメソッドへのメソッドの移行は、プロジェクト(ライブラリやブラウザーなど)でますます一般的になり、Node.jsでさえゆっくりとそれらに移行し始めました。

たとえば、NodeのreadFileメソッドをラップしてみましょう。

 const { readFile } = require('fs'); const asyncReadFile = (path, options) => { return new Promise((resolve, reject) => { readFile(path, options, (error, data) => { if (error) reject(error); else resolve(data); }) }); }

ここでは、Promiseコンストラクター内で実行し、メソッドの結果が成功したときにresolveを呼び出し、エラーオブジェクトが定義されたときにrejectことで、コールバックを隠します。

メソッドがPromiseオブジェクトを返す場合、関数をthenに渡すことで、成功した解決を追跡できます。その引数は、Promiseが解決された値(この場合はdata )です。

メソッド中にエラーがスローされた場合、 catch関数が呼び出されます(存在する場合)。

Promisesがどのように機能するかをより深く理解する必要がある場合は、Jake ArchibaldがGoogleのWeb開発ブログに書いた「JavaScriptPromises:AnIntroduction」の記事をお勧めします。

これで、これらの新しいメソッドを使用して、コールバックチェーンを回避できます。

 asyncRead('./main.less', 'utf-8') .then(data => console.log('file content', data)) .catch(error => console.error('something went wrong', error))

非同期タスクを作成するネイティブな方法と、考えられる結果をフォローアップするための明確なインターフェイスを備えているため、業界はオブザーバーパターンから脱却することができました。 Promiseベースのものは、読み取り不能でエラーが発生しやすいコードを解決するように見えました。

より良い構文の強調表示またはより明確なエラーメッセージがコーディング中に役立つため、推論しやすいコードは、それを読む開発者にとってより予測可能になり、実行パスのより良い画像により、起こりうる落とし穴を見つけやすくなります。

Promisesの採用はコミュニティで非常にグローバルであったため、Node.jsは組み込みバージョンのI / Oメソッドを迅速にリリースして、 fs.promisesからファイル操作をインポートするなどのPromiseオブジェクトを返します。

エラーファーストのコールバックパターンに従った関数をラップし、それをPromiseベースの関数に変換するためのpromisifyも提供しました。

しかし、Promisesはすべての場合に役立ちますか?

Promisesで記述されたスタイルの前処理タスクを再想像してみましょう。

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less') readFile('./main.less', 'utf-8') .then(less.render) .then(result => mkdir('./assets') .then(() => writeFile('assets/main.css', result.css, 'utf-8')) ) .catch(error => console.error(error))

特にcatchに依存しているため、エラー処理の周りでコードの冗長性が明らかに減少していますが、Promisesは、アクションの連結に直接関連する明確なコードインデントを提供できませんでした。

これは、実際には、 readFileが呼び出された後の最初のthenステートメントで実現されます。 これらの行の後に起こることは、後で結果をファイルに書き込むために、最初にディレクトリを作成できる新しいスコープを作成する必要があることです。 これにより、インデントのリズムが崩れ、指示の順序を一目で判断するのが容易になりません。

これを解決する方法は、これを処理し、メソッドの正しい連結を可能にするカスタムメソッドをプリベイクすることですが、タスクを達成するために必要なものをすでに持っているように見えるコードに、もう1つの複雑さを導入します欲しい。

これはサンプルプログラムであり、いくつかの方法を管理しており、それらはすべて業界の慣習に従っていますが、常にそうであるとは限りません。 より複雑な連結や異なる形状のライブラリの導入により、コードスタイルが簡単に壊れてしまう可能性があります。

嬉しいことに、JavaScriptコミュニティは他の言語構文から再び学び、非同期タスクの連結が同期コードほど快適でなく、読みにくい場合に役立つ表記を追加しました。

Async And Await

Promiseは、実行時に未解決の値として定義され、 Promiseのインスタンスを作成することは、このアーティファクトの明示的な呼び出しです。

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less') readFile('./main.less', 'utf-8') .then(less.render) .then(result => mkdir('./assets') .then(() => { writeFile('assets/main.css', result.css, 'utf-8') })) .catch(error => console.error(error))

asyncメソッド内では、 await予約語を使用して、実行を続行する前にPromiseの解決を決定できます。

この構文を使用して、再検討またはコードスニペットを見てみましょう。

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less') async function processLess() { const content = await readFile('./main.less', 'utf-8') const result = await less.render(content) await mkdir('./assets') await writeFile('assets/main.css', result.css, 'utf-8') } processLess()

今日、非同期関数のスコープ外でawaitを使用することはできないため、すべてのコードをメソッドに移動する必要があることに注意してください。

asyncメソッドがawaitステートメントを見つけるたびに、進行中の値またはpromiseが解決されるまで実行を停止します。

非同期実行にもかかわらず、非同期/待機表記を使用することの明らかな結果があります。コードは同期であるかのように見えます。これは、開発者がよく見て推論するものです。

エラー処理はどうですか? そのために、私たちはその言語で長い間存在してきたステートメントを使用し、 trycatchを実行します。

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less'); async function processLess() { try { const content = await readFile('./main.less', 'utf-8') const result = await less.render(content) await mkdir('./assets') await writeFile('assets/main.css', result.css, 'utf-8') } catch(e) { console.error(e) } } processLess()

プロセスでスローされたエラーは、 catchステートメント内のコードによって処理されるので安心してください。 エラー処理を処理する中心的な場所がありますが、今では読みやすく、理解しやすいコードがあります。

結果として値を返すアクションを実行する場合、コードのリズムを崩さないmkdirなどの変数に格納する必要はありません。 また、後のステップでresultの値にアクセスするために新しいスコープを作成する必要もありません。

Promisesは、JavaScriptで非同期/待機表記を有効にするために必要な、言語で導入された基本的なアーティファクトであると言っても過言ではありません。これは、最新のブラウザーと最新バージョンのNode.jsの両方で使用できます。

最近JSConfで、Nodeの作成者であり最初の寄稿者であるRyan Dahlは、主にNodeの目標がイベント駆動型サーバーとファイル管理を作成することであり、Observerパターンの方が適しているため、初期の開発でPromisesに固執しなかったことを後悔しました。

結論

PromisesがWeb開発の世界に導入されたことで、コード内のアクションをキューに入れる方法が変わり、コードの実行についての推論方法や、ライブラリとパッケージの作成方法が変わりました。

しかし、コールバックのチェーンから離れることは解決が難しいので、メソッドを渡さなければthenないことは、オブザーバーパターンと主要ベンダーによって採用されたアプローチに長年慣れていた後、思考の列から離れるのに役立たなかったと思いますNode.jsのようなコミュニティで。

Nolan LawsonがPromise連結での間違った使用についての優れた記事で述べているように、古いコールバックの習慣は一生懸命に死にます! 彼は後でこれらの落とし穴のいくつかを回避する方法を説明します。

Promisesは、非同期タスクを自然に生成するための中間ステップとして必要だったと思いますが、より良いコードパターンを進めるのにあまり役立ちませんでした。実際には、より適応性が高く、改善された言語構文が必要になる場合があります。

JavaScriptを使用してより複雑なパズルを解こうとすると、より成熟した言語の必要性がわかり、以前はWebで見たことのないアーキテクチャとパターンを試します。

「「

私たちは常にJavaScriptガバナンスをWebの外に拡張し、より複雑なパズルを解こうとしているため、ECMAScript仕様が何年にもわたってどのように見えるかはまだわかりません。

これらのパズルのいくつかをより単純なプログラムに変えるために言語から正確に何が必要になるかを今は言うのは難しいですが、WebとJavaScript自体が物事を動かし、課題や新しい環境に適応しようとしていることに満足しています。 今のところ、JavaScriptは、10年以上前にブラウザーでコードを書き始めたときよりも、非同期に適した場所だと感じています。

参考文献

  • 「JavaScriptの約束:はじめに」 Jake Archibald
  • 「PromiseAnti-Patterns」、 Bluebirdライブラリのドキュメント
  • 「私たちは約束に問題があります」とノーラン・ローソン