亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

使用React封裝一個(gè)Tree樹(shù)形組件的實(shí)例代碼

 更新時(shí)間:2024年03月11日 10:58:06   作者:滑動(dòng)變滾動(dòng)的蝸牛  
這篇文章主要介紹了使用React封裝一個(gè)Tree樹(shù)形組件的實(shí)例,文中通過(guò)代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下

前言

為什么要造這樣一個(gè)輪子呢?

最近在學(xué)習(xí) next ,想用 next 重構(gòu)一下自己的博客,而在 自己博客 的編輯頁(yè)面中有使用到 antd 的一個(gè)樹(shù)形的結(jié)構(gòu)組件來(lái)展示文章的分類(lèi);

而我的 個(gè)人博客 (next版) 使用的是 next-ui ,但是里面并沒(méi)有 tree 組件,看了下最近很火的 shadcn 也沒(méi)有類(lèi)似組件,我也不想為了 tree 又引入 antd 了,就想著自己封裝一個(gè)玩玩,權(quán)當(dāng)提升技術(shù)了(當(dāng)然了非 next 版)。順便還能為 我的組件庫(kù) 添加一員。

當(dāng)然我是對(duì)照 antd 作為模板開(kāi)發(fā)的,但是他的 tree 是沒(méi)有單獨(dú) check 的,當(dāng)時(shí)我的舊版博客中為了實(shí)現(xiàn)該需求我可沒(méi)少費(fèi)工夫。

線(xiàn)上 Demo

源碼

實(shí)現(xiàn)思路

我這里主要是根據(jù) antdProps 選擇一部分,并按照自身需求來(lái)增減實(shí)現(xiàn)的。

下面我就講述整個(gè) tree 樹(shù)形組件的核心部分吧,其他一些屬性就不細(xì)講了,感興趣可以直接看 源碼 。

Html 基本結(jié)構(gòu)

下面是整個(gè)組件的基本結(jié)構(gòu),renderTreeList 函數(shù)遞歸調(diào)用渲染 treechildren 節(jié)點(diǎn)。

類(lèi)名 node-content 中的就是節(jié)點(diǎn)的內(nèi)容了,根據(jù)需求樣式自定義即可。

const Tree = forwardRef<TreeInstance, TreeProps>((props, ref) => {
  const { checkable, treeData, checkedKeys, defaultExpandAll, multiple, singleSelected, selectable = true, selectedKeys: propsSelectedKeys, onCheck, onSelect, onRightClick, ...ret } = props
  
  // ... 省略部分內(nèi)容,只展示核心結(jié)構(gòu)

  // 遞歸渲染 tree 的列表 
  const renderTreeList = (list?: TreeNode[]) => {
    // checkTree 的說(shuō)明見(jiàn)下面
    if(!checkTree) return null
    return list?.map(item => {
      const checkItem = checkTree![item.key]
      return (
        <div key={item.key} className={`node`}>
          <div className={`node-content`}>
            // checkItem.show 用來(lái)判斷展開(kāi)
            <div>↓</div>
            // checkItem.checked 用來(lái)處理是否 check
            <Checkbox />
            <div>{item.title}</div>
          </div>
          <div className='children'>
            {renderTreeList(item.children)}
          </div>
        </div>
      )
    })
  }

  return (
    <div className={`${classPrefix} ${ret.className ?? ''}`} style={ret.style}>
      {renderTreeList(treeData)}
    </div>
  )
})

實(shí)現(xiàn)交互的樹(shù)形結(jié)構(gòu) (checkTree)

生成一個(gè)用于實(shí)現(xiàn)交互效果的樹(shù)形結(jié)構(gòu) ( checkTree )

export type CheckTreeItem = {
  /** 父節(jié)點(diǎn)的 key 值 */
  parentKey?: string
  /** 子節(jié)點(diǎn)的 key 數(shù)組 */
  childKeys?: string[]
  /** 是否展開(kāi) */
  show: boolean
  /** 是否選中 */
  checked: boolean
  /** 是否有 checkbox */
  checkable?: boolean
  /** 禁用 checkbox */
  disableCheckbox?: boolean
  /** 禁止整個(gè)節(jié)點(diǎn)的選擇 */
  disabled?: boolean
}

export type CheckTree = Record<string, CheckTreeItem>
// ...
const [checkTree, setCheckTree] = useState<CheckTree>();

整體是一個(gè)只有一層結(jié)構(gòu)的對(duì)象,使用每一項(xiàng)數(shù)據(jù)中唯一的 key 值作為 checkTree 的 key,通過(guò) parentKey 和 childKeys 來(lái)查找該節(jié)點(diǎn)的 父子兄弟節(jié)點(diǎn)。

例:

初始化樹(shù)形結(jié)構(gòu)

根據(jù) generateCheckTree 函數(shù)的遞歸調(diào)用,將傳入的 treeData 樹(shù)狀結(jié)構(gòu)數(shù)據(jù)轉(zhuǎn)變?yōu)榻M件需要的 checkTree。

// ...
useEffect(() => {
  if(!treeData?.length) return
  const generateCheckTree = (list: TreeNode[], parentKey?: string) => {
    return list?.reduce((pre, cur) => {
      // checkedKeys 就是默認(rèn)傳入 check 項(xiàng),用于默認(rèn)是否勾選
      const curChecked = Boolean(checkedKeys?.includes(cur.key));
      pre[cur.key] = {
        // 默認(rèn)是否展開(kāi)該樹(shù)形結(jié)構(gòu)
        show: !!defaultExpandAll, 
        checked: curChecked, 
        parentKey,
      }
      // 一些屬性的默認(rèn)值
      if(cur.checkable) pre[cur.key].checkable = true
      if(cur.disableCheckbox) pre[cur.key].disableCheckbox = true
      if(cur.disabled) pre[cur.key].disabled = true
      // 有孩子節(jié)點(diǎn)就遞歸調(diào)用,生成數(shù)據(jù)
      if(cur.children?.length) {
        pre[cur.key].childKeys = cur.children.map(c => c.key)
        const treeChild = generateCheckTree(cur.children, cur.key)
        pre = {...pre, ...treeChild}
      }
      return pre
    }, {} as CheckTree)
  }
  const state = generateCheckTree(treeData)
  setCheckTree(state)
  // ...
}, [treeData])

大致就是如下圖所示,將 treeData 轉(zhuǎn)變?yōu)?nbsp;checkTree

點(diǎn)擊 check 節(jié)點(diǎn)

對(duì)應(yīng)上面 html 結(jié)構(gòu)中的 CheckBox 位置, checkable 等屬性就是用來(lái)判斷是否展示禁用 CheckBox 的。

// ...
{(checkable && item.checkable !== false) && (
  <CheckBox 
    checked={checkItem.checked} 
    disabled={item.disabled || item.disableCheckbox}
    // 先忽略,用來(lái)判斷當(dāng)前是否有孩子節(jié)點(diǎn)被選中了,true 則代表需要展示 checkbox 的半選樣式
    indeterminate={getIsSomeChildCheck(checkItem, checkTree)}
    onChange={() => {
      if(item.disabled || item.disableCheckbox) return
      onNodeCheck(item.key)
    }} 
  />   
)}

先看 onChange 中觸發(fā)的回調(diào) onNodeCheck 函數(shù),該函數(shù)主要是將 checkItem 中對(duì)應(yīng)該項(xiàng)的 checked 取反一下。

/** 點(diǎn)擊選中節(jié)點(diǎn) */
const onNodeCheck = (key: string) => {
  const checkItem = checkTree![key]
  const curChecked = !checkItem.checked
  checkItem.checked = curChecked;

  // 先忽略,用來(lái)判斷是否是單選的
  if(singleSelected) onSingleCheck(key, curChecked)
  else onCheckChildAndParent(key, curChecked)
  setCheckTree({...checkTree})

  // 先忽略,用來(lái)獲取當(dāng)前 check 的所有 key 值
  const keys = getCheckKeys(checkTree!)

  // check 觸發(fā)的組件回調(diào)
  onCheck?.(keys, {
    key, 
    // 這步判斷主要是單選時(shí),選擇父節(jié)點(diǎn)時(shí)只會(huì)選中其子節(jié)點(diǎn)
    checked: keys.includes(key) ? curChecked : false,  
    parentKeys: getParentKeys(key, checkTree!),
    treeDataItem: getTreeDataItem(key, treeData),
  })
}

然后通過(guò) onCheckChildAndParent 函數(shù),處理對(duì)應(yīng)的父子節(jié)點(diǎn)的選中狀態(tài)。

  • 子節(jié)點(diǎn): checkAllChild 遞歸將當(dāng)前節(jié)點(diǎn)的 子節(jié)點(diǎn) 全選或全不選。

  • 父節(jié)點(diǎn): checkAllParent 遞歸處理當(dāng)前節(jié)點(diǎn)的 父節(jié)點(diǎn) 的選中狀態(tài)。

  • 兄弟節(jié)點(diǎn):只有在單選節(jié)點(diǎn)的時(shí)候需要,選擇同層節(jié)點(diǎn),使 兄弟節(jié)點(diǎn) 取消選中

/** 處理父子節(jié)點(diǎn)的選中狀態(tài) */
const onCheckChildAndParent = (key: string, curChecked: boolean, cTree = checkTree!) => {
  const checkItem = cTree[key];

  // 全選/不選所有子節(jié)點(diǎn)
  (function checkAllChild(childKeys?: string[]) {
    childKeys?.forEach(childKey => {
      cTree[childKey].checked = curChecked
      checkAllChild(cTree[childKey].childKeys)
    })
  })(checkItem.childKeys);

  // 處理父節(jié)點(diǎn)的選中狀態(tài)
  (function checkAllParent(parentKey?: string) {
    if(!parentKey) return
    if(!curChecked) { // 取消所有父節(jié)點(diǎn)的選中
      cTree[parentKey].checked = false
      checkAllParent(cTree[parentKey].parentKey)
    } else { // 將所有子節(jié)點(diǎn)被全選的父節(jié)點(diǎn)也選中
      const isSiblingCheck = !!cTree[parentKey].childKeys?.every(childKey => cTree[childKey].checked)
      if(isSiblingCheck) { // 判斷兄弟節(jié)點(diǎn)是否也全被選中
        cTree[parentKey].checked = true
        checkAllParent(cTree[parentKey].parentKey)
      }
    }
  })(checkItem.parentKey);

  // 同層單選時(shí),使兄弟節(jié)點(diǎn)取消選中
  if(singleSelected && curChecked) {
    const keys = cTree[key].parentKey ? cTree[cTree[key].parentKey!].childKeys : firstNodeKeys
    keys?.forEach(siblingKey => {
      if(siblingKey !== key) {
        cTree[siblingKey].checked = false
      }
    })
  }
}

子節(jié)點(diǎn)的展開(kāi)實(shí)現(xiàn)

html 結(jié)構(gòu)和 css 簡(jiǎn)單樣式如下,通過(guò) show 屬性給 children 節(jié)點(diǎn)賦高度,由于定義了 transition 屬性,所以當(dāng)高度變化時(shí),就會(huì)觸發(fā)節(jié)點(diǎn)的 展開(kāi)/收縮 動(dòng)畫(huà)。

<div 
  className={`node-children`} 
  // height: fit-content; 無(wú)法觸發(fā)過(guò)渡效果,需要準(zhǔn)確的值
  // 也可通過(guò) maxHeight 設(shè)置一個(gè)很大的值來(lái)解決,但值過(guò)大又會(huì)使過(guò)度效果難看,所以這里需要獲取一個(gè)準(zhǔn)確的高度
  style={{maxHeight: checkItem.show ? `${getTreeChildHeight(item.children!)}px` : 0}}
>
  {renderTreeList(item.children)}
</div>
.node-children {
  padding-left: 24px;
  overflow-y: hidden;
  transition: max-height 0.3s ease;
}

這里有一個(gè)點(diǎn)要注意,就是無(wú)法直接給子節(jié)點(diǎn)定義一個(gè)由內(nèi)容撐開(kāi)的高度 height: fit-content;,這樣會(huì)使 transition 無(wú)法正常觸發(fā)。當(dāng)然可以通過(guò)給一個(gè)比較大的 maxHeight 來(lái)設(shè)置最大高度,這樣 transition 就會(huì)以 maxHeight 的高度實(shí)現(xiàn)動(dòng)畫(huà)效果,但是這樣當(dāng)子節(jié)點(diǎn)總高度和 maxHeight 出入過(guò)大時(shí)就會(huì)使動(dòng)畫(huà)效果很不好看。

所以我這里最終通過(guò) getTreeChildHeight 函數(shù)來(lái)準(zhǔn)確計(jì)算孩子節(jié)點(diǎn)的總高度了。

首先等待 checkTree 完成構(gòu)建以及樹(shù)形結(jié)構(gòu)渲染完成,然后準(zhǔn)確獲取每個(gè)節(jié)點(diǎn)的高度,因?yàn)槊總€(gè)節(jié)點(diǎn)的 title 都是 ReactNode ,所以需要都獲取一遍他們的高度。

/** 標(biāo)題的最小高度 */
const TITLE_MIN_HEIGHT = 24;
/** 每個(gè)標(biāo)題的下邊距 */
const TITLE_MB = 6;

// 等待樹(shù)形結(jié)構(gòu)渲染完畢,獲取 title 的高度
useEffect(() => {
  if(!checkTree || !isTreeRender.current) return
  const info: TitleNodeInfo = {};
  for(let key in checkTree) {
    // 每個(gè)標(biāo)題渲染的內(nèi)容,都要根據(jù) key 給一個(gè)唯一的類(lèi)名。
    const titleNode = document.querySelector(`.node-title-${key}`)
    if(titleNode) {
      info[key] = {height: Math.max(titleNode.clientHeight, TITLE_MIN_HEIGHT) + TITLE_MB} 
    }
  }
  setTitleNodeInfo(info)
  isTreeRender.current = false
}, [checkTree])

此時(shí)每個(gè)節(jié)點(diǎn)的 children 節(jié)點(diǎn)的高度,就能通過(guò) getTreeChildHeight 函數(shù)遞歸計(jì)算得出了。

const getTreeChildHeight = (list: TreeNode[]) => {
  return list?.reduce((pre, cur) => {
    pre += (titleNodeInfo[cur.key]?.height ?? (TITLE_MIN_HEIGHT + TITLE_MB))
    if(checkTree![cur.key].show && cur.children?.length) {
      pre += getTreeChildHeight(cur.children)
    }
    return pre
  }, 0) ?? 0
}

ref 方法

然后我在組件里面實(shí)現(xiàn)了一些用于獲取 treeData 數(shù)據(jù)的一些方法,簡(jiǎn)單來(lái)說(shuō)都是一些遞歸調(diào)用等方法。

屬性名描述類(lèi)型
getCheckTree獲取當(dāng)前選中的樹(shù)形結(jié)構(gòu)() => CheckTree | undefined
getParentKeys根據(jù) key 值獲取其父節(jié)點(diǎn),從 key 節(jié)點(diǎn)的最親關(guān)系開(kāi)始排列(key: string) => string[] | undefined
getSiblingKeys根據(jù) key 值獲取其兄弟節(jié)點(diǎn),會(huì)包括自身節(jié)點(diǎn)(key: string) => string[] | undefined
getChildKeys根據(jù) key 值獲取其子節(jié)點(diǎn)(key: string) => string[] | undefined
getCheckKeys獲取當(dāng)前 check 中的所有 key() => string[]
getTreeDataItem獲取當(dāng)前 treeData 中的節(jié)點(diǎn)數(shù)據(jù)(key: string) => TreeNode | undefined

最終實(shí)現(xiàn)的 Props

其他屬性的功能實(shí)現(xiàn)我就不一一敘述了,感興趣可以直接看 源碼

屬性名描述類(lèi)型默認(rèn)值
checkable是否有選擇框booleanfalse
checkedKeys(受控)選中復(fù)選框的樹(shù)節(jié)點(diǎn)的key,當(dāng)不在數(shù)組中的父節(jié)點(diǎn)需要被選中時(shí),對(duì)應(yīng)節(jié)點(diǎn)也將選中,觸發(fā) onCheck 回調(diào),使該值保持正確string[]null
defaultExpandAll默認(rèn)展開(kāi)所有樹(shù)節(jié)點(diǎn)booleanfalse
multiple支持點(diǎn)選多個(gè)節(jié)點(diǎn)(節(jié)點(diǎn)本身)booleanfalse
singleSelected是否只能單選一個(gè)節(jié)點(diǎn)booleanfalse
selectable是否可選中booleantrue
selectedKeys(受控)設(shè)置選中的樹(shù)節(jié)點(diǎn),多選需設(shè)置 multiple 為 truestring[]"-"
treeData樹(shù)形結(jié)構(gòu)的數(shù)據(jù)TreeNode[]--
onCheck點(diǎn)擊復(fù)選框觸發(fā)(checkedKeys: string[], params?: OnCheckParams) => void--
onSelect點(diǎn)擊樹(shù)節(jié)點(diǎn)觸發(fā)(selectKeys: string[], params: OnSelectParams) => void--
onRightClick點(diǎn)擊右鍵觸發(fā)(params: onRightClickParams) => void--
className類(lèi)名string--
stylestyle樣式{}--
childrenchildren節(jié)點(diǎn)ReactNode--
ref-TreeInstance--

以上就是使用React封裝一個(gè)Tree樹(shù)形組件的實(shí)例代碼的詳細(xì)內(nèi)容,更多關(guān)于React封裝Tree組件的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • react native帶索引的城市列表組件的實(shí)例代碼

    react native帶索引的城市列表組件的實(shí)例代碼

    本篇文章主要介紹了react-native城市列表組件的實(shí)例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-08-08
  • react 項(xiàng)目 中使用 Dllplugin 打包優(yōu)化技巧

    react 項(xiàng)目 中使用 Dllplugin 打包優(yōu)化技巧

    在用 Webpack 打包的時(shí)候,對(duì)于一些不經(jīng)常更新的第三方庫(kù),比如 react,lodash,vue 我們希望能和自己的代碼分離開(kāi),這篇文章主要介紹了react 項(xiàng)目 中 使用 Dllplugin 打包優(yōu)化,需要的朋友可以參考下
    2023-01-01
  • React組件的用法概述

    React組件的用法概述

    React組件用來(lái)實(shí)現(xiàn)局部功能效果的代碼和資源的集合(html/css/js/image等等),這篇文章主要介紹了React組件的用法和理解,需要的朋友可以參考下
    2023-02-02
  • React超詳細(xì)講述Fiber的使用

    React超詳細(xì)講述Fiber的使用

    在fiber出現(xiàn)之前,react的架構(gòu)體系只有協(xié)調(diào)器reconciler和渲染器render。當(dāng)前有新的update時(shí),react會(huì)遞歸所有的vdom節(jié)點(diǎn),如果dom節(jié)點(diǎn)過(guò)多,會(huì)導(dǎo)致其他事件影響滯后,造成卡頓。即之前的react版本無(wú)法中斷工作過(guò)程,一旦遞歸開(kāi)始無(wú)法停留下來(lái)
    2023-02-02
  • react組件實(shí)例屬性props實(shí)例詳解

    react組件實(shí)例屬性props實(shí)例詳解

    這篇文章主要介紹了react組件實(shí)例屬性props,本文結(jié)合實(shí)例代碼給大家簡(jiǎn)單介紹了props使用方法,代碼簡(jiǎn)單易懂,需要的朋友可以參考下
    2023-01-01
  • 詳解React Fiber的工作原理

    詳解React Fiber的工作原理

    這篇文章主要介紹了React Fiber的工作原理的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用React框架,感興趣的朋友可以了解下
    2021-04-04
  • 詳解使用React.memo()來(lái)優(yōu)化函數(shù)組件的性能

    詳解使用React.memo()來(lái)優(yōu)化函數(shù)組件的性能

    本文講述了開(kāi)發(fā)React應(yīng)用時(shí)如何使用shouldComponentUpdate生命周期函數(shù)以及PureComponent去避免類(lèi)組件進(jìn)行無(wú)用的重渲染,以及如何使用最新的React.memo API去優(yōu)化函數(shù)組件的性能
    2019-03-03
  • React?SSG實(shí)現(xiàn)Demo詳解

    React?SSG實(shí)現(xiàn)Demo詳解

    這篇文章主要為大家介紹了React?SSG實(shí)現(xiàn)Demo詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>
    2023-07-07
  • react遞歸組件實(shí)現(xiàn)樹(shù)的示例詳解

    react遞歸組件實(shí)現(xiàn)樹(shù)的示例詳解

    在一些react項(xiàng)目中,常常有一些需要目錄樹(shù)這種結(jié)構(gòu),這篇文章主要為大家介紹了如何使用遞歸組件實(shí)現(xiàn)樹(shù),感興趣的小伙伴可以了解下
    2024-10-10
  • JavaScript React如何修改默認(rèn)端口號(hào)方法詳解

    JavaScript React如何修改默認(rèn)端口號(hào)方法詳解

    這篇文章主要介紹了JavaScript React如何修改默認(rèn)端口號(hào)方法詳解,文中通過(guò)步驟圖片解析介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07

最新評(píng)論