在 React 官網的 Choosing the State Structure 中,第四個挑戰題要我們完成一個可以多重選擇的 checkbox:
作法是 <input>
的 onChange
事件被觸發後,呼叫名為 onToggle
的 event handler 來比對函式參數 toggledId
是否存在於 selectedIds
這個 state 裡面。
想通之後,我馬上用 includes()
實作,畢竟先前已經把 selectedIds
設為陣列了,而這的確也是官方提供的解答之一。
沒錯,除了這個比較直觀的作法之外,React 官方提供了第二個解答,至於差別在哪裡,請先看看這段描述:
One minor downside of using an array is that for each item, you’re callingselectedIds.includes(letter.id)
to check whether it’s selected. If the array is very large, this can become a performance problem because array search withincludes()
takes linear time, and you’re doing this search for each individual item. To fix this, you can hold a Set in state instead, which provides a fasthas()
operation
由於 includes
會遍歷整個陣列,所以當陣列資料越多,所耗費的時間就越可觀,這樣查找資料等於 O(n)。如果改用 Set 效能更佳!
這是筆者第一次看到 Set,於是花了點時間查詢相關資訊,發現似乎是滿實用的東西,因此特別記錄下來~
Set 是 JavaScript 於 ES2015 所推出的新物件類型,裡面只能存放單一獨特值,也就是無法存在重複的值。如同上面遇到的範例,Set 時常拿出來和陣列一起討論,但要注意兩者差異如下:
目前僅能透過 constructor 來建立新的 Set:
const hashTags = new Set();
在建立新 Set 的時候,可以帶入 iterable,舉凡陣列、物件、字串 (字元的集合):
const hashTags = new Set(["#做自己", "#無濾鏡"])
// result: Set(2) {'#selfie', '#nofilter'}
如果帶入數字的話就沒辦法說,因為數字並非 iterable。替代方案是將數字包進物件或陣列,或是建立空白的 Set 在透過 add()
新增:
new Set(1234)
// Uncaught TypeError: number 1234 is not iterable
如上,透過 add()
來新增資料到 Set 裡面:
const annoyingHashTags = new Set();
annoyingHashTags.add("#做自己");
annoyingHashTags.add("#無濾淨")
前面有提過,Set 的資料都是單一獨特的,所以如果我們嘗試新增重複資料,Set 會自動忽略,就像我學生時期想要追求的對象:
annoyingHashTags.add("#無濾淨") // 新增重複資料
// result: Set(2) {'#做自己', '#無濾淨'}
另外請注意,add()
一次只能新增一個項目。
和陣列大同小異,但不是用 length
而是 size
:
const annoyingHashTags = new Set();
annoyingHashTags.add("#做自己");
annoyingHashTags.add("#無濾淨")
annoyingHashTags.size //2
終於來到本次的重點了,我們可以透過 has()
來查詢某資料是否存在於 Set 當中,回傳值不意外,當然是 true
或 false
。
const annoyingHashTags = new Set();
annoyingHashTags.add("#做自己");
annoyingHashTags.add("#無濾淨")
annoyingHashTags.has("#做自己"); //true
annoyingHashTags.has("#誰懂我"); //false
透過 delete()
來移除指定資料,如果想要一次清空所有資料,則可以使用 clear()
。
const annoyingHashTags = new Set();
annoyingHashTags.add("#做自己");
annoyingHashTags.add("#無濾淨")
annoyingHashTags.has("#做自己"); //true
annoyingHashTags.delete("#做自己");
annoyingHashTags.has("#做自己"); //false (has been deleted)
前面提過 Set 本質為物件,所以若想要迭代裡面的資料,可以呼喚老朋友 for...of
。
const annoyingHashTags = new Set();
annoyingHashTags.add("#做自己");
annoyingHashTags.add("#無濾淨")
for(let hashtag of annoyingHashTags) {
console.log("超惱人 hashtag:", hashtag);
}
// 超惱人 hashtag:做自己
// 超惱人 hashtag:無濾淨
回到最一開始的 React 問題,我們先將 state 換成 Set,然後把 includes()
改成 has()
。但由於 React 講求 immutibility,所以不能直接更新物件 (還記得 Set 是物件吧),因此務必記得新增一個 Set,更新完畢後再透過 set function 替換掉舊的 Set 物件:
...
const [selectedIds, setSelectedIds] = useState(
new Set() // 改用 Set
);
const selectedCount = selectedIds.size;
function handleToggle(toggledId) {
// 新增 Set,避免直接更動員來的物件
const nextIds = new Set(selectedIds);
// 資料量一多,has() 的效能將比 includes() 好
if (nextIds.has(toggledId)) {
nextIds.delete(toggledId);
} else {
nextIds.add(toggledId);
}
// 透過 set function 替換掉舊的 Set
setSelectedIds(nextIds);
}
...
完整的題目和說明請參考這邊:
https://react.dev/learn/choosing-the-state-structure