banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

青訓キャンプ |「月影に従って JavaScript を学ぶ」ノート

この授業は月影先生によって行われ、内容が豊富で、オブジェクト指向設計、コンポーネントのカプセル化、高階関数(スロットリング、デバウンス、バッチ処理、イテラブル)が含まれています。

本授業の重点内容#

良い JS を書くための原則#

各自の責任#

例を挙げます:JS のコードを書いて、ウェブページを制御し、ライトモード / ダークモードの 2 つのモードをサポートさせるには、どうしますか?

私の最初の反応:ダークモードのクラスを作成し、切り替えボタンのイベントで切り替えます。これが資料で説明されている第二版です。

  • 第一版 直接スタイルを切り替える、適切ではないが、動作する
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(e.target.innerHTML === '☀️') {
    body.style.backgroundColor = 'black';
    body.style.color = 'white';
    e.target.innerHTML = '🌙';
  } else {
    body.style.backgroundColor = 'white';
    body.style.color = 'black';
    e.target.innerHTML = '☀️';
  }
});
  • 第二版 ダークモードのクラスをカプセル化
const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(body.className !== 'night') {
    body.className = 'nignt';
  } else {
    body.className = '';
  }
});
  • 第三版 これは完全な表示行動であるため、HTML と CSS だけで実現できます

    切り替えをcheckboxコントロールとして、id をmodeCheckBoxlabelタグのfor属性を使用してこのコントロールに関連付け、checkbox を隠すことでモードの切り替えを実現できます。image-20220117144502775.png

    "コードを書かなければバグは発生しない" これは各自の責任の一例でもあります。

まとめ:JS によるスタイルの不必要な直接操作を避ける必要があり、状態を表すためにクラスを使用し、純粋な表示クラスのインタラクションはゼロ JS のソリューションを求めるべきです。バージョン 2 にも利点があり、適応性は必ずしもバージョン 3 より良いとは限りません。

コンポーネントのカプセル化#

コンポーネントとは、ウェブページ上で抽出されたテンプレート(HTML)、機能(JS)、スタイル(CSS)を含むユニットのことを指します。良いコンポーネントはカプセル化、正確性、拡張性、再利用性を備えています。現在、多くの優れたコンポーネントが存在するため、通常は自分でコンポーネントを設計する必要はありませんが、彼らの実装を理解しようとすることも重要です。

例を挙げます:ネイティブ JS を使用して e コマースサイトのスライドショーを作成するには、どう実現しますか?

  • 構造:HTML の無秩序リスト(<ul>

    • スライドショーは典型的なリスト構造であり、無秩序リスト<ul>要素を使用して実現できます。各画像は li タグ内に配置されます。
  • 表現:CSS の絶対位置

    • CSS の絶対位置を使用して、画像を重ねて配置します。
    • 状態を切り替えるために修飾子(modifier)を使用します。
      • selected
    • スライドショーの切り替えアニメーションは CSS のtransitionを使用して実現します。
  • 行動:JS

    • API 設計は原子操作を保証し、責任を単一にし、柔軟性を満たす必要があります。
      • ps:原子操作とは、オペレーティングシステムの原語 wait、read などのように中断できない一連の操作を指します。
    • 一部のイベントをカプセル化します:getSelectedItem ()、getSelectedItemIndex ()、slidTo ()、slideNext ()、slidePrevious ()……
    • さらに進めて:制御フローを使用し、カスタムイベントを使用してデカップリングします。

まとめ:コンポーネントのカプセル化は、その構造設計、表示効果、行動設計(API、Event など)が基準を満たしているかに注意する必要があります。

考察:このスライドショーをどのように改善しますか?

リファクタリング 1:プラグイン化、デカップリング#

  • 制御要素を個々のプラグイン(左右の小さな矢印、下の 4 つの小さな円点など)として抽出します。image.png

  • プラグインとコンポーネントの間は依存性注入方式で接続します。

    image.png

この利点は?コンポーネントのコンストラクタが行う作業は、コンポーネントを一つずつ登録するだけで、再利用時には必要のないコンポーネントを直接コンストラクタからコメントアウトするだけで、他のことに気を配る必要がありません。

さらに拡張しますか?

リファクタリング 2:テンプレート化#

HTML もテンプレート化し、<div class='slider'></div>だけで画像スライドショーを実現し、コントローラのコンストラクタを修正して画像リストを渡します。

リファクタリング 3:抽象化#

汎用のコンポーネントモデルを抽象化し、コンポーネントクラス(Component)を作成し、他のコンポーネントクラスはこのクラスを継承し、その render メソッドを実装します。

image.png

class Component{
    constructor(id, opts = {name, data: []}) {
        this.container = document.getElementById(id);
        this.options = opts;
        this.container.innerHTML = this.render(opts.data);
    }
    registerPlugins(...plugins) { 
        plugins.forEach( plugin => {
            const pluginContainer = document.createElement( 'div');
            pluginContainer.className = `${name}__plugin`;
            pluginContainer.innerHTML = plugin.render(this.options.data);
            this.container.appendchild(pluginContainer);

            plugin.action(this);
        });
    }
    render(data) {
        /* abstract */
        return ''
    }
}

まとめ:

  • コンポーネント設計の原則 —— カプセル化、正確性、拡張性、再利用性
  • 実装手順:構造設計、表示効果、行動設計
  • 三回のリファクタリング
    • プラグイン化
    • テンプレート化
    • 抽象化
  • 改善:CSS のテンプレート化、親子コンポーネントの状態同期とメッセージ通信など

プロセスの抽象化#

  • 部分的な詳細制御のためのいくつかの方法

  • 関数型プログラミング思想の基礎応用

    image.png

応用:操作回数制限#

  • 一部の非同期インタラクション
  • 一度きりの HTTP リクエスト

次のコードがあります。毎回クリックするたびに 2 秒後にそのノードを削除しますが、ユーザーがそのノードが完全に削除される前に何度もクリックするとエラーが発生します。

const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button)=>{
    button.addEventListener('click', (evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed';
        setTimeout(()=>{
            list.removeChild(target.parentNode);
        },2000);
    })
});

この操作回数の制限は、高階関数として抽象化できます。

function once(fn) {
    return function(...args) {
        if(fn) {
            const ret = fn.apply(this, args);
            fn = null;
            return ret;
        }
    }
}
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button)=>{
    button.addEventListener('click', once((evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed';
        setTimeout(()=>{
            list.removeChild(target.parentNode);
        },2000);
    }))
});

コードに示されているように、この関数 once は関数を受け取り、返されるのも関数で、受け取った関数が null でないかを判断し、null でない場合はその関数を実行して結果を返し、受け取った関数が null の場合は何も操作を行わない関数を返します。click イベントに登録されているのは実際には once が返した関数であり、これにより何度クリックしてもエラーが発生しません。

ps:素晴らしい応用例ですね!

「一度だけ実行する」という要求を異なるイベント処理に適用するために、この要求を分離するプロセスをプロセスの抽象化と呼びます。

高階関数#

  • 関数を引数として受け取る
  • 関数を返り値として返す
  • 主に関数デコレーターとして使用される
funtion HOF(fn) {
    return function(...args) {
        return fn.apply(this, args);
    }
}

よく使われる高階関数#

Once 一度だけ実行#

前述の通り、ここでは詳述しません。

Throttle スロットリング#

関数に間隔 time を追加し、time ごとに関数を呼び出してその需要を節約します。例えば、あるイベントが非常に頻繁に発生する場合(マウスが上に移動するとトリガーされるなど)、そのイベント関数が非常に速く呼び出され続けるため、スロットリング関数を追加することでクラッシュを防ぎ、トラフィックを節約できます。

function throttle(fn, time = 500) {
    let timer;
    return function(...args) {
        if(!timer) {
            fn.apply(this, args);
            timer = setTimeout(() => {
                timer = null;
            }, time);
        }
    }
}
btn.onclick = throttle(function(e){
    /* イベント処理 */
    circle.innerHTML = parseInt(circle.innerHTML)+1;
    circle.className = 'fade';
    setTimeout(() => circle.className = '', 250);
});

元の関数をラップし、timer がなければ timer を登録し、500ms 後にキャンセルします。この 500ms の間、timer は存在し続けるため、関数は実行されません(または空の関数を実行します)。500ms 後に timer がキャンセルされると、関数が呼び出されて実行されます。

Debounce デバウンス#

上記のスロットリングでは、timer が存在する間は関数が実行されませんが、デバウンスでは毎回イベントが始まると timer をクリアし、timer を dur に設定します。イベントが dur 時間呼び出され、新しいイベントが再度呼び出されない場合(例えば、マウスが移動した後にしばらくホバーする場合)、関数が呼び出されて実行されます。

function debounce(fn, dur) {
    dur = dur || 100;   // durが存在しない場合はdurを100msに設定
    var timer;
    return function() {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, dur);
    }
}

Consumer#

これは、関数を setTimeout のような非同期操作に変える関数です。例えば、あるイベントが何度も呼び出された場合、これらのイベントをリストに追加し、設定した時間間隔で実行して結果を返します。まずはコードを見てみましょう:

function consumer(fn, time) {
    let tasks = [],
        timer;
    return function (...args) {
        tasks.push(fn.bind(this, ...args));
        if(timer == null) {
            timer = setInterval(() => {
                tasks.shift().call(this);
                if(tasks.length <= 0) {
                    clearInterval(timer);
                    timer = null;
                }
            }, time);
        }
    }
}
btn.onclick = consumer((evt) => {
    /*
     * イベント処理 例えば、何度も呼び出されたイベントを
     * リストに追加し、設定した時間間隔で実行して結果を返します。 
     */
    let t = parseInt(count.innerHTML.slice(1)) + 1;
    count.className = 'hit';
    let r = t * 7 % 256,
        g = t * 17  % 128,
        b = t * 31  % 128;
    count.style.color = `rgb(${r}, ${g}, ${b})`.trim();
    setTimeout(() => {
        count.className = 'hide';
    }, 500);
}, 800);

ここでのイベント処理は、ボタンをクリックすると + count を表示し、500ms 後にフェードアウトします。迅速にクリックすると、このクリックイベントがイベントリストに保存され、800ms ごとに実行されます(前の + count がまだ消えていない場合)。

関数の原理を理解するには、bind 関数、shift 関数、call について説明する必要があります。

bind() メソッドは新しい関数を作成し、bind()が呼び出されるとき、この新しい関数のthisbind()の最初の引数として指定され、残りの引数は新しい関数の引数として使用されます。

shift() メソッドは配列から最初の要素を削除し、その値を返します。このメソッドは配列の長さを変更します。これに対して、unshift()は最初の要素を挿入します。

同様のメソッドのペアには、pop()push()があり、これらは配列の最後の要素に作用します。

call() メソッドは指定されたthis値と個別に与えられた 1 つ以上の引数を使用して関数を呼び出します。

したがって、上記の関数の用途は、準備ができた関数を tasks リストに追加し、定期的に tasks を出隊し、すべての tasks がクリアされた場合(現在タスクがない場合)に定期的に実行される定期的なタスクを設定することです。定期的なタスクが空でない場合は、操作を行いません(ただし、tasks リストには追加されます)。

Iterative#

関数を反復可能にすることもでき、これは通常、関数が一連のオブジェクトに対してバッチ操作を実行する必要がある場合に使用されます。例えば、色を一括設定する場合、コードは次のようになります。

const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
function iterative(fn) {
    return function(subject, ...rest) {
        if(isIterable(subject)) {
            const ret = [];
            for(let obj of subject) {
                ret.push(fn.apply(this, [obj, ...rest]));
            }
            return ret;
        }
        return fn.apply(this, [subject, ...rest]);
    }
}
const setColor = iterative((el, color) => {
    el.style.color = color;
})
const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');

Toggle#

状態を切り替えることも高階関数としてカプセル化でき、どれだけの状態があっても、内部に追加するだけで済みます。

例:

function toggle(...actions) {
    return function (...args) {
        let action = actions.shift();
        action.push(action);
        return action.apply(this, args);
    }
}
// いくつでも状態を切り替えられます!
switcher.onclick = toggle(
    evt => evt.target.className = 'off',
    evt => evt.target.className = 'on'
);

考察#

なぜ高階関数を使用するのか?

概念を理解する:純粋関数は、関数の戻り値がその引数のみに依存し、実行プロセス中に副作用がない関数です。

これは、純粋関数が非常に信頼性が高く、外部に影響を与えないことを意味します。

  • ユニットテストが容易になります!
  • システム内の非純粋関数の数を減らすことで、システムの信頼性が向上します。

その他の考察#

  • 命令型と宣言型に優劣はない
  • プロセスの抽象化 / HOF / デコレーター
  • 命令型 / 宣言型
  • コードスタイル、効率、品質のバランス。
    • シーンに応じてバランスを取る

まとめと感想#

素晴らしい!!

この授業を終えて、多くのことを学びました。本当に意味のあるコンポーネントを実現するにはこれほど多くのステップが必要で、JavaScript でもこのようなオブジェクト指向設計が実現できることを知りました。以前学んだ C++/Java のデザインパターンと共通点があることに気づきました。コンポーネントはさらに多くのサブコンポーネントに細分化できます。後の高階関数は知識の盲点であり、JavaScript がこれらの方法を実現できることを知りました。

本文で引用した内容の大部分は月影先生の授業と MDN からのものです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。