JavaScript:非同期プログラミングメソッド

作成者がこのコードを読んでいる人を混乱させることを求めていない同期JavaScriptコードは、通常、単純で明確に見えます。 それを構成するコマンドは、プログラムのテキストでそれらが従う順序で実行されます。 変数と関数の宣言を上げるのは少しわかりにくいかもしれませんが、このJS機能を問題に変えるには非常に一生懸命努力する必要があります。 同期JavaScriptコードには、重大な欠点が1つしかありません。つまり、それだけでは十分ではありません。







ほぼすべての有用なJSプログラムは、非同期開発メソッドを使用して記述されています。 ここで、コールバック関数が口語的に作用します-「コールバック」。 これは、Promise、または通常Promiseと呼ばれるPromiseオブジェクトが使用される場所です。 ここで、ジェネレーターとasync / awaitコンストラクトを実行できます。 非同期コードは、同期と比較して、通常、書き込み、読み取り、および保守が困難です。 コールバック地獄のような完全に不気味な構造に変わることもあります。 しかし、それなしではできません。



今日は、コールバック、プロミス、ジェネレーター、async / awaitコンストラクトの機能について説明し、シンプルで理解しやすく、効率的な非同期コードの書き方を考えます。



同期および非同期コードについて



同期JSコードと非同期JSコードのフラグメントを見てみましょう。 たとえば、通常の同期コードは次のとおりです。



console.log('1') console.log('2') console.log('3')
      
      





彼は、それほど困難なく、コンソールに1から3までの数字を表示します。



これでコードは非同期になりました:



 console.log('1') setTimeout(function afterTwoSeconds() { console.log('2') }, 2000) console.log('3')
      
      





ここでは、シーケンス1、3、2がすでに表示されています番号2は、 setTimeout



関数が呼び出されたときに設定されたタイマーイベントを処理するコールバックから派生しています。 この例では、2秒後にコールバックが呼び出されます。 アプリケーションは停止せず、これらの2秒が経過するのを待ちます。 代わりに、実行が継続され、タイマーが切れると、 afterTwoSeconds



関数が呼び出されます。



たぶん、あなたがJS開発者として始めたばかりなら、あなたは自問するかもしれません。 おそらく非同期コードを同期に再作成することは可能ですか?」 これらの質問に対する答えを探しましょう。



問題の声明



GitHubユーザーを見つけて、彼のリポジトリに関するデータをダウンロードするタスクに直面しているとします。 ここでの主な問題は、正確なユーザー名がわからないことです。そのため、探しているものとそのリポジトリに似た名前を持つすべてのユーザーをリストする必要があります。



インターフェイスに関しては、 単純なものに制限しています。





GitHubユーザーおよび対応するリポジトリ用のシンプルな検索インターフェイス



例では、クエリはXMLHttpRequest



(XHR)を使用して実行されますが、jQuery( $.ajax



)、またはfetch



関数の使用に基づくより現代的な標準アプローチを使用できます。 これらは両方とも、約束の使用に要約されます。 コードは、旅行に応じて変更されますが、最初の例では、この例は次のとおりです。



 //  url   -  'https://api.github.com/users/daspinola/repos' function request(url) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) {   if (xhr.readyState === 4) {     if (xhr.status === 200) {      //          } else {      //           }   } } xhr.ontimeout = function () {   //      ,   ,     } xhr.open('get', url, true) xhr.send(); }
      
      





これらの例では、最終的にサーバーから得られるものやその処理方法ではなく、非同期設計で使用できるさまざまなアプローチを使用したコード自体の編成が重要であることに注意してください。



コールバック関数



JSの関数では、他の関数への引数として渡すなど、多くのことができます。 通常、これは、何らかのプロセスの完了後に転送された関数を呼び出すために行われますが、これには時間がかかる場合があります。 コールバック関数についてです。 以下に簡単な例を示します。



 //   "doThis"      ,    -   "andThenThis".  "doThis"  ,   ,  ,   ,   "andThenThis". doThis(andThenThis) //  "doThis"         "callback" , ,   ,      function andThenThis() { console.log('and then this') } //  ,      ,   , "callback" -     function doThis(callback) { console.log('this first') //  ,  ,      ,  ,      , '()',     callback() }
      
      





このアプローチを使用して問題を解決するために、次のrequest



関数を作成できます。



 function request(url, callback) { const xhr = new XMLHttpRequest(); xhr.timeout = 2000; xhr.onreadystatechange = function(e) {   if (xhr.readyState === 4) {     if (xhr.status === 200) {      callback(null, xhr.response)     } else {      callback(xhr.status, null)     }   } } xhr.ontimeout = function () {  console.log('Timeout') } xhr.open('get', url, true) xhr.send(); }
      
      





現在、リクエストを実行するための関数はcallback



パラメータを受け入れるため、リクエストを実行してサーバーレスポンスを受信した後、エラーが発生した場合、および操作が正常に完了した場合にコールバックが呼び出されます。



 const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` request(userGet, function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) {   request(user.repos_url, function handleReposList(err, repos) {     if (err) throw err     //      }) }) })
      
      





ここで何が起こるかを分析しましょう:





オブジェクトの最初のパラメーターとしてエラーを使用することは、特にNode.jsを使用した開発では広く行われていることに注意してください。



コードをより完全に見て、エラー処理ツールを装備し、コールバック関数の定義をクエリ実行コードから分離すると、プログラムの可読性が向上します。次のようになります。



 try { request(userGet, handleUsersList) } catch (e) { console.error('Request boom! ', e) } function handleUsersList(error, users) { if (error) throw error const list = JSON.parse(users).items list.forEach(function(user) {   request(user.repos_url, handleReposList) }) } function handleReposList(err, repos) { if (err) throw err //     console.log('My very few repos', repos) }
      
      





このアプローチは機能しますが、それを使用すると、クエリレースの状態やエラー処理の問題などの問題が発生するリスクがあります。 ただし、コールバックに関連する主な迷惑は、 forEach



ループで何が起こるかを考慮すると、ここでは3つですが、そのようなコードは読みにくく、保守が難しいということです。 同様の問題は、おそらくコールバック関数が登場した日から存在します;それはコールバック地獄として広く知られています。





すべての栄光の地獄コールバック。 ここから撮影した画像。



この場合、「競合状態」とは、ユーザーリポジトリに関するデータを取得する手順を制御しない状況を意味します。 私たちはすべてのユーザーのデータを要求しますが、これらの要求への回答が混在していることが判明する場合があります。 10番目のユーザーに対する答えが最初に来て、2番目のユーザーに対する答えが最後だとしましょう。 以下に、この問題の可能な解決策について説明します。



約束



promiseを使用すると、コードが読みやすくなります。 その結果、たとえば、新しい開発者がプロ​​ジェクトに来た場合、彼はすべてがそこでどのように配置されているかをすぐに理解します。



約束を作成するには、次の設計を使用できます。



 const myPromise = new Promise(function(resolve, reject) { //    if (codeIsFine) {   resolve('fine') } else {   reject('error') } }) myPromise .then(function whenOk(response) {   console.log(response)   return response }) .catch(function notOk(err) {   console.error(err) })
      
      





この例を見てみましょう:





約束を処理する際に覚えておくべきことがいくつかあります。





別々に定義された関数を使用せずにプロミスを作成できることに注意してください。プロミスの作成時の関数を記述します。 この例で示されているのは、Promiseを初期化する一般的な方法です。



理論上でふらつかないように、例に戻りましょう。 promiseを使用して書き換えます。



 function request(url) { return new Promise(function (resolve, reject) {   const xhr = new XMLHttpRequest();   xhr.timeout = 2000;   xhr.onreadystatechange = function(e) {     if (xhr.readyState === 4) {       if (xhr.status === 200) {         resolve(xhr.response)       } else {         reject(xhr.status)       }     }   }   xhr.ontimeout = function () {     reject('timeout')   }   xhr.open('get', url, true)   xhr.send(); }) }
      
      





このアプローチでは、 request



を呼び出すと、次のようなものが返されます。





これは保留中の約束です。 正常に解決するか拒否することができます。



次に、新しいrequest



関数を使用して、残りのコードを書き直します。



 const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const myPromise = request(userGet) console.log('will be pending when logged', myPromise) myPromise .then(function handleUsersList(users) {   console.log('when resolve is found it comes here with the response, in this case users ', users)   const list = JSON.parse(users).items   return Promise.all(list.map(function(user) {     return request(user.repos_url)   })) }) .then(function handleReposList(repos) {   console.log('All users repos in an array', repos) }) .catch(function handleErrors(error) {   console.log('when a reject is executed it will come here ignoring the then statement ', error) })
      
      





ここでは、最初の表現に自分自身を見つけ、 .then



約束の解決に成功しました。 ユーザーのリストがあります。 2番目の.then



式では、リポジトリに配列を渡します。 何かがうまくいかなかった場合、 .catch



式になります。



このアプローチのおかげで、私たちはレースの状態と、そうすることで生じるいくつかの問題を把握しました。 ここではコールバックの地獄は観察されませんが、コードはまだそれほど読みやすくありません。 実際、この例では、コールバック関数の宣言を強調表示することにより、さらに改善することができます。



 const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const userRequest = request(userGet) //       ,        userRequest .then(handleUsersList) .then(repoRequest) .then(handleReposList) .catch(handleErrors) function handleUsersList(users) { return JSON.parse(users).items } function repoRequest(users) { return Promise.all(users.map(function(user) {   return request(user.repos_url) })) } function handleReposList(repos) { console.log('All users repos in an array', repos) } function handleErrors(error) { console.error('Something went wrong ', error) }
      
      





このアプローチでは、 .then



式のコールバック名を見ると、 userRequest



を呼び出す意味がuserRequest



ます。 コードは扱いやすく、読みやすいです。



実際、これは約束と呼ばれるものの氷山の一角にすぎません。 このトピックをより徹底的に掘り下げたい人に読むことをお勧めする資料があります。



発電機



私たちの問題を解決する別のアプローチは、しかし、あなたが頻繁に出会うことはないでしょうが、ジェネレーターです。 このトピックは他のトピックよりも少し複雑なので、勉強するには時期尚早だと感じた場合は、この資料の次のセクションにすぐに進むことができます。



ジェネレーター関数を定義するには、キーワードfunction



後にアスタリスク「*」を使用できます。 ジェネレーターを使用すると、非同期コードを同期に非常に似たものにすることができます。 たとえば、次のようになります。



 function* foo() { yield 1 const args = yield 2 console.log(args) } var fooIterator = foo() console.log(fooIterator.next().value) //  1 console.log(fooIterator.next().value) //  2 fooIterator.next('aParam') //    console.log      'aParam'
      
      





ここでのポイントは、ジェネレーターがreturn



代わりにyield



式を使用し、次の.next



イテレーターの呼び出しまで関数の実行を停止することです。 これは、promiseを解決するときに実行されるpromiseの.then



式に似ています。



次に、これらすべてをタスクに適用する方法を見てみましょう。 request



関数は次のとおりです。



 function request(url) { return function(callback) {   const xhr = new XMLHttpRequest();   xhr.onreadystatechange = function(e) {     if (xhr.readyState === 4) {       if (xhr.status === 200) {         callback(null, xhr.response)       } else {         callback(xhr.status, null)       }     }   }   xhr.ontimeout = function () {     console.log('timeout')   }   xhr.open('get', url, true)   xhr.send() } }
      
      





ここでは、いつものようにurl



引数を使用しますが、リクエストをすぐに実行するのではなく、レスポンスを処理するコールバック関数がある場合にのみリクエストを実行します。



ジェネレータは次のようになります。



 function* list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = yield request(userGet) yield for (let i = 0; i<=users.length; i++) {   yield request(users[i].repos_url) } }
      
      





ここで何が起こるかです:





これをすべて使用すると、次のようになります。



 try { const iterator = list() iterator.next().value(function handleUsersList(err, users) {   if (err) throw err   const list = JSON.parse(users).items     //       iterator.next(list)     list.forEach(function(user) {     iterator.next().value(function userRepos(error, repos) {       if (error) throw repos       //              console.log(user, JSON.parse(repos))     })   }) }) } catch (e) { console.error(e) }
      
      





ここで、各ユーザーのリポジトリのリストを個別に処理できます。 このコードを改善するために、すでに上で行ったように、コールバック関数を区別できます。



ジェネレーターについてはあいまいです。 一方では、ジェネレーターを見ることでコードに何を期待するかをすぐに理解できます。他方では、ジェネレーターの実行はコールバック地獄で発生する問題と同様の問題につながります。



ジェネレーターは比較的新しい機能であるため、古いバージョンのブラウザーでコードの使用を期待している場合、トランスパイラーでコードを処理する必要があることに注意してください。 さらに、非同期コードを記述するジェネレーターはあまり使用されないため、チーム開発に関与している場合、一部のプログラマーはそれらに慣れていない可能性があることに注意してください。

その場合、このトピックをよりよく理解することにした場合、ジェネレーターの内部構造に関する優れた資料があります。



非同期/待機



この方法は、ジェネレーターとプロミスの混合に似ています。 非同期に実行される関数をasync



で指定し、 await



を使用しawait



、対応するプロミスが解決されるのをコードのどの部分で待つかをシステムに指示するだけです。

いつものように、最初に簡単な例を示します。



 sumTwentyAfterTwoSeconds(10) .then(result => console.log('after 2 seconds', result)) async function sumTwentyAfterTwoSeconds(value) { const remainder = afterTwoSeconds(20) return value + await remainder } function afterTwoSeconds(value) { return new Promise(resolve => {   setTimeout(() => { resolve(value) }, 2000); }); }
      
      





ここで次のことが起こります。





async/await



コンストラクトで使用するrequest



関数を準備します。



 function request(url) { return new Promise(function(resolve, reject) {   const xhr = new XMLHttpRequest();   xhr.onreadystatechange = function(e) {     if (xhr.readyState === 4) {       if (xhr.status === 200) {         resolve(xhr.response)       } else {         reject(xhr.status)       }     }   }   xhr.ontimeout = function () {     reject('timeout')   }   xhr.open('get', url, true)   xhr.send() }) }
      
      





async



を使用しawait



関数を作成します。ここでawait



キーワードを使用します。



 async function list() { const userGet = `https://api.github.com/search/users?page=1&q=daspinola&type=Users` const users = await request(userGet) const usersList = JSON.parse(users).items usersList.forEach(async function (user) {   const repos = await request(user.repos_url)     handleRepoList(user, repos) }) } function handleRepoList(user, repos) { const userRepos = JSON.parse(repos) //       console.log(user, userRepos) }
      
      





そのため、リクエストを処理する非同期list



関数があります。 また、リポジトリのリストを作成するには、 forEach



ループにasync/await



コンストラクトが必要です。 すべてを呼び出すのは非常に簡単です。



 list() .catch(e => console.error(e))
      
      





このアプローチとプロミスの使用は、非同期プログラミングの私のお気に入りの方法です。 それらを使用して記述されたコードは、読み取りや編集に便利です。 async/await



詳細については、 こちらをご覧ください



async/await



のマイナス、およびジェネレーターのマイナスは、この設計が古いブラウザでサポートされていないことです。サーバー開発で使用するには、ノード8を使用する必要があります。



まとめ



ここで、 async/await



を使用して、マテリアルの先頭にある問題を解決するプロジェクトコードを確認できます。 話したことを適切に処理したい場合は、このコードと、説明したすべてのテクノロジーを試してください。



$.ajax



fetch



などの別のクエリ実行方法を使用して例を書き換えると、例が改善され簡潔になることに注意してください。 上記の方法を使用してコードの品質を改善する方法についてアイデアをお持ちの場合、それについて教えいただければ幸いです。



割り当てられたタスクの詳細に応じて、非同期/待機、コールバック、または異なるテクノロジーの混合を使用することが判明する場合があります。 実際、どの非同期開発手法を選択するかという質問に対する答えは、プロジェクトの機能によって異なります。 アプローチが、あなたと他のチームメンバーにとって理解しやすい(そして、しばらくすると明らかになる)保守しやすい読み取り可能なコードを使用して問題を解決できる場合、このアプローチが必要です。



親愛なる読者! 非同期JavaScriptコードを記述するためにどのようなテクニックを使用しますか?



All Articles