Hello React !
DAY0
2021/09/23
目的
React.jsのチュートリアルを通じてReactの概念を理解し、最低限の知識を身に着ける。
Next.jsでのアプリ作成に向けての基礎知識を身につける。
背景
今まで、Laravelやvue.jsを主に使用してきたが、Reactのドキュメントを読んだ時に、設計思想に興味を持った。
また、TypeScriptに興味があり、バックエンドもフロントエンドもTypeScriptで書けるようになりたいと考えており、その上でもよりTypeScriptと相性の良いReactに興味を持った。
今まで、新しく技術を学ぶ時は、ドキュメントを読見込んだり、チュートリアルをしっかり行うのではなく、とりあえず書いて必要に応じてドキュメントを読むという学び方をしてきた。
この方法は効率は良かったかもしれないが、フレームワークの思想や概念を体系的に理解ができないという問題があると考えているため、今回はしっかりドキュメントを読んで、チュートリアルを行うという方法で勉強してく。
教材
計画
- React.jsのドキュメントを読む
- React.jsのチュートリアルをやる
Day 1
2021/09/23
やったこと
React.jsの公式ドキュメントを読む(延期)
Javascriptのドキュメントを読んで知識を深めた。(JavaScript 再入門編)
目的
Reactの設計思想について理解する(延期)
JavaScriptに対する理解を深めるため
Reactのチュートリアルに以下のような記載があり、読んでみたら面白かったので読んでみた。
React は JavaScript ライブラリなので、JavaScript 言語の基本的な理解があることを想定しています。あまり自信がない場合は、JavaScript のチュートリアルを一通り読んで知識レベルを確認し、このガイドを迷わず読み進められるようにしてください。
このドキュメントのタイトルが面白い
JavaScript 再入門: JavaScriptについて知っていると思っている人のための概要です。
確かに、JavaScriptについて知っている
と思っていたが、知らないことが多かったので寄り道をすることにした。
学んだこと
// infinity型(初めて知った)
1 / 0; // Infinity
-1 / 0; // -Infinity
// こんな関数あるんだ
'hello'.charAt(0); // "h"
'hello'.toUpperCase(); // "HELLO"
// 順番によって結果が異なる(そりゃそうだ)
'3' + 4 + 5; // "345"
3 + 4 + '5'; // "75"
// 配列の長さは一番大きな添字より 1 大きい値(これ知らなかった。危ない!)
var a = ['dog', 'cat', 'hen'];
a[100] = 'fox';
a.length; // 101
for...in
について
問題点
解決策
配列の繰り返し処理にはfor...of
を使う。
さらには、繰り返し処理にはfindやfilterなど目的に合ったものを使う。
for...in
やfor...of
、forEach
はただの繰り返しという意味しか持たないのでコードの意図が分かりにくい。
なので、filter
やfind
など目的にあった関数を使うと意図がわかりやすい。
また、単なる繰り返しでもmap
を使うと、よりシンプルに実装できる。
ちなみに、forEach
はループごとにawait
できない。
arguments
とスプレッド構文(...
)について
関数が期待するより多くの引数を渡すとarguments
に引数が格納される。
引数を可変で渡したい時に使える。
function avg() {
var sum = 0;
for (let value of arguments) {
sum += value;
}
return sum / arguments.length;
}
add(2, 3, 4, 5); // 14
上記と同じことをスプレッド構文(...
)を使うと可変の引数を配列として受け取れる。
function avg(...args) {
var sum = 0;
for (let value of args) {
sum += value;
}
return sum / args.length;
}
avg(2, 3, 4, 5); // 3.5
初めて知った。(笑)
実際には配列にしてから関数に引数として渡すことの方が多いと思うが、ベースとなるクラスなどを作るときには便利そう。
クロージャ
名前は聞いたことはあるが、あまり自分で使ったことはない。
知っていて損はないと思うのでメモ。
function makeAdder(a) {
return function(b) {
return a + b;
};
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // 11
add20(7); // 27
最後に
カスタムオブジェクトに関してはclass構文で書きたい気持ちがあるのでスキップ。
ドキュメントにざっと目を通したら、入門編の非同期 JavaScriptが面白そうな内容だったので、次回はこれをやる。
Promise
とかawait/async
とかめっちゃ使うけど、正直よくわかっていない(笑)
というか、Reactを学ぶはずが初回から脱線してしまった。
Day 2
2021/09/24
やったこと
Javascriptのドキュメントを読んで知識を深めた。(非同期 JavaScript編)
目的
普段JavaScriptを仕事で書いていて、async/await
やPromise.all()
をよく使うが、正直あまり理解しないまま感覚で使っているので、非同期処理についてを理解するため。
学んだこと
JavaScriptの非同期とは
JavaScriptはシングルスレッドである。
Asyncされた処理はevent queueに登録されてメインの処理が終わった後に処理される。
このことによって、asyncの処理はメインの処理をブロックしない。(参考)
下記のコードの場合、非同期の処理であるfetchは最後に実施され、consoleには以下のStarting
-> All done!
-> It worked :)
の順番で出力される。
console.log ('Starting');
let image;
fetch('coffee.jpg').then((response) => {
console.log('It worked :)')
return response.blob();
}).then((myBlob) => {
let objectURL = URL.createObjectURL(myBlob);
image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}).catch((error) => {
console.log('There has been a problem with your fetch operation: ' + error.message);
});
console.log ('All done!');
以下の3つの関数でセットされた非同期の処理はメインスレッド上で動く。
setTimeout()
setInterval()
requestAnimationFrame()
setTimeout()
の前やsetInterval()
の反復中に他のコードが実行されている場合は、PCの負荷に応じて、非同期の処理の実行が遅延する可能性がある。
なぜなら、非同期の処理が実行されるのはメインの処理が終わった後だから。
setTimeout()
は処理が実行される時間を保証していない。
setTimeout(0, fn)
としても必ずすぐに実行されるわけではなく、可能な限り早いメインスレッドが空いた時間に実行される。
setInterval()
はsetTimeout()
を再帰的に呼ぶことで同様の繰り返し処理が実現できる。
// setIntervalバージョン
let i = 1;
setInterval(function run() {
console.log(i);
i++;
}, 100);
// setTimeoutバージョン
let i = 1;
setTimeout(function run() {
console.log(i);
i++;
setTimeout(run, 100);
}, 100);
両者には、反復される処理間の時間が保証されるかどうかという違いがある。
setTimeout()
を使う場合は、処理間の時間が指定した時間異常であることが保証される。
処理に時間がかかる可能性がある処理(fetch
など)を繰り返す場合は、setTimeout()
の再帰処理の方が適している。
Promiseについて
Promise
は処理の途中の状態を表すオブジェクトで、その時点では、将来いつどのような結果が返却されるかがわからない。
また、その処理が成功するのか、失敗するのかも保証していない。
一方で、成功して結果が返却された、もしくは失敗した場合に、必ず指定されたコードを実行することを保証している。
複数のPromiseを同時に実行したい場合はPromise.all()
を使う。
Promiseするのは最終的に欲しい値が完成するまでの全体であるべき。
例えば、fetchした値をデコードして使用したい場合は以下の通り。
function fetchAndDecode(url, type) {
return fetch(url).then(response => {
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
if(type === 'blob') {
return response.blob();
} else if(type === 'text') {
return response.text();
}
}
})
.catch(e => {
console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
});
}
let coffee = fetchAndDecode('coffee.jpg', 'blob');
let tea = fetchAndDecode('tea.jpg', 'blob');
let description = fetchAndDecode('description.txt', 'text');
Promise.all([coffee, tea, description]).then(values => {
// データの使用
});
以下のコードだとfetchのみをPromiseしていて、デコードがPromiseの外になっている。
let a = fetch(url1);
let b = fetch(url2);
let c = fetch(url3);
Promise.all([a, b, c]).then(values => {
// デコードとデータの使用
});
今までのサンプルコードではpromise
を返却するfetch関数を使っていたが、独自のPromiseを返却する関数を作るにはnew Promise
を使用する。
function timeoutPromise(message, interval) {
return new Promise((resolve, reject) => {
if (message === '' || typeof message !== 'string') {
reject('Message is empty or not a string');
} else if (interval < 0 || typeof interval !== 'number') {
reject('Interval is negative or not a number');
} else {
setTimeout(() => {
resolve(message);
}, interval);
}
});
}
timeoutPromise('Hello there!', 1000)
.then(message => {
alert(message);
})
.catch(e => {
console.log('Error: ' + e);
});
async/await
async
を関数宣言の最初につけると非同期関数(async function)になる。
async function hello() { return "Hello" };
hello();
非同期関数はPromise
を返却するのでthen()
などが使える。
hello().then((value) => console.log(value))
async
はawait
と一緒に使うことで利点が活かせる。
async function hello() {
return greeting = await Promise.resolve("Hello");
};
hello().then(alert);
async/await
を使うと実行速度が遅くなる問題について。
await
を使うと、その非同期関数のPromise
が解決するまでは他の処理をブロックしてしまう。
例えば、以下のような3000ミリセカンド待つ関数を3回呼ぶ処理があるとする。
これを実行するとalertにはTime taken in milliseconds: 9000
と出てきて、一つ一つが終わるのを待って順番に同期的に実行されているのがわかる。
function timeoutPromise(interval) {
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve("done");
}, interval);
});
};
async function timeTest() {
await timeoutPromise(3000);
await timeoutPromise(3000);
await timeoutPromise(3000);
}
let startTime = Date.now();
timeTest().then(() => {
let finishTime = Date.now();
let timeTaken = finishTime - startTime;
alert("Time taken in milliseconds: " + timeTaken);
})
上記のコードのTimeTest()
を下記のように書き換えてみる。
すると、結果はTime taken in milliseconds: 3000
となり、並行処理
されていることがわかる。
これは、awaitを使わずに、Promise
オブジェクトを変数に代入することで、3つの処理はすぐに実行されるようになるから。
async function timeTest() {
const timeoutPromise1 = timeoutPromise(3000);
const timeoutPromise2 = timeoutPromise(3000);
const timeoutPromise3 = timeoutPromise(3000);
await timeoutPromise1;
await timeoutPromise2;
await timeoutPromise3;
}
非同期処理のエラー処理
上記のパターンで実装を行うとエラーハンドリングで少し問題が発生する。
function timeoutPromiseResolve(interval) {
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve("successful");
}, interval);
});
};
function timeoutPromiseReject(interval) {
return new Promise((resolve, reject) => {
setTimeout(function(){
reject("error");
}, interval);
});
};
async function timeTest() {
const timeoutPromiseResolve1 = timeoutPromiseResolve(5000);
const timeoutPromiseReject2 = timeoutPromiseReject(2000);
const timeoutPromiseResolve3 = timeoutPromiseResolve(3000);
await timeoutPromiseResolve1;
await timeoutPromiseReject2;
await timeoutPromiseResolve3;
}
let startTime = Date.now();
timeTest().then(() => {
}).catch(e => {
console.log(e);
let finishTime = Date.now();
let timeTaken = finishTime - startTime;
alert("Time taken in milliseconds: " + timeTaken);
})
上記のようなエラーハンドリングを行うと、エラーのalertが出るまでに5秒かかる。
理由は、全てがresolve/reject
されてないとthen/catch
以下のブロックに進まないため、一番時間がかかった処理に引っ張られてしまう。
この問題を解決するためにはPromise.all()
を使う。
async function timeTest() {
const timeoutPromiseResolve1 = timeoutPromiseResolve(5000);
const timeoutPromiseReject2 = timeoutPromiseReject(2000);
const timeoutPromiseResolve3 = timeoutPromiseResolve(3000);
const results = await Promise.all([timeoutPromiseResolve1, timeoutPromiseReject2, timeoutPromiseResolve3]);
return results;
}
Promise.all()
は与えられたPromise
のどれかが、reject
されるとreject
を返すので、非同期処理でエラーが発生するとすぐにcatch
に処理が移行する。
言葉の整理
- 並行処理:複数の処理を1つの主体が切り替えながらこなすこと
- 並列処理:複数の処理を複数の主体で同時にこなすこと
- 同期処理:複数の処理をこなす際、ある処理が別の処理の終了を待つような処理
- 非同期処理:複数の処理をこなす際、ある処理は別の処理の終了を待たないような処理
JavaScriptはシングルスレッドなので並列処理はできない。
参考: 参考1 参考2
まとめ
- JavaScriptはシングルスレッドである
- なので、非同期の処理は
並列処理
ではなく、並行処理
- なので、非同期の処理は
- 非同期の処理はevent queueに登録されてメインの処理が終わった後に処理される。
-
await
の付け方や、Promise.all()
の使い方によって、実行時間や処理順、エラーハンドリングが変わってくる。
最後に
普段から仕事でJavaScript(vue)を書いているが、シングルスレッドということを意識したことはなかった。
というか、非同期処理(特にPromise.all()
)は並列処理
がされているものだと思っていた。
JavaScriptはシングルスレッドで並行処理
のため、await
やPromise.all()
も書き方によって実行速度うあエラーハンドリングが変わることを初めて知り、これはすぐにでも仕事に生かせそうなので、学んだ甲斐があった。
次回も、JavaScriptの公式ドキュメントから気になったセクションがあったら読んでまとめたい。
Day 3
2021/09/25
やったこと
Javascriptのドキュメントを読んで知識を深めた。(クライアント側ストレージ編)
目的
オフラインで動作するWEBアプリケーションについて興味があり、その実装の際にクライアント側ストレージへの理解が必要そうなので、使えるようになる。
学んだこと
まず、クライアント側のストレージには4種類ある
- Cookie
- WebStorage API(LocalStorage, SessionStorage)
- IndexedDB API
- Cache API
Cookie
概要
HTTP Cookieは、サーバーがユーザーのウェブブラウザーに送信する小さなデータであり、ブラウザーに保存され、その後のリクエストと共に同じサーバーへ返送される。
Cookie は、クライアントサイドの汎用的なストレージとして使用されていたこともあるが、現在ではWeb Storage API
など、より汎用的なストレージとして適したAPIがあるため、そちらの使用が推奨されている。
一方で、セッション管理(ログインやECのカート)や、トラッキング、パーソナライゼーションなどの用途では、現在でも一般的に使用されている。
ポイント
- Cookie はすべてのリクエストで送信されるため、性能を悪化させる可能性がある
- 保存できるデータの容量は
4kb
までと非常に小さい - サーバーサイドでもクライアントサイドでもデータをセットできる
- 汎用的なストレージとしてが現在はしよ推奨されていない
- セッション管理やトラッキングなどの用途では現在も一般的に使用されている
- データの有効期限を任意に設定できる
基本的な使い方
書き込み(クライアントサイド)
document.cookie = "name=kamah;";
書き込み(サーバーサイド(node.js))
const server = http.createServer((req, res) => {
res.setHeader('Set-Cookie', ['name=kamah', 'age=26']);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
読み込み
let cookies = document.cookie;
削除
document.cookie = "name=kamah; expires=0";
その他オプション(一部)
// 持続期間
"Expires=Thu, 21 Mar 2019 12:00:00 GMT"
// HTTPSに限定
"Secure;"
// クライアントサイドからアクセスを制限(XSSの緩和に有効)
"HttpOnly;"
// Cookie を受信することができるホスト(Domain)を制限
"Domain=mozilla.org;"
// サーバーがオリジン間リクエストにCookieを送ることを制限
SameSite=Strict;"
Web Storage API
概要
Web Storage API は、ブラウザーがキー(常に文字列)と値のペアを安全に保存できる仕組みを提供する。
Cookieの後続技術とされている。
ポイント
- 何度ページを読み込んでも存在し続ける。
- 各オリジン毎に分割された保存領域を管理する
-
Web Storage
にはsessionStorage
とlocalStorage
の2種類ある- sessionStorage
- ページセッションの間に使用可能で、ページの再読み込みなどを行ってもブラウザーが開いている間はデータが持続する
- localStorage
- ブラウザーを閉じたり再び開いたりしてもデータが持続する
基本的な使い方
書き込み
localStorage.setItem('user_name', document.getElementById('user_name').value);
sessionStorage.setItem('user_name', document.getElementById('user_name').value);
読み込み
localStorage.setItem('user_name', document.getElementById('user_name').value);
sessionStorage.setItem('user_name', document.getElementById('user_name').value);
読み込み
localStorage.setItem('user_name', document.getElementById('user_name').value);
sessionStorage.setItem('user_name', document.getElementById('user_name').value);
削除
// 指定したキーのみ
localStorage.removeItem('user_name');
sessionStorage.removeItem('user_name');
// 全て
localStorage.clear()
sessionStorage.clear()
StorageEvent を使用してストレージの変更に反応する
window.addEventListener('storage', function(e) {
document.querySelector('.my-key').textContent = e.key;
document.querySelector('.my-old').textContent = e.oldValue;
document.querySelector('.my-new').textContent = e.newValue;
document.querySelector('.my-url').textContent = e.url;
document.querySelector('.my-storage').textContent = JSON.stringify(e.storageArea);
});
IndexedDB API
Cache API
参考サイト