forEachとmapの違い【JavaScript】
forEachはundefinedを、mapは値を返す
表記のとおりです。
久しぶりにJSを使ったら、ちょっとつまづいたのでメモです。
const arr = [1, 2, 3].forEach(value => { return value * 2; }) console.log(arr) // undefined
const arr = [1, 2, 3].map(value => { return value * 2; }) console.log(arr) // [2, 4, 6]
速度はmap
の方が速いらしいですね。
forEachの代替
for…of
というもの。
書き方はこんな感じです。
for (const value of [1, 2, 3]) { console.log(value * 2); }
参照
非道な非同期処理を理解したい【JavaScript】
目次
非同期処理って理解しづらいですよね。でも、分解して見ていけば、きっとわかるはず!
- 同期処理
- 非同期処理
- コールバック
- Promise
- async/await
- 参考
※ 注意
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
関数内の処理を中断します。結果が返ってくると、
処理は再開されます。await
はasync
関数内でしか指定できません。
ここで注意が必要なのは、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
を使って非同期処理を書くのが主流になってきているようです。
Promise
はasync/await
に至るまでの過渡的なものだった、という見方ができますが、当然async/await
よりもスマートな書き方が
出てきてもおかしくありません。そういうときは、「また勉強しなきゃいけないか」という気持ちではなく、「また便利なのが出てきた、やったー」なんて
気持ちを持つことが大事だと思います。
6. 参考
Docker ComposeでLAMP環境構築
この記事は今までの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
最初に見るべきはState
とSTATUS
です。Up
となっているでしょうか。
次にPorts
とPORTS
。docker-compose.yml
に書いたとおりにポートフォワーディングされていれば成功です。
DBセットアップ
まず、やらなくてはいけないのが、MySQLのコンテナのIPアドレスを調べることです。
docker exec -it mysql_comp_test /bin/bash
でコンテナに入って
hostname -i
でIPアドレスを確認できます。
確認したらexit
でホストマシンへ戻ってきます。
database.php
のhost=
の部分に確認したIPアドレスを入力します。
// mysqlコンテナ側のIPアドレスを入れてやらないといけない 'mysql:dbname=sample;host=xxx.xx.x.x;charset=utf8;',
続いてデータベースとテーブルを作成します。
phpMyAdminにhttp://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:8000
やhttp://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ファイルを流し込むことで、データベース定義やテーブル作成、データの挿入
を自動で行ってくれるそうです。