# React(MRWR)第 16 節:Getting Clever with Data Sorting > Udemy課程:[Modern React with Redux [2023 Update]](https://www.udemy.com/course/react-redux/) `20240203Sat.~20240209Fri.` :::danger 16-260 Sorting in JavaScript (數字 > return a-b 或 b-a) 16-261 Sorting Strings (字串 > 用到localeCompare()幫忙排序) 16-263 Object Sort Implementation (物件 > 先轉換、用typeof判斷,再排序) 16-268 React Fragments (可加上key prop,且直接回傳children,不會衍生其他DOM) ::: ## 16-259. Adding Sorting to the Table 這章節主要處理資料排序問題。 ![2024-02-03 10-01-45 的螢幕擷圖](https://hackmd.io/_uploads/H1ui0fi5p.png) 在做sorting時,要注意我們是會整個row一起移動! 分成上升排序或下降排序: * Ascending(上升): ![2024-02-03 10-03-56 的螢幕擷圖](https://hackmd.io/_uploads/r16d1Xjca.png) * Decending(下降): ![2024-02-03 10-06-36 的螢幕擷圖](https://hackmd.io/_uploads/ry2p1Qsqp.png) **** ## 16-260. Reminder on Sorting in JavaScript > 參考資料: > 1. [JS 將陣列 Array 重新排列的 sort()](https://ithelp.ithome.com.tw/articles/10225733) > 2. [ECMAS-Array.prototype.sort ( comparefn )](https://tc39.es/ecma262/#sec-array.prototype.sort) > 3. [Day 28 咩色用得好 - Array.prototype.sort (part - 1)](https://ithelp.ithome.com.tw/articles/10308185) > 4. [Day 29 咩色用得好 - Array.prototype.sort (part - 2)](https://ithelp.ithome.com.tw/articles/10308587) 在JS中做sorting,第一個想到的就是array method中的`sort()`。基於上學期剛修完演算法,好奇查了下`sort()`背後的演算法是什麼,找到了一篇在[stackoverflow](https://stackoverflow.com/questions/57763205/what-is-array-prototype-sort-time-complexity)的解釋。 ![2024-02-03 11-13-42 的螢幕擷圖](https://hackmd.io/_uploads/r1Ztk4jcp.png) 他提到在不同瀏覽器所實現的演算法不同,例如: * Firefox中是使用"merge sort" * Chrome中則是使用一種混合merge sort+insertion sort的演算法,叫做"[Timsort](https://v8.dev/blog/array-sort#timsort)"(此算法時間複雜度遠小於排序演算法最佳複雜度O(nlogn)) 回到`sort()`本身,其實`sort()`並不是單純去做sorting而已,他會先把我們放進去的array每一個元素轉成string形式,利用各個string的Unicode 編碼做排序的,所以就會碰到例如10排在3, 4, 5之前的怪異現象。 ![2024-02-03 11-58-16 的螢幕擷圖](https://hackmd.io/_uploads/Sk9y5Nj96.png) 所以說當我們想在JS中使用`sort()`的話,就必須去新增一個"comparator(比較器)"函式(一般都用箭頭函式,這邊的comparator只是給它的一個代稱)。 這裡用簡單的實際案例看看。 首先,假如我們直接對data陣列做`sort()`,會得到[10, 3, 4, 5]的排序,因為在JS的`sort()`,他會先將這四個數字轉成字串,再用Unicode 編碼做排序,但是這不符合我們的期望。 ```javascript! const data = [5, 10, 4, 3] data.sort(); //[10, 3, 4, 5] ``` 於是,我們加上一個匿名函數,也就是先前提到的comparator函式, ```javascript! const data = [5, 10, 4, 3] data.sort((a, b) => { return a - b; }); //[3, 4, 5, 10] ``` 可以用chrome的dev tool跑跑看: * 第一種方法: ![2024-02-03 12-18-29 的螢幕擷圖](https://hackmd.io/_uploads/SJnaR4sqT.png) * 第二種方法: ![2024-02-03 12-18-38 的螢幕擷圖](https://hackmd.io/_uploads/Sy3aAVo9p.png) 但...這是為什麼呢?為什麼加上所謂的comparator函式,就可以成功排序了呢?我們先把a, b給印出來,看看這兩個參數到底代表著什麼: ![2024-02-03 12-25-44 的螢幕擷圖](https://hackmd.io/_uploads/B1qPxHiqp.png) 先不急著研究,每次看到array method,就會想到阿傑的文章!發現在sort的部份,阿傑用了兩篇來解釋sort: 1. [Day 28 咩色用得好 - Array.prototype.sort (part - 1)](https://ithelp.ithome.com.tw/articles/10308185) 2. [Day 29 咩色用得好 - Array.prototype.sort (part - 2)](https://ithelp.ithome.com.tw/articles/10308587) 原來`sort()`可以帶入一個compareFn參數(這個參數代入與否為optional)。因此我們在直接使用`sort();`而沒有給予任何參數時,`sort()`會用預設的排序方法做排序,也就是根據Unicode 編碼做排序。 而compareFn參數,顧名思義就是一個函式(也就是先前提到的comparator function),這函式可以接收兩個參數a, b,分別代表要用來比較的元素,這個compareFn參數會根據回傳值來決定a, b的排列順序: (此表格式用來定義排序方式,例如說要遞增?遞減?或是若不傳入compareFn參數,則利用unicode編碼由小到大排序。至於排序的實作方式如同前面提到的,在不同瀏覽器下,會有不同的實作演算法,不太理解這裡的意思的話,可以看看阿傑[Day29](https://ithelp.ithome.com.tw/articles/10308587)最後的程式碼說明,會清楚很多!) | compareFn(a, b)回傳值 | a, b的排序 | | --------------------- | --------------------- | | > 0 | a排在b後面 e.g. [b,a] | | < 0 | a排在b前面 e.g. [a,b] | | === 0 | 照a, b原先的順序排序 | 這個時候再回來看下圖,就能更輕易理解為什麼要return a-b了,因為return a-b若回傳值大於0,那麼a就要排在後面,因此這裡就可以形成一個遞增的排序。 ![2024-02-03 12-25-44 的螢幕擷圖](https://hackmd.io/_uploads/B1qPxHiqp.png) 另外,可以從阿傑的[Day28](https://ithelp.ithome.com.tw/articles/10308185)文章最後看到,關於comparator function的注意事項,他必須符合以下幾點 (其中的reflexive反身性,symmetric對稱性, trensitive遞移性,剛好這陣子在念離散一直被這些概念瘋狂轟炸,念到懷疑人生,不過...看到原來這些知識也可以運用在這裡好感動) ![2024-02-03 12-53-34 的螢幕擷圖](https://hackmd.io/_uploads/ryqbwSo5T.png) **** ## 16-261. Sorting Strings > 參考資料:[String.prototype.localeCompare()](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) 前面說過,若不傳入參數到sort() method,則JavaScript會對陣列中的元素,先轉成String的形式,再依照Unicode編碼排序,故若我們現在要對字串做排序,使用sort()就會相當的方便好用。 但是,如果同時出現大小寫,可能就不會照我們預期的排序了,這裡示範簡單的例子: ```javascript! const data = ["t", "A", "a", "B", "b"] data.sort(); ``` 結果如下所示,他會先把大寫排出,再排小寫,但我們希望的是所有的"a"都排在最前面,接著是"b",無論大寫還是小寫: ![2024-02-05 11-03-39 的螢幕擷圖](https://hackmd.io/_uploads/rkxEgRpca.png) 這裡會使用到一個JS內建的函式`localeCompare()`,這個函式其實就是所謂的comparator function,他會回傳正數、負數或是0,回傳值如下: ![2024-02-05 11-19-19 的螢幕擷圖](https://hackmd.io/_uploads/rJyAQR65T.png) 可參考下方範例: ```javascript! // "a" 在 "c" 之前,所以會回傳負數 "a".localeCompare("c"); // -2、-1 或是其他負數值 // 按字母順序,「check」的順序在「against」之後,所以回傳正數 "check".localeCompare("against"); // 2、1 或其他正數值 // "a" 和 "a" 相同,所以回傳 0 "a".localeCompare("a"); // 0 ``` 因此當他得到回傳值之後,我們就能依照先前的表格對照,的確回傳負數代表著a排在b前面,所以基本上我們不需要再做其他的動作即可做好排序。 | compareFn(a, b)回傳值 | a, b的排序 | | --------------------- | --------------------- | | > 0 | a排在b後面 e.g. [b,a] | | < 0 | a排在b前面 e.g. [a,b] | | === 0 | 照a, b原先的順序排序 | 此時,我們把剛才的範例,加上`localeCompare()`,看看結果如何: ```javascript! const data = ["t", "A", "a", "B", "b"] data.sort((a, b) => { return a.localeCompare(b); }); ``` 結果如下所示: ![2024-02-05 11-26-39 的螢幕擷圖](https://hackmd.io/_uploads/HJvKrAaq6.png) **** ## 16-262. Sorting Objects ![2024-02-05 11-31-04 的螢幕擷圖](https://hackmd.io/_uploads/ry5YLCpqp.png) 在物件中做排序,有非常多方法,以上圖為例,我們可以依照weight排序、可以依照cost排序,也可以照name的第一個字母排序,當然我們甚至可以拿 cost/weight 算出每一單位重量要花多少錢來排序,又或者依照name的單字長度做排序(雖然沒啥意義)。 因此,這裡要做的就是從物件抽取我們要的資料即可,轉變成單純數字或單純字串,如此一來我們就可以拿著這些數字或字串,依照前面幾節學到的方法來排序。 **** ## 16-263. Object Sort Implementation ![2024-02-05 11-37-36 的螢幕擷圖](https://hackmd.io/_uploads/BkUr_Aa5a.png) 實作程式碼: ```javascript! const data = [ {weight: 15, cost: 5, name: "Onion"}, {weight: 10, cost: 2, name: "Tomato"}, {weight: 4, cost: 8, name: "Carrot"} ]; function getSortValue(vegitable){ return vegitable.cost; }; data.sort((a, b) => { const valueA = getSortValue(a); const valueB = getSortValue(b); return valueA - valueB; }) ``` 實作結果: ![2024-02-05 11-43-46 的螢幕擷圖](https://hackmd.io/_uploads/B1rKKAa9p.png) 所以若要針對物件中的某個值去做排序,最主要的就是getSortValue() 函式,因為他可以幫我們把物件中的屬性值轉換出來,讓我們可以去做比較。 現在的問題是,我們要排序的東西是"string"還是"number"? 因為從16-260與16-261大概可以看出我們針對"string"或是"number"的作法,是不同作法的。 因此,這裡將使用到`typeof`,來幫助我們判斷是"string"還是"number",之後再進一步做處理。 首先,先來看看[typeof](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Operators/typeof)如何使用以及他的回傳值,基本上這裡只會用到"string"或是"number",其他更詳細的內容,可以到MDN上查詢。 ![2024-02-05 11-54-21 的螢幕擷圖](https://hackmd.io/_uploads/r1GWnAaca.png) 實作程式碼: ```javascript! const data = [ {weight: 15, cost: 5, name: "Onion"}, {weight: 10, cost: 2, name: "Tomato"}, {weight: 4, cost: 8, name: "Carrot"} ]; function getSortValue(vegitable){ return vegitable.cost; }; data.sort((a, b) => { const valueA = getSortValue(a); const valueB = getSortValue(b); if(typeof valueA === "string"){ return valueA.localeCompare(valueB); }else{ return valueA - valueB; } }) ``` 這裡改用name做測試,測試結果如下: ![2024-02-05 11-56-01 的螢幕擷圖](https://hackmd.io/_uploads/HJI_nAp56.png) 所以說,如果想要改變sorting的目標,我們只需要更改getSortValue()即可。 例如說,我想要依照cost/weight算出每一單位重量要花多少錢來排序,那麼只要更改getSortValue()成如下所示,即可做排序: ```javascript! function getSortValue(vegitable){ return vegitable.cost / vegitable.weight; }; ``` **** ## 16-264. Reversing Sort Order 在[16-260](https://hackmd.io/vGXi5O3nQhaFk6S6XHUJvw?both#16-260-Reminder-on-Sorting-in-JavaScript)提過,遞增或是遞減排序,可以依照傳入的compareFn 參數來決定。 前幾節都是遞增排序,所以若我們想要遞減排序,其實只要在return值上乘上一個負號就可以了。 但問題是,若每次要統一成遞增或是遞減,都要對兩邊return值乘上1,或是乘上-1,動作非常瑣碎,且還找到return值的位置。 因此這裡我們一樣在外面建立一個變數`sortOrder`,讓它專門幫助我們處理要乘1(遞增)或是乘-1(遞減)。 實作程式碼: ```javascript! const data = [ {weight: 15, cost: 5, name: "Onion"}, {weight: 10, cost: 2, name: "Tomato"}, {weight: 4, cost: 8, name: "Carrot"} ]; function getSortValue(vegitable){ return vegitable.cost; }; const sortOrder = "ascending" data.sort((a, b) => { const valueA = getSortValue(a); const valueB = getSortValue(b); const reverseOrder = sortOrder === "ascending" ? 1 : -1; if(typeof valueA === "string"){ return valueA.localeCompare(valueB) * reverseOrder; }else{ return (valueA - valueB) * reverseOrder; } }) ``` 遞增結果: ![image](https://hackmd.io/_uploads/Hyw2U1Rc6.png) 遞減結果: ![image](https://hackmd.io/_uploads/BkrTI1C9T.png) **** ## 16-267. Customizing Header Cells 接下來的這幾節,基本上就是講如何將前面所學的排序應用到React中,重點在於要讓整個table reusable!!! 這裡會用到config變數幫助我們處理,主要在於"header",若config中出現"header",則會用"header"的函式去覆蓋原先的樣式或DOM元素等等。 ![2024-02-05 12-57-18 的螢幕擷圖](https://hackmd.io/_uploads/BJGgxlAca.png) ![2024-02-05 13-00-30 的螢幕擷圖](https://hackmd.io/_uploads/Skflxe0qT.png) **** ## 16-268. React Fragments * 問題: ![image](https://hackmd.io/_uploads/SyigreA56.png) * 問題解決: 利用React內建的方法解決,利用「Fragment元件」,幫我們達成想要加上key prop,卻又不希望產生多餘的DOM元素。 「Fragment元件」的原理老師上課有稍微提到,我們建立一個Echo元件,而此元件只會return children,至於children是什麼?children prop即是Echo元件裡面所包覆的東西,以此處為例便是`{column.header()}` ![image](https://hackmd.io/_uploads/SJiwdxA9a.png) 不過,React其實有內建一個叫做"Fragment"的元件,可以幫助我們做到像是上方Echo元件在做的事情,使用方法如下圖所示,有了"Fragment元件",即便需要添加key prop,也不必再額外渲染不必要的DOM元素了。 ![image](https://hackmd.io/_uploads/Byqh_gCcT.png) **** ## 16-269. The Big Reveal 這裡稍做複習: ![image](https://hackmd.io/_uploads/SJZWolRq6.png) ![image](https://hackmd.io/_uploads/BJni2xA96.png) 現在,我們要將先前提到的`getSortValue()`在需要時,可以加進config陣列中的任一物件裡使用。 不過,我們稍微改個名字,改成叫做`sortValue()`。 ![2024-02-05 14-18-26 的螢幕擷圖](https://hackmd.io/_uploads/rJ2f0eCqa.png) 接著,我們還要再做一個元件叫做"SortableTable",在"SortableTable"將會寫入大量的邏輯,專注在處理排序我們的資料,以及顯示我們的header(就是每一欄位的名稱,例如name、color、score)是否要顯示可以排序的按鈕,而當顯示可排序按鈕時,使用者可以點擊,並且在點擊之後進行資料的排序。 ![image](https://hackmd.io/_uploads/ByS4M-Acp.png) **** ## 16-270. Adding SortableTable (這裡只條列出一些碰到可能會出錯的問題) * 在React中,基本上不會去修改傳進來的props,所以像我們要針對config中擁有sortValue屬性的物件,對傳進來的config prop新增header屬性時,不是直接去新增,而是用mapping function,這樣才不會動到原先的config prop。 * Event Handlers with Arrow Functions ![image](https://hackmd.io/_uploads/H1JpQYliT.png) **** ## 16-272. Quick State Design * **Bad Idea:** ![2024-02-07 12-18-03 的螢幕擷圖](https://hackmd.io/_uploads/Bkh9EtliT.png) * **Good Idea:** ![image](https://hackmd.io/_uploads/ByDTuYxip.png) 流程圖: ![image](https://hackmd.io/_uploads/HyJRKYxsp.png) * **程式碼實作:** // SortableTable ```javascript! import { useState } from "react"; import Table from "./Table"; function SortableTable(props) { const [sortOrder, setSortOrder] = useState(null); const [sortBy, setSortBy] = useState(null); const { config } = props; const handleClick = (label) => { if (sortOrder === null) { setSortOrder("asc"); setSortBy(label); } else if (sortOrder === "asc") { setSortOrder("desc"); setSortBy(label); } else if (sortOrder) { setSortOrder(null); setSortBy(null); } } const updatedConfig = config.map((column) => { if (!column.sortValue) return column; return { ...column, header: () => <th onClick={() => handleClick(column.label)}> {column.label} IS SORTED </th> } }) return ( <div> {sortOrder} - {sortBy} <Table {...props} config={updatedConfig} /> </div> ) } export default SortableTable; ``` **** ## 16-275. Determining Icon Set ![2024-02-07 14-38-50 的螢幕擷圖](https://hackmd.io/_uploads/rydsSolsa.png)