WebEngine

だらだらと綴る技術系メモ

非道な非同期処理を理解したい[JavaScript]

目次

非同期処理って理解しづらいですよね。でも、分解して見ていけば、きっとわかるはず!

  1. 同期処理
  2. 非同期処理
  3. コールバック
  4. Promise
  5. async/await
  6. 参考

※ 注意
Google Chormeでしかテストしていません。
各項目での説明やソースコードは参考にした記事をちょっと変えただけ、というものが多いです。それらのリンクは最後の「参考」に掲載してあります。

1. 同期処理

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

こんな処理があったとします。結果は順に 1、2、3 と表示される、一般的な 処理です。JavaScriptは、関数をキューに登録し、順番に1つずつ実行 していくみたいです。この処理では、log(1)log(2)log(3)と 順番に処理していくので結果も、それに倣って出力されるわけです。

JavaScriptでは、上から順にキューに登録していく処理を同期処理と呼ぶ ...みたいです。

2. 非同期処理

console.log(1);
setTimeout(() => { console.log(2); }, 1000);
console.log(3);

処理を遅らせるsetTimeoutを使っています。この場合、log(1)setTimeout()log(3)の順にキューに登録されます。

キューに登録されたとおり、まずlog(1)が実行され、1が出力されます。次に setTimeoutが実行され、タイマーにlog(2)が登録されます。つづいて log(3)が実行され、3が表示されます。ここまでは同期処理になります。

タイマーに設定されていたlog(2)は1000ミリ秒後にキューに登録されます。log(2)は、 最初の3つの関数とは別物としてキューに登録されるので、非同期処理と呼ばれます。

結果、最後にlog(2)が実行されて、1、3、2 と出力されるわけです。


この非同期で、独立してキューに登録されるという事実を知っておくと以下のような処理も理解できます。

console.log(1);
setTimeout(() => { console.log(2); }, 0);
console.log(3);

0ミリ秒後にセットされているので、非同期処理を知らない人は 1、2、3 と出力されると考えがちです。
しかし実際には、setTimeoutが実行されたあと、log(2)が独立してキューに登録されるので、0ミリ秒後となっていても 1、3、2 と出力されます。


非同期処理は、外部APIへのアクセスやDBまわりの処理等で使います。ざっくりいうとバックグラウンドでの処理に近いですね。

3. コールバック

JavaScriptでは関数の引数に関数を渡すことが可能です。
この機能を利用して、ある処理Aが無事終了した場合、引数に設定していた関数の処理Bを実行するといった処理の流れを 実装できます。非同期処理は、そうしなければならない処理なので必然的にコールバックが出てくるわけです。

コールバックは無名関数でなくとも良いです。

下記処理は上と同じ非同期処理で、結果も同じです。

const logFunc = function() {
    console.log(2);
}
console.log(1);
setTimeout(logFunc, 0);  // 先程は第1引数が無名関数だった
console.log(3);


無論、このsetTimeoutの部分が自作の関数でも良いです。

// 渡される引数の関数は何でも良い
function mainFunc(callback) {
    callback();  // 引数で渡した関数を実行
}
// コールバック関数
const argsFunc = function() {
    console.log("Hello, Callback!");
}
mainFunc(argsFunc);


4. Promise

非同期処理を書く際には、コールバックを記述する必要が出てくることを説明しましたが、コールバックを書く際に陥りがち なのがコールバック地獄です。そのコールバック地獄を回避するために生まれたのがPromiseです。

// Promiseオブジェクトを返す
function asyncFunc() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Hello!');
        }, 1000);
    });
}
// -- Promiseフロー --
asyncFunc().then(value => {
    console.log(value);  // 成功時処理
    console.log('asyncFunc success!');  // asyncFuncが終了するまでは実行されない
}).catch(error => {
    console.log(error);  // エラー時処理
});
// -- Promiseフロー --
console.log('asyncFunc run?');  // asyncFuncの実行が終わってなくても出力される

一定時間が経過するとHello!という文字列を返すasyncFunc関数があるとします。中では、Promiseオブジェクトを 返すように実装してあります。結果は次のとおり。

asyncFunc run?
Hello!
asyncFunc success!

非同期処理なので、まず、処理順が確定されていない最後のログasyncFunc run?が出力されます。このときasyncFunc success!は 出力されていませんよね? ということは、Promiseオブジェクトを返し、その返した関数をどう扱うか記述したフロー(ここではコメントアウト「Promiseフロー」 でくくった箇所を指す)では、Promiseオブジェクトがリターンされるまで処理が実行されないことが確約されます。

要はPromiseの登場で、安心して非同期処理が書けるようになったのでした。


Promiseは、jQueryのメソッドチェーンみたいに連続した非同期処理を記述することも可能です。

// 1秒後に引数の値を返す
function asyncDelay(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num);
        }, 1000);
    });
}
// 計算した値を返す
function asyncCalc() {
    let result = 0;
    return asyncDelay(5)
        .then(value => {
            console.log(value);
            result += value;
            return asyncDelay(10);
        })
        .then(value => {
            console.log(value);
            result *= value;
            return asyncDelay(20);
        })
        .then(value => {
            console.log(value);
            result += value;
            return result;
        });
}
// -- main ---
asyncCalc().then(value => {
    console.log(value);
    console.log("計算終了");
});
console.log('同期処理');
同期処理
5
10
20
70
計算終了


5. async/await

先程、連続した非同期処理をPromiseで記述しましたが、もうちょっとスタイリッシュに書けるんじゃないか、と思った方もいるのではないでしょうか。 そんなわけでできたのがasync/awaitです。

Promiseの項目で書いた連続する非同期処理をasync/awaitを使って実装したものがこちら。

// 1秒後に引数の値を返す
function asyncDelay(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(num);  // ログで確認するため追加
            resolve(num);
        }, 1000);
    });
}
// 計算した値を返す
async function asyncCalc() {
    return await asyncDelay(5) * await asyncDelay(10) + await asyncDelay(20);
}
// -- main ---
asyncCalc().then(value => {
    console.log(value);
    console.log("計算終了");
});
console.log('同期処理');

結果はPromiseを使って書いたソースコードと同じになるはずです。
asyncCalc内は1行で済ませていますが、次のように書くことも可能です。

// 計算した値を返す
async function asyncCalc() {
    const a = await asyncDelay(5);
    const b = await asyncDelay(10);
    const c = await asyncDelay(20);
    return a * b + c;
}


asyncは定義する関数の前につけます。こいつを装着した関数はコールされるとPromiseを返すようになります。
上記コードを見てもわかるとおり、Promiseを返しているのでthenなんかが使えています。

awaitを前につけると、つけた関数の値が返されるまでasync関数内の処理を中断します。結果が返ってくると、 処理は再開されます。awaitasync関数内でしか指定できません。

ここで注意が必要なのは、awaitで指定した関数で返ってくるのはPromiseではなく値だということです。

// 1秒後に引数の値を返す
function asyncFunc(num) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num);
        }, 1000);
    });
}
// awaitなし
async function checkAsyncFunc() {
    const a = asyncFunc(1);
    console.log(a);
    return a;
}
// -- main --
checkAsyncFunc().then(value => {
});

結果はPromiseがリターンされてきます。

Promise {<pending>}

awaitをつけるとどうなるか。

// awaitあり
async function checkAsyncFunc() {
    const a = await asyncFunc(1);
    console.log(a);
    return a;
}

今度はPromiseではなく、値が返ってきていることがわかります。

1

当たり前じゃん、と思う方もいると考えますが、ここら辺が曖昧だと、返ってきた値に対してthenなんかをつけて、エラーが 起きる、みたいなことがあります。


さて、ざっと非同期処理について見てきましたが、今はasync/awaitを使って非同期処理を書くのが主流になってきているようです。
Promiseasync/awaitに至るまでの過渡的なものだった、という見方ができますが、当然async/awaitよりもスマートな書き方が 出てきてもおかしくありません。そういうときは、「また勉強しなきゃいけないか」という気持ちではなく、「また便利なのが出てきた、やったー」なんて 気持ちを持つことが大事だと思います。

6. 参考


Docker ComposeでLAMP環境構築

f:id:web-engine:20171209214441p:plain

この記事は今までのDocker系記事のゴール地点になる位置づけになります。(まだまだ怪しいところたくさんあるけれど...)
Dockerは初めて、という方は以下の記事を順に追う形で読まれていくとぼんやりとイメージが湧いてくるんじゃないかと思います。

ところどころに我流な点があるかと思いますがご容赦下さい。

環境

  • Docker for mac
  • Docker version 17.09.0-ce, build afdb6d4
  • docker-compose version 1.16.1, build 6d1ac21

Docker composeとは

複数のコンテナを連携して開発ができるコマンド。
すごく便利。

ディレクトリ構成

作業用ディレクトリを下のような構成にします。

  - docker-compose.yml
  - php
    - Dockerfile
    - php.ini
  - html
    - database.php
    - fetchTester.php
    - index.php


docker-compose.yml

version: '3'
services:
  mysql:
    container_name: mysql_comp_test
    image: mysql:5.7
    ports:
      - 3333:3306
    environment:
      MYSQL_ROOT_PASSWORD: password
    volumes:
      - db:/var/lib/mysql
  phpmyadmin:
      container_name: phpmyadmin_comp_test
      image: phpmyadmin/phpmyadmin
      environment:
        - PMA_ARBITRARY=1
        - PMA_HOST=mysql
        - PMA_USER=root
        - PMA_PASSWORD=password
      links:
        - mysql
      ports:
        - 8080:80
      volumes:
        - /sessions
  php:
    container_name: php_apache_comp_test
    build: ./php
    ports:
      - 8000:80
    volumes:
      - ./html:/var/www/html
    depends_on:
      - mysql
volumes:
  db:

いらない記述などがあるような気がしますが、今回はとりあえずこれで済ませます。解説は後でします。

php/Dockerfile

FROM php:7-apache
COPY php.ini /usr/local/etc/php/
RUN apt-get update \
  && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng12-dev libmcrypt-dev \
  && docker-php-ext-install pdo_mysql mysqli mbstring gd iconv mcrypt


php/php.ini

[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"


html/index.php

<?php
require_once 'fetchTester.php';

$fetchtester = new FetchTester();
$fetchtester->fetch("testtable"); // 引数:テーブル名


html/fetchTester.php

<?php
require_once 'database.php';

class FetchTester
{
    private $pdo;

    function __construct()
    {
        $this->pdo = Database::connectDB();
    }

    public function fetch($table)
    {
        $stmt = $this->selectTable($table);
        while($row = $stmt->fetch()) {
            var_dump($row);
        }
    }

    private function selectTable($table)
    {
        $sql = 'SELECT * FROM ' . $table;
        $stmt = $this->pdo->query($sql);
        return $stmt;
    }
}


html/database.php

<?php
class Database
{
    public static function connectDB()
    {
        $pdo = '';
        try {
            $pdo = new PDO(
                // mysqlコンテナ側のIPアドレスを入れてやらないといけない
                'mysql:dbname=sample;host=xxx.xx.x.x;charset=utf8;',
                'root',
                'password',
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
                ]
            );
        }
        catch (PDOException $e) {
            header('Content-Type: text/plain; charset=UTF-8', true, 500);
            exit($e->getMessage());
        }
        return $pdo;
    }
}

htmlディレクトリ下のファイルはすべて、以前記事にした PDOのfetch系メソッド のものを再利用しています。

docekr-compose.yml

1. version

シンタックスバージョンです。現時点では3が最新。

2. services

この配下にある設定を1つのサービス(コンテナ)として認識します。 この配下にサービスを複数書いていく形になります。

3. container_name

コンテナの名称を設定。記述しない場合はサービス名などから自動的につくられますが、この名前がやたら長いものになったりならなかったり。

4. image

このイメージをもとにコンテナを作成します。イメージがなければプルしてきます。

5. ports

ホスト側のポート:コンテナ側のポートとすることで、ホスト側で指定したポート番号でアクセスできるようになります。

6. environment

コンテナを起動する際に渡す環境変数を設定します。この単語のスペルをenviroment書いて何度もビルドエラーを起こしたのは内緒の話。

7. volumes

ホスト側パス:コンテナ側パスで、ホストのディレクトリをコンテナにボリュームとしてマウントすることができます。

8. build

任意のディレクトリ下のファイルをビルドします。ここでは./php下をビルドする、という指示になります。

9. link、depends_on

コンテナをほかのサービスとリンクさせます。サービス名:エイリアス名でサービス名とリンク用エイリアスの両方を指定できます。


ビルド、ラン

docker-compose up -dでビルドしつつ起動させます。 -dでバックグラウンドでの起動となります。

処理が終了したらdocker-compose psで状態をチェックします。

       Name                      Command               State           Ports         
--------------------------------------------------------------------------------------
mysql_comp_test        docker-entrypoint.sh mysqld      Up      0.0.0.0:3333->3306/tcp
php_apache_comp_test   docker-php-entrypoint apac ...   Up      0.0.0.0:8000->80/tcp  
phpmyadmin_comp_test   /run.sh phpmyadmin               Up      0.0.0.0:8080->80/tcp  

docker ps -aでどうなるかも見てみます。

CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                    NAMES
783bdf5eb792        phpmyadmin/phpmyadmin   "/run.sh phpmyadmin"     4 minutes ago       Up 4 minutes        0.0.0.0:8080->80/tcp     phpmyadmin_comp_test
9db98d802fc7        dockerlamp_php          "docker-php-entryp..."   4 minutes ago       Up 4 minutes        0.0.0.0:8000->80/tcp     php_apache_comp_test
13655efc377f        mysql:5.7               "docker-entrypoint..."   4 minutes ago       Up 4 minutes        0.0.0.0:3333->3306/tcp   mysql_comp_test

最初に見るべきはStateSTATUSです。Upとなっているでしょうか。
次にPortsPORTSdocker-compose.ymlに書いたとおりにポートフォワーディングされていれば成功です。

DBセットアップ

まず、やらなくてはいけないのが、MySQLのコンテナのIPアドレスを調べることです。

docker exec -it mysql_comp_test /bin/bash

でコンテナに入って

hostname -i

IPアドレスを確認できます。 確認したらexitでホストマシンへ戻ってきます。

database.phphost=の部分に確認したIPアドレスを入力します。

// mysqlコンテナ側のIPアドレスを入れてやらないといけない
'mysql:dbname=sample;host=xxx.xx.x.x;charset=utf8;',


続いてデータベースとテーブルを作成します。 phpMyAdminhttp://localhost:8080でアクセスできます。コンテナに入って、コマンド処理で作業しても良いのですが、やはり面倒ですよね。

sampleというデータベース (utf8_general_ci) をつくって、その中に、testtableというテーブルを作成します。データを適当に入れておきましょう。

CREATE TABLE `testtable` (
    `id` int NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `name` varchar(10) NOT NULL
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8

いよいよhttp://localhost:8000へアクセスしてみます。

array(2) {
  ["id"]=>
  string(1) "1"
  ["name"]=>
  string(4) "hoge"
}
array(2) {
  ["id"]=>
  string(1) "2"
  ["name"]=>
  string(8) "hogehoge"
}

こんなふうにデータがダンプされれば成功です。

データが永続化されているか

docker-compose downで立ち上げたコンテナ群を終了させることができます。

今度はオプション-dを外してdocker-compose upで起動をしてみましょう。こうすることでdocker-compose downと打って終了する手間が省けるといえば省けます。 http://localhost:8000http://localhost:8080にアクセスすると、先ほどと同じ出力がされるはずです。DBのデータが消滅していないことがわかります。

プロセスはCtrl + cで終了することができます。

開発スピードアップ

index.phpを少々いじります。

<?php
// require_once 'fetchTester.php';

// $fetchtester = new FetchTester();
// $fetchtester->fetch("testtable"); // 引数:テーブル名
echo "Hello, Docker-compose!";

再度docker-compose upしてブラウザにアクセスすると、表示が変わるはずです。
前回の DockerfileでPHP7環境構築 の記事を踏まえて読んでくださっている方はわかると思いますが、すごく楽です。前回はブラウザに 表示させるだけ、ポートフォワーディングするだけ、で長いコマンドを打たなくてはいけなかったのが、 docker-compose.ymlへすべて書いてしまうことで解決しています。

このファイルを開発メンバで共有すれば、コマンドを幾つか打つだけで同じ開発環境が整うのも得点高いです。

改良の余地アリ

まずMySQLのコンテナのIPアドレスを調べて設定しなければいけないプロセスが面倒ですね。 IPアドレスを固定でセッティングするようなこともできるような気もするので上手くいけば手順を省略 できます。

また、今回はphpMyAdminを使ってみたかったので試行できませんでしたが、MySQLの公式イメージでは 特定の(既存の)ディレクトリにSQLファイルを流し込むことで、データベース定義やテーブル作成、データの挿入 を自動で行ってくれるそうです。

参考


DockerfileでPHP7環境構築

f:id:web-engine:20171023213836j:plain

今回、Docker HubにあるPHP公式リポジトリCreate a Dockerfile in your PHP projectの部分を参考にしました。

環境

mac OS Sierra上にDocker for macPHP7 + apacheの環境を構築していきます。今はnginxが主流となりつつありますが、トラブルシューティングしやすいように、今回は慣れているapacheを使います。

  • Docker version 17.06.1-ce

ディレクトリ構造

php7testというディレクトリのなかに以下の3つのファイルを作成

  • Dockerfile
  • src/index.php
  • config/php.ini


Dockerfile

FROM php:7.0-apache
MAINTAINER username

COPY config/php.ini /usr/local/etc/php/
COPY src/ /var/www/html/

Dockerfileをビルドすることで、オリジナルのイメージを作成することができます。

FROM php:7.0-apacheは、公式のphp:7.0-apacheというイメージを取得する、という意味です。まずイメージを取得し、それのイメージに新しく情報をつけ加えてきます。プログラミング的には、継承の感覚に近いかもしれません。このイメージを取得した時点で、PHP7 とapacheは入っているということです。

MAINTAINER usernameはDockerfileの製作者です。自身の名前を入れれば良いですが、必要なければカットしても良い情報です。

COPY config/php.ini /usr/local/etc/php/では、configディレクトリ内につくったオリジナルのPHP設定ファイルを、後々つくられるコンテナ側のOSの/usr/local/etc/php/下にコピーします。
COPY src/ /var/www/html/も同じようなものですね。

これで、一度イメージをビルドすれば、コンテナ作成 →動作確認→コンテナ破棄→ホスト側ファイルの修正→コンテナ作成→動作確認のサイクルを確立できます。(srcファイル下であれば、ですが)



src/index.php

<?php
phpinfo();

入っているPHPの情報をざっと確認したならこれですよね。


config/php.ini

date.timezone = "Asia/Tokyo"

PHPの設定ファイルです。最初は少しだけの情報にして、上手くいったら追加の設定を書き込んでいくのが安全です。

ビルド

Dockerfileが存在するディレクトリ下でコマンドを実行していきます。

docker build -t php7test .

php7testの部分はイメージ名です。つけたいイメージ名を書いてください。.をつけ忘れないようにしましょう。

終了したら、docker imagesでイメージが作成された確認しておきます。


ホスト側のブラウザで確認

docker run -v /Users/yourname/php7test/src/:/var/www/html -d -p 8000:80 --name php7cont php7test

php7testというイメージからphp7contというコンテナを作成しています。(コンテナ名を付ける場合は--nameが必要)

docker ps -aで上手くいっているかチェックしておきます。

PORTS0.0.0.0:8000->80/tcpとなっているでしょうか。0.0.0.0とはlocalhostのことですね。

http://localhost:8000にアクセスするとPHPの情報が表示されるはずです。バージョン7が入っていれば成功です。

ネット上の情報をみていると、localhostではダメな場合もあるようで、その場合は次のように、IPアドレスで指定するようですね。 (たぶん、dockerとホストのあいだにvagrantとかを挟んでいるのが原因)

IPアドレス:8000

IPアドレスifconfigコマンドで調べることができます。


肝心のコマンドが長いですが、分解すると見えてきます。

1. ホスト・コンテナ間でファイルを共有

docker run -v ホスト側パス:コンテナ側パス

なぜか僕の環境ではホスト側のパスだけフルパスで書かないと上手くいきませんでした。上手くいかないときは試してみてください。

2. コンテナ側のポートをホストでも使えるようにする(ポートフォワード)

docker run -p ホスト側ポート:コンテナ側ポート

3. デタッチモードでコンテナ起動(バックグラウンドで動作)

docker run -d イメージ


php.iniの設定が反映されているか確認

phpinfoで表示した部分をチェックしておきます。

f:id:web-engine:20171022214141p:plain デフォルトのタイムゾーンAsia/Tokyoになっていますね。上手くいっているみたいです。
今度イメージを作成するときは、ほかの設定もいじってみましょう。

サイクル体感

docker stop コンテナ名(コンテナID)でコンテナを停止できます。
docker rm コンテナ名(コンテナID)でコンテナを削除できます。
src/index.phpを書き換えて、もう一度docker run ~を実行してみましょう。
ブラウザに書き換えたindex.phpの内容が表示されれば、Dockerによる開発サイクルを実感できるはずです。

参考