高性能多級多選級聯組件開發

2020-06-13     segmentfault官方

原標題:高性能多級多選級聯組件開發

本文轉載於SegmentFault社區

作者:離塵不理人

最近在項目開發過程中,有個一個多級多選的公共組件開發需求,特在這裡記錄下開發過程中所做的一些優化以及分享一下我是如何從零開發並設計一個組件的思路,希望給閱讀這篇文章的讀者帶來一點收穫。

效果預覽

單個項選中

多個部分項選中

需求分析

在拿到需求之後,我們首先要做的是需求分析;通過上面的效果預覽我們可以初步知道我們所需要處理的核心邏輯:

  1. 默認加載第一層級數據

  2. 滑鼠 hover

    1. 異步獲取數據

    2. 切換下級渲染數據

  3. 滑鼠點擊

    1. 點擊當前項狀態改變:選中 or 未選中

    2. 當前項的父級狀態改變:選中、半選、不選中,並且需要遞歸處理

    3. 當前項的子級狀態改變:全選、全不選

組件設計

在設計組件之前,我們需要考慮組件的性能、通用型等問題;如何設計一個與業務解耦的組件,是我們需要首先考慮的問題;那麼,如何將組件數據請求與業務解耦呢:

  • 組件提供一個 service 入參,service 是一個返回 Promise 的異步請求方法

  • 組件提供一個 dataMapper,用來做數據轉換,將 service 請求返回的值轉化為符合我們組件數據解構的數據

  • 組件內部通過調用外部傳入的 service 來獲取數據

入參設計如下:

interfaceProps { ... // 外部傳入服務 service: ( args: { parentId: string} ) => Promise<{ list: SelectorItemType[] }>; dataMapper?: ( args: any) => { list: SelectorItemType[] }; /** * 回顯數據 * @default [] */ data?: SelectorItemType[]; onSubmit?: SubmitCallback; onCancel?: => void; }

try{ constdata = awaitservice({ parentId: itemId }); nextColumnList = dataMapper ? dataMapper(data).list : data.list;} catch(error) { Notification.error(error);nextColumnList = [];}

整體思路設計

通過上面的 UI 呈現,現在大家應該有個基礎的認識,我們需要做什麼樣的需求了。

我們在接到一個需求的時候,先不要著急著碼代碼,更好的方式是先規劃我們的組件方案設計,並且提前思考好各種邏輯分支;

這裡給大家看下我的設計初稿,我習慣性的選擇腦圖來發散自己的思維:

通過上圖,我們能夠在大腦中有個大概的清晰認識到我們需要做哪些核心模塊的設計與開發,接下來就是規劃我們的核心模塊劃分:

  • 數據緩存

  • 異步數據獲取

  • 選中數據緩存

  • 渲染數據源設計

數據緩存設計

要設計一個高性能多級多選組件,肯定離不開我們的數據優化部分:數據緩存

那麼如果如何設計才能做到性能最優呢?通過上面的腦圖,我們初步是通過一個 dataCaheMap 來緩存異步拉取回來的數據,這樣子我們在取的時候,時間複雜度就是 O(1) ;既然是有 Map 來緩存數據,那麼用什麼作為 key 也是我們緩存的關鍵;

在這個組件裡面,最終我選擇的是:列索引+行索引+id 作為緩存 key

這樣設計的目的是,防止後台出現同時操作增刪改類目配置;通過這種方式,能避免因為後台在同步操作到新增加或者刪除了某個類目之後,取的緩存數據還是舊數據,這點是很關鍵的!

// 數據緩存映射 Mapconst[dataCacheMap, setDataCacheMap] = useState<{ [x: string]: SelectorItemType[] }>({});

/*** 獲取緩存 key* @param itemId selectedItem id* @param itemIndex selectedItem 當前 item 索引* @param columnIndex 當前 column 索引*/constgetCacheKey = ( itemId: string, itemIndex: number, columnIndex: number) => ` ${itemId}- ${itemIndex}- ${columnIndex}` ;

// 取緩存值asyncfunctiongetItemList( ) { constcacheKey = getCacheKey(itemId, itemIndex, columnIndex);

letnextColumnList = dataCacheMap[cacheKey]; let_selectedValues = { ...selectedValues };

if(!nextColumnList) { setLoading( true); constdata = awaitservice({ parentId: itemId }); // dataMapper 用來自定義數據轉換nextColumnList = dataMapper ? dataMapper(data.list) : data.list;}

setDataCacheMap( ( prev) => ({ ...prev,[ ` ${cacheKey}` ]: nextColumnList, }));

setLoading( false);

...}

數據請求設計

如果我們組件要與業務解耦,那麼必須要將數據請求與組件解耦;所以我們設計組件的是,提供了一個 service 屬性作為異步數據請求服務傳入;並且通過 TS 來約束 參數與響應體結構,讓接口服務返回的數據符合我們的組件所需的數據結構:單個數據項必須含有 id, parentId, label 三個必須屬性,其中 parentId 是我們處理級聯依賴的關鍵;針對不同的業務,可能第一級的 parentId 不一樣,所以我們也提供了一個 defaultParentId 作為屬性供外部傳入

如果服務層的數據無法改變,我們還提供了 dataMapper 回調函數來幫助我們格式化返回的數據

/*** 單個類目項*/exportinterfaceSelectorItemType { id: string; /*** @default '0'*/parentId: string; /*** 是否可選* @default true*/disabled?: boolean; /*** 選項文案* @default '-'*/label: string;

/*** 是否半選狀態* @default false*/indeterminate?: boolean; [x: string]: any; }

interfaceProps { ...// 外部傳入請求數據服務service: ( args: { parentId: string} ) => Promise<{ list: SelectorItemType[] }>; defaultParentId: string; dataMapper?: ( args: any) => { list: SelectorItemType[] }; /*** @default []*/data?: SelectorItemType[];onSubmit?: SubmitCallback;onCancel?: => void; }

渲染數據源設計

在有了前面的『數據緩存』、『數據請求』之後,我們接下來設計渲染所需的數據結構;從交互層面,我們最容易想到的是二維數組數據結構;通過二維數組的方式,能方便的幫助我們渲染所需的 UI;

假設我們的數據是如下數據格式:

// 組件內部數據源const [ source, setSource] = useState < SelectorItemType[][]> ([]);

但是因為我們的交互上面,是有個『部分選中』這個狀態存在,但是這個狀態 與後台類目無關,只是前端展示需要用到的欄位,所以我們需要對接口返 回的數據做一個初始化的操作:將數據源項新增一個半選狀態 indeterminate 標誌位,後續我們在處理級聯狀態的時候,需要頻繁的改動到這個狀態值

categoryList.forEach( ( item) => { result.push({...item,id: item.categoryId, label: item.title, // 半選狀態標誌位indeterminate: false, });});

< divclassName= {styles.selectorItemContainer}> {column.map((item, index) => {return (< divkey= {`${ item.id} -${ columnIndex}`} className= {styles.selectorItem}onMouseEnter= {=> debouncedHoverCallback(item.id, index, columnIndex)} >< Checkboxvalue= {Boolean(selectedValues[item.id])}disabled= {item.disabled}// 判斷是否半選indeterminate= {item.indeterminate}className= {styles.checkbox}onClick= {=> handleItemClick(index, columnIndex)} >< divclassName= {styles.labelText}> {item.label || '-'} div> Checkbox> < IconclassName= {styles.iconRight}type= "arrowright"/> div> );})}div>

已選數據設計

我們的組件是『多級多選』無限層級,在組件渲染的時候,如何判斷當前 item 項是否選中,依靠的就是我們的已選數據 state:

// 已選擇類目,組件內部維護狀態const[selectedValues, setSelectedValues] = useState({});

< Checkbox// 判斷是否選中value= {Boolean(selectedValues[item.id])}disabled= {item.disabled}indeterminate= {item.indeterminate}className= {styles.checkbox}onClick= {=> handleItemClick(index, columnIndex)} >< divclassName= {styles.labelText}> {item.label || '-'} div> Checkbox>

通過打平數據結構,我們無需關心渲染層級,時間複雜度層面也是保持 O(1);

交互邏輯詳解

Hover 事件邏輯詳情

滑鼠 hover 操作,我們主要是需要:

  1. 處理異步數據的獲取與緩存

  2. 處理當前項的子級數據狀態;通過在 Hover 的時候來控制子級的狀態,可以讓我省去遞歸子級的操作來提高我們的整體性能

注意:在 Hover 事件過程中,我們需要對 debounce 操作

import{ useDebouncedCallback } from'use-debounce'; const[debouncedHoverCallback] = useDebouncedCallback( ( itemId: string, itemIndex: number, columnIndex: number) => { setQueryData({itemId,columnIndex,itemIndex,});},100, );

key={ ` ${item.id}- ${columnIndex}` } className={styles.selectorItem}onMouseEnter={ => debouncedHoverCallback(item.id, index, columnIndex)}>....< /div>

多選項 Click 邏輯詳情

滑鼠 click 操作,核心邏輯:

  1. 改變當前點擊項狀態

  2. 改變子級狀態

  3. 改變父級狀態

數據回調

在我們選中操作完成之後,我們需要將用戶選擇的數據提交給後台,通常多級多選的數據結構設計是平級設計,所以當我們父級如果是選中的數據,那麼它的子級數據就沒有必要提交給後台了;

所以我們需要衝選中池中過濾出父級 parentId 不在選中池中的數據,這個就是我們最終需要返回給用戶與後台的數據

consthandleSubmit = => { constresult: SelectorItemType[] = Object.keys(selectedValues).map( ( key) => selectedValues[key], );// 核心邏輯:過濾出當前 parentId 不在選中池中數據,就表示它的父級沒有選中constfilterData = result.filter( ( item) => !selectedValues[item.parentId] || !item.parentId); onSubmit && onSubmit(filterData);};

Q&A

到這裡我們就基本介紹完了如何從 0 到 1完整的設計一個多級多選的組件;該組件支持任意層級的數據,只需要滿足我們的層級依賴關係的數據結構,將能復用這個組件

但是我們還有幾個思考題:

  1. 如果多選組件還需要能展示禁選項,邏輯如何調整?

  2. 如何解耦 DOM 結構與 CSS 實現

這兩個問題歡迎各位在評論區討論

SegmentFault 思否社區和文章作者展開更多互動和交流。

文章來源: https://twgreatdaily.com/zh-mo/u1iIrHIBd4Bm1__YbHjP.html

Flutter 知識點

2020-08-10