<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
先不說別的,上兩個arco design table的bug。本來是寫react table元件,然後看原始碼學習思路,結果看的我真的很想吐槽。(其他元件我在學習原始碼上受益匪淺,尤其是工程化arco-cli那部分,我自己嘗試寫的輪子也是受到很多啟發,這個吐槽並不是真的有惡意,我對arco和騰訊的tdeisgn是有期待的,因為ant一家獨大太久了,很期待新鮮的血液)
如果arco deisgn的團隊看到這篇文章,請一定讓寫table的同學看一下!!!把多級表頭的篩選 + 排序 + 固定邏輯好好梳理一下,目前的寫法隱患太多了,我後面會寫為什麼目前的寫法隱患很多,非常容易出bug!
1、這是線上bug demo codesandbox.io/s/jovial-ka…
bug顯示
2、繼續看,我篩選userInfo上,工資大於2000的行,根本沒效果
線上bug 的demo codesandbox.io/s/competent…
說實話,我隨便送給大家幾個table的bug,都可以去給官方提pr了。(這個寫table的人一定要好好的批評一下!!!!)
filter是啥呢,我們看下圖
這個表頭的篩選我們簡稱為filter
首先官方把columns上所有的受控和非受控的filter收集起來,程式碼如下:
const { currentFilters, currentSorter } = getDefaultFiltersAndSorter(columns);
columns我們假設長成這樣:
const columns = [ { title: "Name", dataIndex: "name", width: 140, }, { title: "User Info", filters: [ { text: "> 20000", value: "20000", }, { text: "> 30000", value: "30000", }, ], onFilter: (value, row) => row.salary > value, }, { title: "Information", children: [ { title: "Email", dataIndex: "email", }, { title: "Phone", dataIndex: "phone", }, ], }, ]
getDefaultFiltersAndSorter的程式碼如下,不想看細節的,我就說下結論,這個函數是把filters受控屬性,filteredValue和非受控屬性defaultFilters放到currentFilters物件裡,然後匯出,其中key可以簡單認為是每個columns上的dataIndex,也就是每一列的唯一識別符號。
currentSorter我們暫時不看,也是為排序的bug埋下隱患,我們這篇文章先不談排序的bug。
function getDefaultFiltersAndSorter(columns) { const currentFilters = {} as Partial<Record<keyof T, string[]>>; const currentSorter = {} as SorterResult; function travel(columns) { if (columns && columns.length > 0) { columns.forEach((column, index) => { const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex; if (!column[childrenColumnName]) { if (column.defaultFilters) { currentFilters[innerDataIndex] = column.defaultFilters; } if (column.filteredValue) { currentFilters[innerDataIndex] = column.filteredValue; } if (column.defaultSortOrder) { currentSorter.field = innerDataIndex; currentSorter.direction = column.defaultSortOrder; } if (column.sortOrder) { currentSorter.field = innerDataIndex; currentSorter.direction = column.sortOrder; } } else { travel(column[childrenColumnName]); } }); } } travel(columns); return { currentFilters, currentSorter }; }
這裡的已經為出bug埋下隱患了,大家看啊,它是遞迴收集所有columns上的filter相關的受控和非受控的屬性,而且受控的屬性會覆蓋非受控。
這裡沒有單獨區分受控的filter屬性和非受控的屬性就很奇怪。後面分析,因為arco deisgn有個專門處理受控和非受控的hooks,因為他現在不區分,還用錯這個hooks,造成我看起來它的程式碼奇怪的要命!!
接著看!
然後,他用上面的currentFilters去
const [filters, setFilters] = useState<FilterType<T>>(currentFilters);
接著看一下useColunms,這個跟filters後面息息相關,所以我們必須要看下useColumns的實現
const [groupColumns, flattenColumns] = useColumns<T>(props);
簡單描述一下useColumns的返回值 groupColumns, flattenColumns分別代表什麼:
flattenColumns是啥意思呢?就是columns葉子節點組成的陣列,葉子節點是指所有columns中沒有children屬性的節點。以下是具體程式碼,有興趣的可以看看,我們接著看,馬上很奇怪的程式碼就要來了!
function useColumns<T>(props: TableProps<T>): [InternalColumnProps[][], InternalColumnProps[]] { const { components, // 覆蓋原生表格標籤 rowSelection, // 設定表格行是否可選,選中事件等 expandedRowRender, // 點選展開額外的行,渲染函數。返回值為 null 時,不會渲染展開按鈕 expandProps = {}, // 展開引數 columns = [], // 外界傳入的columns childrenColumnName, // 預設是children } = props; ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59dbcdab3b154494b751f61eeebe2432~tplv-k3u1fbpfcp-watermark.image?) // 下面有getFlattenColumns方法 // getFlattenColumns平鋪columns,因為可能有多級表頭,所以需要平鋪 // getFlattenColumns,注意這個平鋪只會蒐集葉子節點!!!! const rows: InternalColumnProps[] = useMemo( () => getFlattenColumns(columns, childrenColumnName), [columns, childrenColumnName] ); // 是否是checkbox const isCheckbox = (rowSelection && rowSelection.type === 'checkbox') || (rowSelection && !('type' in rowSelection)); // 是否是radio const isRadio = rowSelection && rowSelection.type === 'radio'; // 展開按鈕列的寬度 const { width: expandColWidth } = expandProps; // 是否有expand—row const shouldRenderExpandCol = !!expandedRowRender; const shouldRenderSelectionCol = isCheckbox || isRadio; // 獲取到自定義的操作欄,預設是selectNode和expandNode const { getHeaderComponentOperations, getBodyComponentOperations } = useComponent(components); const headerOperations = useMemo( () => getHeaderComponentOperations({ selectionNode: shouldRenderSelectionCol ? 'holder_node' : '', expandNode: shouldRenderExpandCol ? 'holder_node' : '', }), [shouldRenderSelectionCol, shouldRenderExpandCol, getHeaderComponentOperations] ); const bodyOperations = useMemo( () => getBodyComponentOperations({ selectionNode: shouldRenderSelectionCol ? 'holder_node' : '', expandNode: shouldRenderExpandCol ? 'holder_node' : '', }), [shouldRenderSelectionCol, shouldRenderExpandCol, getBodyComponentOperations] ); // rowSelection.fixed 表示checkbox是否固定在左邊 const selectionFixedLeft = rowSelection && rowSelection.fixed; // 選擇列的寬度 const selectionColumnWidth = rowSelection && rowSelection.columnWidth; const getInternalColumns = useCallback( (rows, operations, index?: number) => { const operationFixedProps: { fixed?: 'left' | 'right' } = {}; const _rows: InternalColumnProps[] = []; rows.forEach((r, i) => { const _r = { ...r }; if (!('key' in r)) { _r.key = _r.dataIndex || i; } if (i === 0) { _r.$$isFirstColumn = true; if (_r.fixed === 'left') { operationFixedProps.fixed = _r.fixed; } } else { _r.$$isFirstColumn = false; } _rows.push(_r); }); const expandColumn = shouldRenderExpandCol && { key: INTERNAL_EXPAND_KEY, title: INTERNAL_EXPAND_KEY, width: expandColWidth, $$isOperation: true, }; const selectionColumn = shouldRenderSelectionCol && { key: INTERNAL_SELECTION_KEY, title: INTERNAL_SELECTION_KEY, width: selectionColumnWidth, $$isOperation: true, }; if (selectionFixedLeft) { operationFixedProps.fixed = 'left'; } if (typeof index !== 'number' || index === 0) { [...operations].reverse().forEach((operation) => { if (operation.node) { if (operation.name === 'expandNode') { _rows.unshift({ ...expandColumn, ...operationFixedProps }); } else if (operation.name === 'selectionNode') { _rows.unshift({ ...selectionColumn, ...operationFixedProps }); } else { _rows.unshift({ ...operation, ...operationFixedProps, title: operation.name, key: operation.name, $$isOperation: true, width: operation.width || 40, }); } } }); } return _rows; }, [ expandColWidth, shouldRenderExpandCol, shouldRenderSelectionCol, selectionColumnWidth, selectionFixedLeft, ] ); const flattenColumns = useMemo( () => getInternalColumns(rows, bodyOperations), [rows, getInternalColumns, bodyOperations] ); // 把表頭分組的 columns 分成 n 行,並且加上 colSpan 和 rowSpan,沒有表頭分組的話是 1 行。 // 獲取column的深度 const rowCount = useMemo( () => getAllHeaderRowsCount(columns, childrenColumnName), [columns, childrenColumnName] ); // 分行之後的rows const groupColumns = useMemo(() => { if (rowCount === 1) { return [getInternalColumns(columns, headerOperations, 0)]; } const rows: InternalColumnProps[][] = []; const travel = (columns, current = 0) => { rows[current] = rows[current] || []; columns.forEach((col) => { const column: InternalColumnProps = { ...col }; if (column[childrenColumnName]) { // 求出葉子結點的個數就是colSpan column.colSpan = getFlattenColumns(col[childrenColumnName], childrenColumnName).length; column.rowSpan = 1; rows[current].push(column); travel(column[childrenColumnName], current + 1); } else { column.colSpan = 1; // 這是 column.rowSpan = rowCount - current; rows[current].push(column); } }); rows[current] = getInternalColumns(rows[current], headerOperations, current); }; travel(columns); return rows; }, [columns, childrenColumnName, rowCount, getInternalColumns, headerOperations]); return [groupColumns, flattenColumns]; } export default useColumns; function getFlattenColumns(columns: InternalColumnProps[], childrenColumnName: string) { const rows: InternalColumnProps[] = []; function travel(columns) { if (columns && columns.length > 0) { columns.forEach((column) => { if (!column[childrenColumnName]) { rows.push({ ...column, key: column.key || column.dataIndex }); } else { travel(column[childrenColumnName]); } }); } } travel(columns); return rows; }
接下來這個函數求的是受控的filters的集合!
為啥你受控的集合不在上面我們提到的getDefaultFiltersAndSorter裡面就求出來,非要自己單獨再求一遍?
const controlledFilter = useMemo(() => { // 允許 filteredValue 設定為 undefined 表示不篩選 const flattenFilteredValueColumns = flattenColumns.filter( (column) => 'filteredValue' in column ); const newFilters = {}; // 受控的篩選,當columns中的篩選發生改變時,更新state if (flattenFilteredValueColumns.length) { flattenFilteredValueColumns.forEach((column, index) => { const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex; if (innerDataIndex !== undefined) { newFilters[innerDataIndex] = column.filteredValue; } }); } return newFilters; }, [flattenColumns]);
結果我們一看,flattenColumns裡去拿受控的columns屬性的值,而flattenColumns是拿的葉子節點,這麼說來,這個controlledFilter還是跟之前的getDefaultFiltersAndSorter裡的currentFilters是有區別的,一個是葉子節點,一個是全部的columns。
但是!問題來了,你只求葉子節點的受控屬性,那非葉子節點的受控屬性萬一使用者給你賦值了,豈不是沒有作用了!!!
這就是我們最開始提到的第二個bug的根本原因,你自己最開始求得是所有columns中的filters的集合,現在用的是葉子節點的filters的屬性,這不是牛頭不對馬嘴嗎???
接著看,上面的離譜邏輯導致後面的程式碼想去打修補程式,結果就是打不全修補程式!
const innerFilters = useMemo<FilterType<T>>(() => { return Object.keys(controlledFilter).length ? controlledFilter : filters; }, [filters, controlledFilter]);
你看,他去得到一個innerFilters,咋求的呢?如果controlledFilter有值,也就是葉子節點有filter的受控屬性,那麼就用葉子節點的受控屬性作為我們要使用的filters,但是!!!!
如果沒有葉子節點的受控屬性的filters,他居然用的是filters,filters是咋求出來的,不就是最上面的getDefaultFiltersAndSorter嗎,這個函數求的是所有columns裡filters的集合。
這個函數就非常非常離譜,為啥邏輯不對啊,一個針對葉子節點,一個針對全部節點!!!
// stateCurrentFilter 標記了下拉框中選中的 filter 專案,在受控模式下它與 currentFilter 可以不同 const [currentFilter, setCurrentFilter, stateCurrentFilter] = useMergeValue<string[]>([], { value: currentFilters[innerDataIndex] || [], });
注意,這裡有個useMergeValue的hooks,這個hooks 在arco deisgn中起著舉足輕重的作用,我們必須好好說一下這個hooks,再看看寫這個元件的同學為什麼用錯了!
我們簡單解釋一下這個hooks的目的,我們在用元件的時候一般會有兩種模式,受控元件和非受控,這個hooks就是完美解決這個問題,你只要把value傳入受控元件的屬性,defaultValue傳入非受控屬性,這個hooks就自動接管了這兩種狀態的變化,非常棒的hooks,寫的人真的很不錯!
import React, { useState, useEffect, useRef } from 'react'; import { isUndefined } from '../is'; export default function useMergeValue<T>( defaultStateValue: T, props?: { defaultValue?: T; value?: T; } ): [T, React.Dispatch<React.SetStateAction<T>>, T] { const { defaultValue, value } = props || {}; const firstRenderRef = useRef(true); const [stateValue, setStateValue] = useState<T>( !isUndefined(value) ? value : !isUndefined(defaultValue) ? defaultValue : defaultStateValue ); useEffect(() => { // 第一次渲染時候,props.value 已經在useState裡賦值給stateValue了,不需要再次賦值。 if (firstRenderRef.current) { firstRenderRef.current = false; return; } // 外部value等於undefined,也就是一開始有值,後來變成了undefined( // 可能是移除了value屬性,或者直接傳入的undefined),那麼就更新下內部的值。 // 如果value有值,在下一步邏輯中直接返回了value,不需要同步到stateValue if (value === undefined) { setStateValue(value); } }, [value]); const mergedValue = isUndefined(value) ? stateValue : value; return [mergedValue, setStateValue, stateValue]; }
從這個hooks匯出的[mergedValue, setStateValue, stateValue]
,我們簡單分析下怎麼用,mergedValue是以受控為準的,也就是外部發現如果用了受控屬性,取這個值就行了,而且因為useEffect監聽了value的變化,你就不用管受控屬性的變化了,自動處理,多好啊!
然後setStateValue主要是手動去更新stateValue的,主要是在非受控的條件下去更新值,所以stateValue一般也是外部判斷,如果是非受控條件,就取這個值!
我們接著看arco deisgn中這個人咋用的,完全浪費了這麼一個好hook。
// stateCurrentFilter 標記了下拉框中選中的 filter 專案,在受控模式下它與 currentFilter 可以不同 const [currentFilter, setCurrentFilter, stateCurrentFilter] = useMergeValue<string[]>([], { value: currentFilters[innerDataIndex] || [], });
value傳了一個currentFilters[innerDataIndex] ,currentFilters是指所有columns裡有可能是filters受控的屬性集合,有可能是非受控filters屬性的集合,innerDataIndex值的當前列的dataindex,也就是唯一識別符號key。
那麼問題來了value明明人家建議你傳的是受控!受控!受控屬性的值啊,因為你currentFilters目前既可能是受控,也可能是非受控,所以你傳給value是沒有辦法的辦法,因為你傳給defaultValue也不對!
useEffect(() => { setCurrentFilter(currentFilters[innerDataIndex] || []); }, [currentFilters, innerDataIndex]); useEffect(() => { if (currentFilter && currentFilter !== stateCurrentFilter) { setCurrentFilter(currentFilter); } }, [filterVisible]);
第一個useEffect是賦值了一個不知道是受控還是非受控的filters,然後第二個假設currentFilter存在,就是說如果受控的filters存在就賦值給優先順序更高的受控屬性!
上面造成兩個useEffect的原因,不就是最開始在收集filters的時候,沒有區分受控和非受控filters,然後後面程式碼再求一遍嗎,而且求的邏輯讓人不好看懂,對不起,我想說這程式碼寫的,太容易出bug了,寫的這個人真的是一己之力把table元件毀了!!!
然後我們看filters在確定篩選時的函數!
/** ----------- Filters ----------- */ function onHandleFilter(column, filter: string[]) { const newFilters = { ...innerFilters, [column.dataIndex]: filter, }; const mergedFilters = { ...newFilters, ...controlledFilter, }; if (isArray(filter) && filter.length) { setFilters(mergedFilters); const newProcessedData = getProcessedData(innerSorter, newFilters); const currentData = getPageData(newProcessedData); onChange && onChange(getPaginationProps(newProcessedData), innerSorter, newFilters, { currentData, action: 'filter', }); } else if (isArray(filter) && !filter.length) { onHandleFilterReset(column); } }
搞笑操作再次上演,innerFilters本來就是個奇葩,然後用
[column.dataIndex]: filter
去覆蓋innerFilters裡的dataIndex裡的filter,這裡的filter本來就是非受控的屬性,你完全不區分受控非受控就上去一頓合併,萬一是受控的屬性呢??????
然後在mergedFilters里居然用controlledFilter再次去亡羊補牢,想用假如說有受控的filters,那麼就優先用受控的值去覆蓋innerFilters。
最開始不區分受控和非受控filters,後面全是一頓修補程式!你開始區分不就程式碼邏輯就很清晰了嗎,造成這麼多次的遍歷columns還有很多多餘的更新react元件,讓我忍不住想吐槽一下!!!
我簡單寫一下如何解決最開始寫的第二個bug。
首先,getDefaultFiltersAndSorter要區分受控和非受控的情況,這是給後面的useMergeProps傳遞給受控和非受控屬性做鋪墊,題外話!大家寫元件庫的話可以copy一份useMergeProps這個hook,真的好東西!改進如下:
// currentFilteredValues代表非受控的filters的全部收集器 // currentDefaultFilters代表受控的filters的全部收集器 const { currentFilteredValues, currentDefaultFilters, currentSorter } = getDefaultFiltersAndSorter(columns); function getDefaultFiltersAndSorter(columns) { const currentFilteredValues = {} as Partial<Record<keyof T, string[]>>; const currentDefaultFilters = {} as Partial<Record<keyof T, string[]>>; const currentSorter = {} as SorterResult; function travel(columns) { if (columns && columns.length > 0) { columns.forEach((column, index) => { const innerDataIndex = column.dataIndex === undefined ? index : column.dataIndex; if (!column[childrenColumnName]) { // 篩選的非受控寫法 if (column.defaultFilters) { currentDefaultFilters[innerDataIndex] = column.defaultFilters; } // 篩選的受控屬性,值為篩選的value陣列 string[] if (column.filteredValue) { currentFilteredValues[innerDataIndex] = column.filteredValue; } // 預設排序方式 'ascend' | 'descend' if (column.defaultSortOrder) { currentSorter.field = innerDataIndex; currentSorter.direction = column.defaultSortOrder; } // 排序的受控屬性,可以控制列的排序,可設定為 ascend descend if (column.sortOrder) { currentSorter.field = innerDataIndex; currentSorter.direction = column.sortOrder; } } else { // 篩選的非受控寫法 if (column.defaultFilters) { currentDefaultFilters[innerDataIndex] = column.defaultFilters; } // 篩選的受控屬性,值為篩選的value陣列 string[] if (column.filteredValue) { currentFilteredValues[innerDataIndex] = column.filteredValue; } // 預設排序方式 'ascend' | 'descend' if (column.defaultSortOrder) { currentSorter.field = innerDataIndex; currentSorter.direction = column.defaultSortOrder; } // 排序的受控屬性,可以控制列的排序,可設定為 ascend descend if (column.sortOrder) { currentSorter.field = innerDataIndex; currentSorter.direction = column.sortOrder; } travel(column[childrenColumnName]); } }); } } travel(columns); return { currentFilteredValues, currentDefaultFilters, currentSorter }; }
然後初始化filters的時候,就要簡單判斷一下,我這裡寫的很爛,因為抽出一個函數的,主要是自己當初為了跑通程式碼,隨便寫了下,意思大家懂就行
const [filters, setFilters] = useState<FilterType<T>>( Object.keys(currentDefaultFilters).length ? currentDefaultFilters : Object.keys(currentFilteredValues).length ? currentFilteredValues : undefined );
然後在 columns檔案裡,useMergeValue做受控屬性和非受控屬性的收口,因為之前我們區分了受控和非受控讓後面的程式碼邏輯清晰很多。
const innerDataIndex = dataIndex === undefined ? index : dataIndex; // stateCurrentFilter 標記了下拉框中選中的 filter 專案,在受控模式下它與 currentFilter 可以不同 // currentFilter是受控value, setCurrentFilter主要是給非受控value的, stateCurrentFilter 內部value const [currentFilter, setCurrentFilter] = useMergeValue<string[]>([], { value: currentFilteredValues[innerDataIndex], defaultValue: currentDefaultFilters[innerDataIndex], });
然後點選filters的時候如何排序呢,這裡filters就是受控和非受控的合併體,再用 [column.dataIndex]: filter更新當前最新的filter,後面更新資料就很自然了,getProcessedData是計算filters後的列,這個函數也需要改一下,把只計算葉子節點的改為計算所有的columns
function onHandleFilter(column, filter: string[]) { const mergedFilters = { ...filters, [column.dataIndex]: filter, // 篩選項 }; if (isArray(filter) && filter.length) { setFilters(mergedFilters); const newProcessedData = getProcessedData(innerSorter, mergedFilters); const currentData = getPageData(newProcessedData); onChange && onChange(getPaginationProps(newProcessedData), innerSorter, mergedFilters, { currentData: getOriginData(currentData), action: 'filter', }); } else if (isArray(filter) && !filter.length) { onHandleFilterReset(column); } }
以上就是前端框架arco table原始碼遇到的問題解析的詳細內容,更多關於前端框架arco table解析的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45