もりけん塾 JavaScript課題7 ローディングの実装

もりけん塾 JavaScript課題7 ローディングの実装

課題7は、Promiseを使ってローディングの実装をします。3秒後にローディング画像を取り除く、という課題です。

お題

loadingを実装してみてください。
resolveになるまでの間にloading画像をだして、終わったら除く 今持っている知識でできるはずです。
どうすればできそうか書く前に考えましょう。
これはサーバーから値が渡ってくるまではそれを出して、渡ってきたら値を加工してhtmlとして書き出すを想定しています。

準備

最初に考えたコードはこちらです。まず、ローディング画像を作って、Promiseで3秒後に解決したら、removeします。

//loading画像の作成
const loading = document.getElementById('js-loading');
const loadingImg = document.createElement('img');
loadingImg.src = "loading-circle.gif";
loadingImg.alt = "ローディング画像";
loading.appendChild(loadingImg);

//listsデータの作成・3秒後に解決して値を取得
const createList = new Promise(resolve => {
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});

//受け取った値をvalueに渡してlistを作成
createList.then((value) => {
    loading.remove();

    const fragment = document.createDocumentFragment();
    for(const list of value){
        const li = document.createElement('li');
        const a = document.createElement('a');
        const img = document.createElement('img');
        a.textContent = list.text;
        a.href = `/${list.to}`;
        img.src = list.img;
        img.alt = list.alt;
        fragment.appendChild(li).appendChild(a).appendChild(img);
    }
    const ul = document.getElementById('js-list');
    ul.appendChild(fragment);
});

コードをまとめる

コードが長く、複数の処理が一気に記述されているため、とても読みにくいです。そのため、レビューする側は辛く、自分はミスに気づきにくいです。コードレビューしていただくときにも、コードを読む負担が大きそうです。もっとわかりやすくまとめたいな、と思いました。

そこで、関数について調べました。関数については、処理をまとめてあるもの、と言うくらいの知識しかなかったので、まず関数とは何か、関数の使い方について調べます。

関数ってなに?

関数は、ある処理をまとめたもののことです。関数には、引数と返り値(戻り値)があります。引数は関数に渡す値のことです。返り値(戻り値)は、関数の処理をして返ってきた値です。言葉で説明されていても、わかりにくかったので、算数や、電動調理器などに例えると理解しやすかったです。

算数で言うと、 x+y = z という式の場合、x,yが引数、zが返り値です。足し算をするか割り算をするかなどの処理を関数の中に書きます。

関数の{ }の後ろには、 ; が必要ありません。書いてもエラーにはならないですが、意味がないのであえて書く必要がないようです。

let x = 1;
let y = 2;
function add(x, y) {
  const z = x + y;
  console.log(z);
  return z;
}

//関数の呼び出し
add(x, y);

出力結果

3

引数には、呼び出し時に任意の値を入れることができます。

function add(x, y) {
  const z = x + y;
  console.log(z);
  return z;
}

//関数の呼び出し
add(2, 3);

出力結果

5

関数について、なんとなく理解できましたが、まだ自信がないです。

他には、ジューサー(関数名)にりんご(引数:材料)を入れ、りんごジュース(返り値 or 戻り値:結果)が出てくる、というイメージが理解しやすかったです。お金(引数)を入れると自動販売機(関数)でジュース(返り値)が出てくると言う説明もわかりやすいな、と思いました。

returnとは

最初、返り値(戻り値)の理解が足りていなくて、最初に自分で考えたコードではエラーが出てしまいました。

returnを使うことで「ある関数で得られた値を別の関数で使う」といった応用が可能になります。逆に言えば、returnで返り値を設定しておかないと、ある関数の結果を別の関数で使えないと言うことです。

例えば、なんらかの果物(引数)を使ってジュースを作る(関数1)→ジュース(返り値)を使って、アイスを作る(関数2)場合、関数1の返り値であるジュースという結果がなかったら、アイスを作る処理は動きません。

とりあえず、別の関数で、ある関数の結果を使うときは、rerturnすると覚えました。

アロー関数

関数はアロー関数で書くこともできます。function同様、引数がひとつもないときでも、()を書く必要があります。

const add = (x, y) => {
  const z = x + y;
  console.log(z);
  return z;
}

//関数の呼び出し
add(2, 3);

結果

5

アロー関数は、記述を簡略化できて読みやすくていいなぁ!と思ったのですが、単純に簡略化できるだけの違いではないようです。通常の関数とは何が違うのか、調べました。

普通の関数とアロー関数では、thisの扱いが違うそうです。通常の関数はthisが変化していきますが、アロー関数の場合、それ自体はthisを持っていません。アロー関数では、宣言した時点でその外側のthisを参照します。thisを固定したいときや、固まった処理をするときにアロー関数を使うといいようです。

他にもコンストラクタを持たない、argumentsを持たないなどの特徴があります。コンストラクタとargumentsとは何のことなのかまだわかっていないので、今回はアロー関数を使うのはやめて、普通の関数にしました。

通常の関数と、アロー関数の違いはこちらの記事を参考にしました。

https://tcd-theme.com/2021/06/javascript-function2.html?gclid=Cj0KCQiAmeKQBhDvARIsAHJ7mF6jU-Ykl4Lg4TWfuFK_8C1sgE4HbB1KtPtBWve_aDlNKCiobUL_Z80aApK_EALw_wcB

コードを書く

関数とは何か、関数の書き方がわかったところで、今まで書いたコードを関数にまとめていきます。

前の課題で書いたPromiseの処理を、関数でまとめてみたら、.then((value) => { のところで、エラーが出てしまい、うまく動きませんでした。returnを使っておらず、値を返してなかったので、次の関数でエラーが出てしまいました。

繰り返しになりますが、返り値とは、関数を実行したときに、呼び出し元へ返される値のことです。returnを使うことで「ある関数で得られた値を別の関数で使う」といった応用が可能になります。

getList().then(value) =>{ の中のcreateList(value);の処理は、function getList();の値を使って実行する処理なので、function getList();の値が返されていないと動きません。よく考えると当たり前なんですが、最初全くわからなくて悩みました。returnについては、何度も何度も調べました。

返り値(戻り値)については、こちらの記事がわかりやすかったです。

https://job-support.ne.jp/blog/javascript/return-value

returnしていなくて、別の関数が動かなかった部分のコード↓

function getList(){
    new Promise(resolve => { 
    createLoading(); 
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});
}

getList().then((value) => {
    removeLoading();
    createList(value); 
});

returnを使って、その後の関数も動いたコード↓

function getList(){
    return new Promise(resolve => { 
    createLoading(); 
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});
}

getList().then((value) => {
    removeLoading();
    createList(value);
});

コードレビュー

関数化してみたところ、関数化する前のコードと比べると、かなり読みやすくなりました。

関数化前のコード↓

const loading = document.getElementById('js-loading');
const loadingImg = document.createElement('img');
loadingImg.src = "loading-circle.gif";
loadingImg.alt = "ローディング画像";
loading.appendChild(loadingImg);

const createList = new Promise(resolve => {
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});

createList.then((value) => {
    loading.remove();

    const fragment = document.createDocumentFragment();
    for(const list of value){
        const li = document.createElement('li');
        const a = document.createElement('a');
        const img = document.createElement('img');
        a.textContent = list.text;
        a.href = `/${list.to}`;
        img.src = list.img;
        img.alt = list.alt;
        fragment.appendChild(li).appendChild(a).appendChild(img);
    }
    const ul = document.getElementById('js-list');
    ul.appendChild(fragment);
});

関数化後のコード(最初のプルリクエスト)↓

const loading = document.getElementById('js-loading');

function createLoading(){
    const loadingImg = document.createElement('img');
    loadingImg.src = "loading-circle.gif";
    loadingImg.alt = "ローディング画像";
    loading.appendChild(loadingImg);
}

function removeLoading(){
    loading.remove();
}

function createList(value){
    const fragment = document.createDocumentFragment();
    for(const list of value){
        const li = document.createElement('li');
        const a = document.createElement('a');
        const img = document.createElement('img');
        a.textContent = list.text;
        a.href = `/${list.to}`;
        img.src = list.img;
        img.alt = list.alt;
        fragment.appendChild(li).appendChild(a).appendChild(img);
    }
    const ul = document.getElementById('js-list');
    ul.appendChild(fragment);
}

function getList(){
    return new Promise(resolve => {
    createLoading();
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});
}

getList().then((value) => {
    removeLoading();
    createList(value);
});

今回は、senさんちひろさんにコードレビューして頂きました!レビューのお時間をとって頂き、毎回感謝しかないです。

senさんから、グローバル変数で書いたloadingの変数宣言も関数内にまとめたらいいかも、とアドバイスをもらいました。

元のコード↓

const loading = document.getElementById('js-loading');

function createLoading(){
    const loadingImg = document.createElement('img');
    loadingImg.src = "loading-circle.gif";
    loadingImg.alt = "ローディング画像";
    loading.appendChild(loadingImg);
}

function removeLoading(){
    loading.remove();
}

関数内にまとめてローカル変数にしたコード↓

function createLoading(){
    const loading = document.getElementById('js-loading');
    const loadingImg = document.createElement('img');
    loadingImg.src = "loading-circle.gif";
    loadingImg.alt = "ローディング画像";
    loading.appendChild(loadingImg);
}

function removeLoading(){
    const loading = document.getElementById('js-loading');
    loading.remove();
}

まとめられるところは、なるべくまとめるとスッキリしてコードが読みやすい!と言うのを学ぶことができました。

ちひろさんから、下記の createLoading();はPromiseの外にあるべき、Promiseで解決するのは、const listsの部分のみではないか、というレビューを頂きました。よく考えるとそうでした。処理によっては全然違う結果になってしまいます。違和感を感じずに、Promiseの中に入れてしまったので、注意しようと思いました。この辺りは、Promiseの理解が足りていないせいだと思うので、よく復習しようと思います。

元のコード↓

function getList(){
    return new Promise(resolve => {
    createLoading();
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});
}

修正したコード↓

function getList(){
    createLoading();
    return new Promise(resolve => {
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});
}

最終的なコード

修正後の、最終的なコードはこちらです。

function createLoading(){
    const loading = document.getElementById('js-loading');
    const loadingImg = document.createElement('img');
    loadingImg.src = "loading-circle.gif";
    loadingImg.alt = "ローディング画像";
    loading.appendChild(loadingImg);
}

function removeLoading(){
    document.getElementById('js-loading').remove();
}

function createList(value){
    const fragment = document.createDocumentFragment();
    for(const list of value){
        const li = document.createElement('li');
        const a = document.createElement('a');
        const img = document.createElement('img');
        a.textContent = list.text;
        a.href = `/${list.to}`;
        img.src = list.img;
        img.alt = list.alt;
        fragment.appendChild(li).appendChild(a).appendChild(img);
    }
    const ul = document.getElementById('js-list');
    ul.appendChild(fragment);
}

function getList(){
    createLoading();
    return new Promise(resolve => {  
    const lists = [
        {to: "bookmark.html", img: "1.png", alt:"画像1", text: "ブックマーク"},
        {to: "message.html", img: "2.png", alt:"画像2", text: "メッセージ"}
    ];
    setTimeout(() => resolve(lists),3000);
});
}

getList().then((value) => {
    removeLoading();
    createList(value);
});

今回学んだこと

  • 関数の書き方
  • 関数・引数・返り値について
  • Promiseの考え方
  • コードのまとめ方

次の課題は、さらに難しそうです。Promiseについてもう一度確認しようと思います。

参考にさせて頂いた記事

関数についてはこちらの記事を参考にさせて頂きました。

https://jsprimer.net/basic/function-declaration/
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Functions

関数・引数・返り値については、こちらの記事が非常にわかりやすかったです!

https://blog.senseshare.jp/argument.html

私が所属しているフロントエンドエンジニアを目指す方のための塾 「もりけん塾」の森田賢二先生のTwitterはこちら!

先生のブログ「武骨日記」はこちら!

https://kenjimorita.jp/

まい

Webサービス制作会社で、Wordpressのテーマ開発や2Dシミュレーターの開発、JavaScriptを使用したフロントエンド周りの実装を担当しています。 JavaScriptが好きです。 最近は、3D Model Configuratorの制作にチャレンジしています。

もりけん塾カテゴリの最新記事