banner
cos

cos

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

青訓營 |「跟著月影學 JavaScript」筆記

這節課是由月影老師講的,幹貨滿滿,包括了面向對象的設計、組件封裝、高階函數(節流、防抖、批處理、可迭代化)

本堂課重點內容#

寫好 js 的原則#

各司其責#

舉個栗子:寫一段 JS,控制一個網頁,讓他支持淺色 / 深色兩種模式。你會怎麼做呢?

我的第一反應:寫一個深色類,在切換按鈕事件進行切換。這也是課件裡講的第二版。

  • 第一版 直接切換樣式,不妥,但能用
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 實現

    將切換作為一個 type 為 checkbox 控件,id 為 modeCheckBox,使用 label 標籤的 for 控件,id 為 modeCheckBox,使用 label 標籤的 for 屬性將其關聯到這個控件,再把 checkbox 隱藏掉即可實現點擊切換模式。image-20220117144502775.png

    "只要不寫代碼就不會有 bug" ,這也是各司其責的一種體現。

總結:需要避免不必要的由 js 直接操作樣式,可以用 class 來表示狀態,而純展示類的交互尋求零 JS 方案。版本 2 也是有其好處的,如適應性是不一定有版本 3 的好的。

組件封裝#

組件是指 web 頁面上抽出來的一個個包含模板 (HTML)、功能(JS)和樣式(CSS)的單元,好的組件具備封裝性、正確性、擴展性和復用性。雖然現在由於有很多優秀的組件存在,往往我們不需要去自己設計一個組件,但我們也要去試著了解他們的實現。

舉個栗子:用原生 JS 寫一個電商網站的輪播圖,應該怎麼實現?

  • 結構:HTML 中的無序列表( <ul>

    • 輪播圖是典型的列表結構,可以用無序列表 <ul> 元素來實現,每個圖放在一個 li 標籤中。
  • 表現:CSS 絕對定位

    • 使用 CSS 的絕對定位,將圖片重疊在一個位置
    • 切換狀態使用修飾符(modifier)
      • selected
    • 輪播圖切換動畫使用 CSS transition 實現
  • 行為:JS

    • API 設計應保證原子操作,職責單一,滿足靈活性
      • ps:原子操作就是指 不可中斷的一個或一系列操作,比如操作系統中的原語 wait、read 等。
    • 封裝一些事件:getSelectedItem ()、getSelectedItemIndex ()、slidTo ()、slideNext ()、slidePrevious ()……
    • 更進一步:控制流,使用自定義事件來進行解耦。

總結:組件封裝需要注意其結構設計、展現效果、行為設計(API、Event 等)是否達標

思考:如何來改進這個輪播圖?

重構 1:插件化,解耦#

  • 將控制元素抽取成一個個插件(左右小箭頭、底下的四個小圓點)等等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 請求

有這樣一段代碼,在每次點擊時延時 2s 後移除該節點,但如果用戶在該節點還沒完全移除的時候又點了幾次則會報錯。

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() 被調用時,這個新函數的 this 被指定為 bind() 的第一個參數,而其餘參數將作為新函數的參數,供調用時使用。

shift() 方法從數組中刪除第一個元素,並返回該元素的值。此方法更改數組的長度。與之相反的則是 unshift() 插入第一個元素。

與之相似的一對方法還有,pop()push() ,他們作用於數組最後一個元素

call() 方法使用一個指定的 this 值和單獨給出的一个或多個參數來調用一個函數。

那麼不難看出上面這個函數的用途,將每次準備調用的函數放入 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 / 裝飾器
  • 命令式 / 聲明式
  • 代碼風格、效率、質量的權衡。
    • 根據場景來權衡

總結感想#

太牛了!!

看完這節課,收穫非常多,實現一個真正意義上的組件原來需要這麼多步驟,原來 js 也能實現如此面向對象的設計,結合之前學過的 c++/java 的設計模式,發現都是有共通之處的,一個組件可以向下細分為許許多多的子組件。後面的高階函數更是知識盲區,原來 js 還能實現這些方法

本文引用的內容大部分來自月影老師的課以及 MDN。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。