LoginSignup
0

More than 1 year has passed since last update.

JavaScriptでのAsynchronous(非同期)勉強ノート

Last updated at Posted at 2022-05-02

初めに

今回は非同期通信について自分なりにまとめてみました。
前提としてほかのアジェンダも触れていますが、主な参考資料はこちらです。
イベントループとは一体何ですか? | Philip Roberts | JSConf EU

Asynchronous VS. Synchronous

Asynchronous(非同期):同調しない。
 → 自分と他人のやることに分けて、同時進行する。
Synchronous(同期):同調する。
 → みんな同調して前の人の仕事が終わるまで待ち続ける。

Node.js VS. Browser runtime

Node.jsもブラウザもJavaScriptのruntime、Node.jsはChromeのV8エンジンを基にして作られたruntimeです。

Node.jsが提供するインタフェース(file system、fs)を通して、JavaScriptを使ってファイルの読み込みができるようになったり、いろんなモジュール(os, http, setTimeout)が使えようになったりする。これでサーバも作れる。

Browserで使える機能がNode.jsより少ないんですが、同じくJavaScriptを使ってXMLHttpRequest, fetch, document, setTimeout等々でレンダリングを行う。

(違うruntimeであれば、同じsetTimeoutメソッドがあっても背景にある実行する動作やメカニズムは違っている。)

Node.jsの角度からの非同期や同期

動作を見るために、
Node.js fs.readFileSync() Method
Node.js fs.readFile() Method
を使ってデモしていきたいと思います。
まずは同期の動作、

// fs.readFileSync(path, options)
// a Synchronous method

// include fs module
const fs = require('fs')

// call readFileSync() to read input.txt file
const data = fs.readFileSync('./input.txt', { encoding: 'utf8', flag: 'r' })

// display the file data
console.log(data) // hello, world

fsモジュールを導入してから、readFileSync()メソッドを使ってファイルを読み込み、最後はファイル内容を出力する。
ファイルが小さいから問題にはならないが、大きかったらファイルの読み込み時間につれて最後の出力まで時間がかかってしまい、それに読み込みしている間、何もできないし待ち続けるしかない。
これがblocking(遮断)、Synchronous(同期)の一番厄介なところです。

その反対語はnon-blocking、つまり非同期のメソッドredFile()を使って、一緒にファイルを読み込みしたら、

// fs.readFileSync(path, options) // a Synchronous method
// fs.readFile(filename, encoding, callback_function) // a Asynchronous method

// include fs module
const fs = require('fs')

// call readFile() to read input1.txt file
fs.readFile('./input1.txt', 'utf8', function (err, data) {
  // display input1
  if (err) {
    console.log(err)
  } else {
    console.log(data)
  }
})

// call readFileSync() to read input2.txt file
const data = fs.readFileSync('./input2.txt', { encoding: 'utf8', flag: 'r' })

// display input2
console.log(data)

// hello, world(input2.txt file)
// hello, world(input1.txt file)

ここで出力順番としては、readFileSync()が先にinput2を出力してから、readFile()がinput1を出力する。
これはreadFile()非同期の機能を使ってファイルを読み込みながら、下のreadFileSync()へ継続してinput2を読み込みし出力が終わって、またreadFile()に戻りinput1の読み込み結果をcallbackに任し出力するわけです。

Blocking methods execute synchronously and non-blocking methods execute asynchronously.

Overview of Blocking vs Non-Blocking | Node.js

(別のプログラム言語ではnon-blockingasynchronouslyが同一視しないんですが、JavaScriptではコンセプトとして共通していると思います。)

補足:非同期関数式(callback)のパラメータは、書き方としてerror firstが決まり文句みたいに使われている。returnがいくつあるのに対し、errorが一つだけなわけです。また、エラーが発生するとき一番最初にお知らせしてほしい、そうしないとasynchronously(非同期)処理が下のコードに継続していってしまい、どこにエラーが出たのか判断しにくくなります。
なので、このような書き方がよく見られます。

const fs = require('fs')
fs.readFile('./input1.txt', 'utf8', (err, data) => {
  // if err occur, return
  if (err) {
    console.log(err)
    return
  }
  console.log(data)
})

Browser(Chrome)の角度からの非同期や同期

まずはブラウザの内部がどのようにプロセスとスレッドを構築していくかを。ここではChromeの内部でどのように動いているかを自分なりにまとめていきたいと思います。

A process can be described as an application’s executing program. A thread is the one that lives inside of process and executes any part of its process's program.

ブラウザでは、いろんなプロセスがおり、各自のプログラム処理を行っている。

When you start an application, a process is created. The program might create thread(s) to help it do work, but that's optional.

アプリケーションを一旦実行すると、プロセスが設立されその中にスレッドたちが造り出されてプロセスを手伝うようになるが、もちろんこれはアプリケーションにより違う場合もあります。(一定のルールに従うことではありません)

So how is a web browser built using processes and threads? Well, it could be one process with many different threads or many different processes with a few threads communicating over IPC.
The important thing to note here is that these different architectures are implementation details. There is no standard specification on how one might build a web browser. One browser’s approach may be completely different from another.

ブラウザにはプロセス間通信を通して、一つのプロセスにたくさんのスレッドか、たくさんのプロセスにいくつのスレッドで実行していくかもしれません。一番注目してほしいのは、これら違う設計では実行内容も違ってくる。スタンダードみたい具体的なルールはありませんから。なのでブラウザの取り組み方によって変わってきます。

Inside look at modern web browser (part 1) - Chrome Developers

そしてブラウザ(Chrome)の中、Javascriptの実行を担当するのがmain thread、プロセスも画面のレンダリングもmain threadを通さなければなりません。すると、

When the HTML parser finds a <script> tag, it pauses the parsing of the HTML document and has to load, parse, and execute the JavaScript code. Why? because JavaScript can change the shape of the document using things like document.write() which changes the entire DOM structure (overview of the parsing model in the HTML spec has a nice diagram). This is why the HTML parser has to wait for JavaScript to run before it can resume parsing of the HTML document.

HTMLパーサー(解析)が一旦<script>タグを見つけたら、HTML文書解析を中止し、JavaScriptファイルを読み込みし実行しなければなりません。なぜかというと、JavaScriptはHTML文書内容を変えることができるからです。たとえばdocument.write()がDOMツリー構造を変えてしまうことと同じように。これがパーサーがHTML文書を一旦中止して、JavaScriptが実行完了まで待たなければならない理由です。

There are many ways web developers can send hints to the browser in order to load resources nicely. If your JavaScript does not use document.write(), you can add async or defer attribute to the <script> tag. The browser then loads and runs the JavaScript code asynchronously and does not block the parsing. You may also use JavaScript module if that's suitable. is a way to inform browser that the resource is definitely needed for current navigation and you would like to download as soon as possible.

ここではリソースの読み込みにやさしい作法がいくつあります。もしJavaScriptファイルではdocument.write()を使用しない場合、scriptタグにはasyncdefer属性を入れてたら、ブラウザがJavaScriptコードの読み込みが非同期になり、文書解析も遮断されずに済みます。また、JavaScript moduleにも使っていいのです。<link rel="preload">を使えば、「このリソースが絶対必要だから、一番さきにダウンロードしなきゃいけないのだ!」ってブラウザに伝えるようになります。

Inside look at modern web browser (part 3) - Chrome Developers

JavaScriptの読み込みがもたらしたHTML文書解析の遮断は、main threadへ直接にblockingしてしまうので(全作業が一時中断)、完了まで画面が固まって動かない状態になっている。

⇒ 以上のようにいろいろな理由があって、Synchronous(同期)がもたらした問題を解決するため、
AJAX(Asynchronous JavaScript and XML)が誕生した。

(参考資料が言及したようにasyncdefer属性を使えばJavaScriptファイルの読み込みにはmain threadの動きに妨げをなくしますが、JavaScript自体の書き方も非同期にしなければ、インタアクティブな操作でリスポンスを受け取るまで、画面が一時停止するようになってしまいます。後述でその理由を説明します。)

JavaScriptでの非同期の実現とは

ここからは前述したイベントループとは一体何ですか? | Philip Roberts | JSConf EUの話に基づいて自分なりにまとめていきたいと思います。
(映像が大変分かりやすく説明しているのでぜひご覧ください。)

JSとブラウザの非同期の実現には、

  • call stack
  • WebAPIs
  • callback queue
  • event loop

がブラウザの背後で動いている上に成り立つことです。

まず、DOM(document)、AJAX(XMLHttpRequest)、setTimeoutなど、これらがブラウザから提供されたWebAPIです。確かにJSを用いて作られたメソッドですが、V8エンジンのランタイムに入っていません。なので、これらのメソッドを使う時、どういう順番で処理プロセスされていくか、この映像で一番重要なポイントだと思います。

Call Stack

one thread == one call stack == one thing at a time
JSでは、一回で処理できることは一つしかない。
stackというのは、処理プロセスが呼び出される順番によって、下から上に積み重ねていくものです。
全てのプロセスは、JSプログラムの本体(入口)としてmain()関数が呼び出され、main()は全ての処理が終わるまで常に一番下にある。

// demo
function multiply(a, b) {
  return a * b
}

function square(n) {
  return multiply(n, n)
}

function printSquare(n) {
  let squared = square(n)
  console.log(squared)
}

printSquare(4)

multiply(a, b)
↑ ↓ return (*pop from stack)
square(n)
↑ ↓ return (*pop from stack)
printSquare(4)
↑ ↓ console.log (*pop from stack)
main() (*done) (anonymous function in devTools)

(映像にはほかの例もあるけど飛ばしました)
→ この部分がCall Stackでスタックのプロセスがどう処理していくかを説明しました。

blocking

そしてよくあるサイトのリスポンスとか、画像の読み込みとか遅くなったりして、そのとき何か起こったか?
→ ブロッキングです

(前述で話したSynchronous(同期)動作が実際にもたらした問題のなかみは、ランタイムでプロセス処理が遅いとか、Call Stack上のプロセスが延々と終わらないとか、これらが最終的にブロッキングという言葉に集約できてるんです。)

なので、ブラウザでasyncdefer属性を使って事前読み込み処理(これも時間がかかることをお忘れずに)を終わらせても、新しいリクエストを送ったら、Synchronous同期動作がまたブロッキングを引き起こしてしまい、レンダリングができないため画面の更新速度が下がってフリーズしたように見える。

→ 解決方法としては、Asynchronous Callback(非同期コールバック)

Asynchronous Callback

基本的にブラウザでもNode.jsでも、すべての関数式がAsynchronous(非同期)に作られて、ブロッキングをもたらさないんです。(必要なときだけ呼び出されるから)

そしたら、DOM(document)、AJAX(XMLHttpRequest)、setTimeoutなどブラウザが提供してくれたWebAPIを使ったらどうなるでしょうか。

ここで映像のようにsetTimeoutを例にしてみると、

console.log('let us test!')

setTimeout(function () {
  console.log('this is a setTimeout demo with 0 second')
}, 0)

console.log('JSConfEU')

/*
let us test!
JSConfEU
this is a setTimeout demo with 0 second
*/

実際はsetTimeoutの秒数と関係なく、どんな設定でも同じ結果になります。これはJavaScriptがsingle-threaded(単一スレッド/シングルスレッド)、一回は一つのプロセスしか処理できないからです。
なのでAsynchronous(非同期)というのは、ブラウザが背後でほかの処理をしてくれているからこそ、実現できます。
それが、

  • WebAPIs
  • callback queue
  • event loop

のおかげです。

ブラウザがWebAPIsで非同期の処理を行い、処理結果をcallback queueという行列に入れてstackでの処理が終わるまで待ち続ける。
そしてstackが空状態になった途端、event loopがその隙間を見てcallback queue行列からWebAPIsの処理結果をstackに引き上げ、非同期処理を実現していくわけです。

実際はJSの動作は変わらず一回に一つしか処理できないんですが、ブラウザが別のところで非同期処理を行って結果を一時保留しながら、タイミングをはかってレンダリングしていくので、画面が凍結したように見えなくなります(非同期)。

コードの非同期処理どう行っているか、
気になる方がいたら、発表者さんが作ったデモサイトにご参考いただければと思います。
Loupe - Philip Roberts

参考資料まとめ

主なリソース
イベントループとは一体何ですか? | Philip Roberts | JSConf EU
Overview of Blocking vs Non-Blocking | Node.js
Inside look at modern web browser (part 1) - Chrome Developers
Inside look at modern web browser (part 3) - Chrome Developers
Loupe - Philip Roberts

デモに使われたメソッドたち
Node.js fs.readFileSync() Method
Node.js fs.readFile() Method
setTimeout() - Web API | MDN

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0