Vue3非遞歸渲染Tree組件的初步實(shí)現(xiàn)代碼
本節(jié)開始,咱們一起來(lái)開發(fā)一個(gè)tree
組件,通過逐步迭代,使其從光禿禿、毫無(wú)生機(jī)變?yōu)樯鷻C(jī)盎然、裝飾漂亮。
樹結(jié)構(gòu)的定義
首先對(duì)于樹結(jié)構(gòu),我們自然想到下面的展現(xiàn)形式:
[ { label: '前端', id: 'frontend' }, { label: 'java', id: 'java', expanded: true, children: [ { label: '11', id: '11' }, { label: '22', id: '22' }, { label: '33', id: '33' }, { label: '44', id: '44', expanded: true, children: [ { label: 'aaa', id: 'aaa', expanded: true, children: [ { label: 'a11', id: 'a11' }, { label: 'a22', id: 'a22' } ] }, { label: 'bbb', id: 'bbb', expanded: false, children: [ { label: 'b11', id: 'b11' }, { label: 'b22', id: 'b22' } ] } ] }, { label: '55', id: '55' }, { label: '66', id: '66' }, { label: '77', id: '77' }, { label: 'Java基礎(chǔ)', id: 'javaBasic' }, { label: 'Java Web', id: 'javaWeb', children: [ { label: 'ssm', id: 'ssm' } ] } ] }, { label: '數(shù)據(jù)庫(kù)', id: 'db', expanded: false, children: [ { label: '關(guān)系型數(shù)據(jù)庫(kù)', id: 'relationShipDB' }, { label: '非關(guān)系型數(shù)據(jù)庫(kù)', id: 'nonRelationShipDB' } ] } ]
這里,小卷隨便造了些數(shù)據(jù),這是一個(gè)帶有children
子節(jié)點(diǎn)數(shù)組的嵌套結(jié)構(gòu),此外還具有的屬性:label
(標(biāo)簽)、id
(節(jié)點(diǎn)標(biāo)識(shí)id)和expanded
(是否展開)。很自然,我們可以為這種嵌套結(jié)構(gòu)的節(jié)點(diǎn)抽取出類型定義,創(chuàng)建一個(gè)維護(hù)樹節(jié)點(diǎn)類型定義的types.ts
文件: src/components/tree/types.ts
// 定義基本的樹節(jié)點(diǎn)類型 export interface ITreeNode { label: string // 節(jié)點(diǎn)標(biāo)簽名 id?: string // 節(jié)點(diǎn)id,可為空 children?: ITreeNode[] // 子節(jié)點(diǎn)列表,可為空 expanded?: boolean //是否展開,空則表示默認(rèn)折疊 }
這里咱們創(chuàng)建并導(dǎo)出一個(gè)ITreeNode
類型,其中label
字段是非空的,children
是自身類型的一個(gè)數(shù)組。 有了前面的tree
數(shù)據(jù)結(jié)構(gòu)和節(jié)點(diǎn)類型的定義,是不是咱們就可以編寫組件模板來(lái)渲染這棵樹了呢?答案是肯定的!我們可以定義一個(gè)TreeNode
的組件,對(duì)其子節(jié)點(diǎn)列表的渲染采用遍歷的方式繼續(xù)遞歸渲染子組件TreeNode
,這種思路是開發(fā)具有嵌套的層級(jí)結(jié)構(gòu)組件大伙兒所能想到的常規(guī)思路。
咱們這里要介紹的是對(duì)嵌套數(shù)據(jù)結(jié)構(gòu)進(jìn)行扁平化處理后,采用列表的形式來(lái)渲染,只不過樹的層級(jí)結(jié)構(gòu)是由節(jié)點(diǎn)元素按照層級(jí)進(jìn)行一定的paddingLeft
來(lái)實(shí)現(xiàn)的。隨著后續(xù)的學(xué)習(xí),小伙伴會(huì)發(fā)現(xiàn),盡管tree
組件的渲染不是遞歸的,但是對(duì)鋪平節(jié)點(diǎn)列表之前的拍平處理以及后續(xù)子節(jié)點(diǎn)的計(jì)算處理卻依然采用的遞歸的思想呢。
注意
拍平結(jié)構(gòu)避免了樹組件內(nèi)部的遞歸渲染,但帶來(lái)的麻煩是,需要開發(fā)者對(duì)于子節(jié)點(diǎn)范圍劃定做更多的編程處理。后續(xù)開發(fā)中,小卷會(huì)提供思路,如何來(lái)簡(jiǎn)化這種處理方式。
樹結(jié)構(gòu)拍平處理
現(xiàn)在咱們寫一個(gè)工具函數(shù)對(duì)之前的嵌套結(jié)構(gòu)進(jìn)行拍平處理。小伙伴們跟著小卷的思路,咱們先來(lái)實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的數(shù)組結(jié)果處理的需求:把一個(gè)數(shù)組中的元素復(fù)制到一個(gè)新數(shù)組中。創(chuàng)建一個(gè)utils.ts
的工具函數(shù)編寫文件:
src/components/tree/utils.ts
function copyArr(arr: number[]) { return arr.reduce((result, cur) => { return result.concat(cur) }, [] as number[]) } const newArr = copyArr([1, 2, 3]) console.log(newArr)
這里我們巧用了Array
的reduce
方法,在遍歷每個(gè)元素時(shí)進(jìn)行轉(zhuǎn)存的處理,把結(jié)果存入箭頭回調(diào)函數(shù)的第一個(gè)參數(shù)result
中,而在reduce
的第2個(gè)參數(shù)中對(duì)這個(gè)存放轉(zhuǎn)存元素的數(shù)組進(jìn)行初始化。 現(xiàn)在我們測(cè)試這個(gè)copyArr
函數(shù),可以全局安裝一個(gè)ts-node
工具來(lái)執(zhí)行.ts
文件:
npm i -g ts-node@10.9.2
同時(shí),在工程tsconfig.json
中加入ts-node
配置,啟用對(duì)es module
的編譯支持:
{ ... "ts-node": { "esm": true }, ... }
測(cè)試下,ok!
核心處理函數(shù)
發(fā)散思維
對(duì)于嵌套的樹結(jié)構(gòu),我們只需要寫一個(gè)函數(shù),接收一個(gè)代表當(dāng)前層級(jí)的數(shù)組,也就是通過數(shù)組的
reduce
方法,將當(dāng)前層級(jí)的每個(gè)節(jié)點(diǎn)放到一個(gè)新的數(shù)組中,而對(duì)于父節(jié)點(diǎn)的情況,我們遞歸調(diào)用該函數(shù),對(duì)其子節(jié)點(diǎn)列表做相同的處理,將得到的拍平的數(shù)組,插入到當(dāng)前父節(jié)點(diǎn)之后即可。
ok!現(xiàn)在我們可以輕易寫出拍平核心處理函數(shù):
src/components/tree/utils.ts
import { ITreeNode } from './types' export function generateFlatTree(tree: ITreeNode[]): ITreeNode[] { return tree.reduce((prev, cur) => { if (cur.children) { // 遞歸,得到子節(jié)點(diǎn)拍平的數(shù)組 const children = generateFlatTree(cur.children) return prev.concat(cur, children) } else { return prev.concat(cur) } }, [] as ITreeNode[]) }
測(cè)試一下,ok!
扁平化數(shù)據(jù)結(jié)構(gòu)
現(xiàn)在我們系統(tǒng)拍平后的結(jié)構(gòu)能夠展示父節(jié)點(diǎn)id、所處的層級(jí),并且把嵌套的children
屬性移除掉,用一個(gè)是否葉子節(jié)點(diǎn)的標(biāo)記屬性代替,也就是說,咱們要定義一個(gè)新的IFlattreeNode
結(jié)構(gòu)來(lái)替換掉原始的ITreeNode
,我們將在原來(lái)節(jié)點(diǎn)類型基礎(chǔ)上進(jìn)行擴(kuò)展: src/components/tree/types.ts
... // 擴(kuò)展層級(jí)關(guān)系,用于拍平結(jié)構(gòu)的樹節(jié)點(diǎn) export interface IFlatTreeNode extends ITreeNode { parentId?: string // 父節(jié)點(diǎn)id,若是一級(jí)節(jié)點(diǎn)則為空 level: number // 節(jié)點(diǎn)層級(jí),數(shù)值從1開始 isLeaf: boolean //是否為葉子節(jié)點(diǎn) originalNode: ITreeNode // 指向?qū)?yīng)的原始節(jié)點(diǎn) }
這里,我們對(duì)ITreeNode
擴(kuò)展了4個(gè)字段,其中parentId
可為空,originalNode
的引用有助于對(duì)其子節(jié)點(diǎn)做遞歸計(jì)算處理。
對(duì)generateFlatTree
函數(shù)進(jìn)一步完善后導(dǎo)出,方法參數(shù)擴(kuò)展level
和pid
參數(shù),使得遞歸時(shí)能綁定節(jié)點(diǎn)上下級(jí)關(guān)系,對(duì)拍平的節(jié)點(diǎn)這里我們從原節(jié)點(diǎn)拷貝出一個(gè)對(duì)象作為IFlatTreeNode
類型,后續(xù)會(huì)通過邏輯處理為其擴(kuò)展屬性賦值,并最終把children
屬性移除掉,完善后的核心代碼:
import { IFlatTreeNode, ITreeNode } from './types' /** * * @param tree 當(dāng)前層級(jí)的節(jié)點(diǎn)列表 * @param level 表示當(dāng)前節(jié)點(diǎn)所處的層級(jí) * @param pid 父節(jié)點(diǎn)id */ export function generateFlatTree(tree: ITreeNode[], level = 0, pid = ''): IFlatTreeNode[] { level++ // 層級(jí)加1 // 巧用數(shù)組的reduce方法對(duì)嵌套的樹形結(jié)構(gòu)進(jìn)行拍平處理,prev為當(dāng)前層級(jí)要處理的拍平結(jié)構(gòu)結(jié)果,cur為當(dāng)前遍歷的節(jié)點(diǎn) return tree.reduce((prev, cur) => { // 拷貝當(dāng)前節(jié)點(diǎn) const o = { ...cur } as IFlatTreeNode // 綁定關(guān)系 o.originalNode = cur o.level = level // 為內(nèi)層節(jié)點(diǎn)設(shè)置父id if (level > 1 && pid) { o.parentId = pid } // 判斷當(dāng)前節(jié)點(diǎn)是否存在children,如果存在則遞歸處理 if (o.children) { // 以當(dāng)前節(jié)點(diǎn)作為父節(jié)點(diǎn),對(duì)子節(jié)點(diǎn)列表做遞歸處理,得到內(nèi)部拍平的內(nèi)容 const children = generateFlatTree(o.children, level, o.id!) // 移除嵌套結(jié)構(gòu) delete o.children // 在已經(jīng)拍平的結(jié)構(gòu)基礎(chǔ)上再拼接當(dāng)前節(jié)點(diǎn)和內(nèi)部拍平節(jié)點(diǎn) o.isLeaf = false return prev.concat(o, children) } else { // 葉子節(jié)點(diǎn),處理會(huì)更簡(jiǎn)單 o.isLeaf = true return prev.concat(o) } }, [] as IFlatTreeNode[]) }
通過ts-node
進(jìn)行測(cè)試,ok!
Tree組件開發(fā)
有了前面樹結(jié)構(gòu)轉(zhuǎn)換的鋪墊,樹組件的開發(fā)會(huì)變得非常簡(jiǎn)單!在types.ts
中定義組件的data
屬性:
import { ExtractPropTypes, PropType } from 'vue' // 樹數(shù)據(jù)的屬性定義 export const props = { data: { // 類型為一個(gè)元素為ITreeNode類型的數(shù)組 type: Object as PropType<Array<IFlatTreeNode>>, required: true } } as const // 設(shè)置為常量,外部無(wú)法修改 // 提取tree組件的屬性定義類型 export type Props = ExtractPropTypes<typeof props> ...
注意,這里節(jié)點(diǎn)的定義類型為IFlatTreeNode
,這樣我們可以在外部先完成拍平操作后再給tree
組件傳入data
屬性即可。
src/components/tree/index.tsx
import { defineComponent } from 'vue' import { props, Props } from './types' export default defineComponent({ name: 'JuanTree', props, setup(props: Props) { // 解構(gòu)出傳入的tree數(shù)據(jù) const { data } = props return () => { return ( <div class='juan-tree'> {/* 相當(dāng)于v-for */} {data.map((treeNode) => ( <div key={treeNode.id} class='juan-tree-node' style={{ /* 樹的層級(jí)縮進(jìn) */ paddingLeft: `${24 * (treeNode.level - 1)}px` }} > {treeNode.label} </div> ))} </div> ) } } })
值得注意的是,這里tsx的元素遍歷的寫法,需要為元素綁定key
,以實(shí)現(xiàn)節(jié)點(diǎn)變化后的局部dom
渲染。這里我們對(duì)節(jié)點(diǎn)元素<div>
采用了動(dòng)態(tài)綁定style
屬性的方式依據(jù)level
來(lái)決定其左邊留白的距離。
在App
組件中應(yīng)用JuanTree
組件:
import { defineComponent } from 'vue' import JuanTree from './components/tree' import { generateFlatTree } from './components/tree/utils' import { ITreeNode } from './components/tree/types' export default defineComponent({ setup() { // 這里數(shù)據(jù)省略 const treeData = [...] as ITreeNode[] return () => { // 樹的扁平化處理 const flatTree = generateFlatTree(treeData) return ( <div class='m-4'> <JuanTree class='bg-gray-200' data={flatTree}></JuanTree> </div> ) } } })
看下頁(yè)面效果,粗略的展示出一棵樹:
實(shí)現(xiàn)樹節(jié)點(diǎn)的展開與折疊
思路點(diǎn)撥
實(shí)際要渲染的樹結(jié)構(gòu),咱們應(yīng)該排除掉所有
expanded
不為true
的節(jié)點(diǎn)的后代節(jié)點(diǎn)。也就是應(yīng)該有一個(gè)方法來(lái)計(jì)算一個(gè)IFlatTreeNode
下所有后代節(jié)點(diǎn)的長(zhǎng)度,在從上到下對(duì)傳入的data
進(jìn)行遍歷時(shí),跳過這些長(zhǎng)度的節(jié)點(diǎn)即可得到最終要渲染的樹的列表結(jié)構(gòu)。
這里要計(jì)算的后代節(jié)點(diǎn)長(zhǎng)度,聰明的小伙伴想到在generateFlatTree
工具函數(shù)中,其實(shí)基于遞歸調(diào)用得到的結(jié)果,咱們是直接可以利用的。我們不妨打印到控制臺(tái)看看:
if (o.children) { const children = generateFlatTree(o.children, level, o.id!) console.log(o.id + '子節(jié)點(diǎn)長(zhǎng)度:' + children.length) ... }
這樣還要啥自行車,咱直接獲取就行了嘛。自然我們可以在IFlatTreeNode
類型中擴(kuò)展一個(gè)length
屬性:
export interface IFlatTreeNode extends ITreeNode { ... length: number // 所有子孫節(jié)點(diǎn)的長(zhǎng)度 }
在generateFlatTree
函數(shù)中判斷是父節(jié)點(diǎn)的邏輯中設(shè)置下該屬性:
if (o.children) { const children = generateFlatTree(...) // 記錄當(dāng)前節(jié)點(diǎn)子代的長(zhǎng)度 o.length = children.length ... }
接著,咱們需要借助于計(jì)算屬性對(duì)響應(yīng)式的tree
數(shù)據(jù)進(jìn)行計(jì)算,得到真正要展示的數(shù)據(jù),自然我們想到在tree
組件的setup
方法中進(jìn)行這樣的處理:
// 讓其變?yōu)轫憫?yīng)式數(shù)據(jù)以加入計(jì)算屬性的計(jì)算 const flatData = ref(data) // 獲取那些展開的節(jié)點(diǎn)列表 const expandedTree = computed(() => { const result = [] // 循環(huán)列表,跳過那些非expanded for (let i = 0; i < flatData.value.length; i++) { const item = flatData.value[i] // 當(dāng)當(dāng)前節(jié)點(diǎn)處于折疊狀態(tài),它的子節(jié)點(diǎn)應(yīng)該被排除 if (!item.isLeaf && item.expanded !== true) { // 跳過內(nèi)部所有的節(jié)點(diǎn) i += item.length } result.push(item) } // 得到折疊后的新節(jié)點(diǎn)列表 return result })
此時(shí),模板使用的是計(jì)算屬性返回的數(shù)據(jù),遍歷的是expandedTree.value
:
<div ...> {expandedTree.value.map((treeNode) => ( ... ))} </div>
看下效果,ok!正是我們需要的折疊后的效果!
對(duì)是否展開的節(jié)點(diǎn)做下標(biāo)記,在渲染的節(jié)點(diǎn)模板中我們暫且以button
元素作為節(jié)點(diǎn)折疊、展開的修飾部件:
<div ...> {treeNode.isLeaf ? ( /* 葉子節(jié)點(diǎn)臨時(shí)展示,留出間距,確保同級(jí)的父節(jié)點(diǎn)和葉子節(jié)點(diǎn)對(duì)齊 */ <span class='mr-1 inline-block w-[20px]'></span> ) : ( <button class='mr-1 inline-block h-[18px] w-[20px]'> {/* 父節(jié)點(diǎn)的展開/折疊操作臨時(shí)用+、-代替 */} {treeNode.expanded ? <span>-</span> : <span>+</span>} </button> )} {treeNode.label} </div>
再瞅一眼,good!
最后再來(lái)錦上添花,實(shí)現(xiàn)展開與折疊效果,給button
綁定點(diǎn)擊事件:<button onClick={() => toggleNode(treeNode)} ...>
,在TSX的setup
方法中聲明下事件處理函數(shù):
const toggleNode = (node: ITreeNode) => { // 對(duì)展開狀態(tài)取反 node.expanded = !node.expanded }
這里對(duì)實(shí)際為IFlatTreeNode
類型的節(jié)點(diǎn)更新expanded
屬性,因?yàn)?code>expandedTree計(jì)算屬性中參與計(jì)算的flatData
是響應(yīng)式的,而計(jì)算屬性返回?cái)?shù)據(jù)列表中的節(jié)點(diǎn)對(duì)象來(lái)自于傳入的data
,節(jié)點(diǎn)對(duì)象屬性發(fā)生變化自然會(huì)觸發(fā)計(jì)算屬性重新計(jì)算啦看下效果,杠杠滴
存在的問題
聰明的小伙伴會(huì)提出這樣的問題:“小卷,現(xiàn)在咱們的樹節(jié)點(diǎn)是固定的,如果可以動(dòng)態(tài)增刪節(jié)點(diǎn),那么
IFlatTreeNode
節(jié)點(diǎn)的length
屬性豈不是不會(huì)變化了嘛?!”“非常好的問題!”咱們后面要進(jìn)一步使用計(jì)算屬性來(lái)修復(fù)這個(gè)問題,給善于思考的小伙伴一個(gè)大大的??
好了!學(xué)到這里,咱們一顆基本的樹組件就“畫”好了,后續(xù)咱們會(huì)繼續(xù)逐步迭代來(lái)豐富展示效果和交互體驗(yàn)!大家加油!
以上就是Vue3非遞歸渲染Tree組件的初步實(shí)現(xiàn)代碼的詳細(xì)內(nèi)容,更多關(guān)于Vue3 Tree組件實(shí)現(xiàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue3利用customRef實(shí)現(xiàn)防抖
防抖就是對(duì)于頻繁觸發(fā)的事件添加一個(gè)延時(shí)同時(shí)設(shè)定一個(gè)最小觸發(fā)間隔,防抖大家都學(xué)過,但是如何在?Vue3?里中將防抖做到極致呢,下面小編就來(lái)和大家詳細(xì)講講2023-10-10詳解vue表單驗(yàn)證組件 v-verify-plugin
本篇文章主要介紹了詳解vue表單驗(yàn)證組件 v-verify-plugin,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-04-04淺談Vue.js之初始化el以及數(shù)據(jù)的綁定說明
今天小編就為大家分享一篇淺談Vue.js之初始化el以及數(shù)據(jù)的綁定說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2019-11-11Vue實(shí)現(xiàn)根據(jù)hash高亮選項(xiàng)卡
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)根據(jù)hash高亮選項(xiàng)卡,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05vue router 通過路由來(lái)實(shí)現(xiàn)切換頭部標(biāo)題功能
在做單頁(yè)面應(yīng)用程序時(shí),一般頁(yè)面布局頭尾兩塊都是固定在布局頁(yè)面,中間為是路由入口。這篇文章主要介紹了vue-router 通過路由來(lái)實(shí)現(xiàn)切換頭部標(biāo)題 ,需要的朋友可以參考下2019-04-04一款移動(dòng)優(yōu)先的Solid.js路由solid router stack使用詳解
這篇文章主要為大家介紹了一款移動(dòng)優(yōu)先的Solid.js路由solid router stack使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08Vue-input框checkbox強(qiáng)制刷新問題
這篇文章主要介紹了Vue-input框checkbox強(qiáng)制刷新問題,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-04-04Vue集成three.js并加載glb、gltf、FBX、json模型的場(chǎng)景分析
這篇文章主要介紹了Vue集成three.js,并加載glb、gltf、FBX、json模型,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09