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

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

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

效果預覽

單個項選中

單個選中項

多個部分項選中

多個選中項

需求分析

在拿到需求以後,咱們首先要作的是需求分析;經過上面的效果預覽咱們能夠初步知道咱們所須要處理的核心邏輯:前端

  1. 默認加載第一層級數據
  2. 鼠標 hover數組

    1. 異步獲取數據
    2. 切換下級渲染數據
  3. 鼠標點擊緩存

    1. 點擊當前項狀態改變:選中 or 未選中
    2. 當前項的父級狀態改變:選中、半選、不選中,而且須要遞歸處理
    3. 當前項的子級狀態改變:全選、全不選

組件設計

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

  • 組件提供一個 service 入參,service 是一個返回 Promise 的異步請求方法
  • 組件提供一個 dataMapper,用來作數據轉換,將 service 請求返回的值轉化爲符合咱們組件數據解構的數據
  • 組件內部經過調用外部傳入的 service 來獲取數據

入參設計以下:app

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

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

總體思路設計

經過上面的 UI 呈現,如今你們應該有個基礎的認識,咱們須要作什麼樣的需求了。異步

咱們在接到一個需求的時候,先不要着急着碼代碼,更好的方式是先規劃咱們的組件方案設計,而且提早思考好各類邏輯分支;
這裏給你們看下個人設計初稿,我習慣性的選擇腦圖來發散本身的思惟:
腦圖草稿async

經過上圖,咱們可以在大腦中有個大概的清晰認識到咱們須要作哪些核心模塊的設計與開發,接下來就是規劃咱們的核心模塊劃分:函數

  • 數據緩存
  • 異步數據獲取
  • 選中數據緩存
  • 渲染數據源設計

核心模塊劃分

數據緩存設計

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

那麼若是如何設計才能作到性能最優呢?經過上面的腦圖,咱們初步是經過一個 dataCaheMap 來緩存異步拉取回來的數據,這樣子咱們在取的時候,時間複雜度就是 O(1) ;既然是有 Map 來緩存數據,那麼用什麼做爲 key 也是咱們緩存的關鍵;
在這個組件裏面,最終我選擇的是:列索引+行索引+id 做爲緩存 key優化

這樣設計的目的是,防止後臺出現同時操做增刪改類目配置;經過這種方式,能避免由於後臺在同步操做到新增長或者刪除了某個類目以後,取的緩存數據仍是舊數據,這點是很關鍵的!

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

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

// 取緩存值
async function getItemList() {
  const cacheKey = getCacheKey(itemId, itemIndex, columnIndex);

  let nextColumnList = dataCacheMap[cacheKey];
  let _selectedValues = { ...selectedValues };

  if (!nextColumnList) {
    setLoading(true);
    const data = await service({ 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 回調函數來幫助咱們格式化返回的數據

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

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

interface Props {
  ...
  // 外部傳入請求數據服務
  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,
  });
});

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

已選數據設計

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

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

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

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

交互邏輯詳解

Hover 事件邏輯詳情

鼠標 hover 操做,咱們主要是須要:

  1. 處理異步數據的獲取與緩存
  2. 處理當前項的子級數據狀態;經過在 Hover 的時候來控制子級的狀態,可讓我省去遞歸子級的操做來提升咱們的總體性能

Hover Detail

多選項 Click 邏輯詳情

鼠標 click 操做,核心邏輯:

  1. 改變當前點擊項狀態
  2. 改變子級狀態
  3. 改變父級狀態

HandleItemClick.png

數據回調

在咱們選中操做完成以後,咱們須要將用戶選擇的數據提交給後臺,一般多級多選的數據結構設計是平級設計,因此當咱們父級若是是選中的數據,那麼它的子級數據就沒有必要提交給後臺了;

因此咱們須要衝選中池中過濾出父級 parentId 不在選中池中的數據,這個就是咱們最終須要返回給用戶與後臺的數據

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

Q&A

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

可是咱們還有幾個思考題:

  1. 若是多選組件還須要能展現禁選項,邏輯如何調整?
  2. 如何解耦 DOM 結構與 CSS 實現

這兩個問題歡迎各位在下面討論

相關文章
相關標籤/搜索