欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue3非遞歸渲染Tree組件的初步實(shí)現(xiàn)代碼

 更新時(shí)間:2024年05月30日 08:26:04   作者:Java小卷  
這篇文章主要介紹了Vue3非遞歸渲染Tree組件的初步實(shí)現(xiàn),文中通過代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定幫助,需要的朋友可以參考下

本節(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)

這里我們巧用了Arrayreduce方法,在遍歷每個(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ò)展levelpid參數(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)文章

最新評(píng)論