這節課是由月影老師講的,幹貨滿滿,包括了面向對象的設計、組件封裝、高階函數(節流、防抖、批處理、可迭代化)
本堂課重點內容#
寫好 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 隱藏掉即可實現點擊切換模式。"
只要不寫代碼就不會有 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 設計應保證原子操作,職責單一,滿足靈活性
總結:組件封裝需要注意其結構設計、展現效果、行為設計(API、Event 等)是否達標
思考:如何來改進這個輪播圖?
重構 1:插件化,解耦#
-
將控制元素抽取成一個個插件(左右小箭頭、底下的四個小圓點)等等
-
插件與組件之間通過依賴注入方式建立聯繫、
這樣的好處?組件的構造器做的工作就只是將組件們一一註冊了,日後復用的時候不需要的組件直接將構造器註釋掉即,無需關注其他的。
再進一步擴展?
重構 2:模板化#
將 html 也模板化,做到只需一個 <div class='slider‘></div>
就能實現圖片輪播,修改控制器的構造,傳入圖片列表。
重構 3:抽象化#
將通用的組件模型,抽象出來一個組件類(Component),其他組件類通過繼承該類並實現其 render 方法。
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 模板化、父子組件的狀態同步和消息通信等等
過程抽象#
-
處理局部細節控制的一些方法
-
函數式編程思想的基礎應用
應用:操作次數限制#
- 一些異步交互
- 一次性的 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()
插入第一個元素。
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。