Tree组件引发的一些思考

836 阅读3分钟

概述

即为无限层级,无限节点。大致应用场景为:文件夹目录,组织架构等带有明显层级关系的业务场景。本文说明了一些自己在实现树组件的一些思考和优化(jsx)。

// 树形数据
const treeData = [
  {
    name: '一级 1',
    id: 1,
    children: [
      {
        name: '二级 1-1',
        id: 2,
        children: [
          {
            name: '三级 1-1-1',
            id: 3,
            children: [
              {
                name: '四级 1-1-1-1',
                id: 4,
                children: [],
                checked: true,
                disabled: true
              },
              {
                name: '四级 1-1-1-2',
                id: 5,
                children: [],
                checked: false
              }
            ]
          }
        ]
      },
      {
        name: '二级 1-2',
        id: 8,
        children: [
          {
            name: '三级 1-2-1',
            id: 9,
            children: [
              {
                name: '四级 1-2-1-1',
                id: 10,
                children: []
              }
            ]
          }
        ]
      }
    ]
  },
  {
    name: '一级 2',
    id: 11,
    children: [
      {
        name: '二级 2-1',
        id: 12,
        children: []
      },
      {
        name: '二级 2-2',
        id: 15,
        children: []
      }
    ]
  }
]

设计

每一个节点都可以认为是拥有同样的功能,即 每一个节点都是在渲染相同的组件。从这一点触发,树组件拆分为两个组件,一个是Tree入口组件,一个是Node叶子组件。

  1. Tree组件负责处理公共数据、方法,以及初始化时组件需要内置的一些数据。并向外暴露标准API
  2. Node组件负责渲染真实的节点,并且递归渲染自身。

优化

  1. 在入口组件初始化时,遍历整个树形数据,为每一个子节点绑定其父节点引用。目的是为了在更新节点选中状态时,获取当前引用,反向遍历更新节点状态
// Tree.vue 初始化数据
initData (arr) {
  console.time('init-Data')
  if (this.showCheckbox) {
    arr.forEach(node => {
      const stack = [node]
      while (stack.length !== 0) {
        const item = stack.pop()
        // 选中节点
        if (item.checked) this.commonData.checkedNode.push(item)
        // 包含子节点
        if (this.hasChild(item)) {
          item[this.childKey].forEach(child => {
            child._parent = item._parent ? [...item._parent, item] : [item]
            stack.push(child)
          })
        }
        // 半选按钮
        if (stack.length === 0) this.resetState(item)
      }
    })
  }
  console.timeEnd('init-Data')
  // console.log(arr)
  this.nodeData = arr
}
resetState (node) {
  /**
   * 思考:treeData,父节点的半选状态需要根据子节点的状态去展现,
   * 有什么思路可以最快确定所有节点自身的半选状态。
   *   1. treeData无限级嵌套
   *   2. treeData依赖变量x的展现,所以一次递归几乎不可能完成。
   *   3. 如何避免循环更新节点状态导致的性能浪费。
   * 思路:
   *   1. 递归当前Object,找出层级最高的一个节点,并且每个节点保存其父节点引用
   *   2. 根据父节点引用进行反向查找,从而确定每个父级点的状态
   *   3. 一级节点父节点即自身
   */
  // const length = node._parent?.length || 0
  const length = node._parent ? node._parent.length : 0
  for (let i = length - 1; i >= 0; i--) {
    let halfelEction = false
    let cur = node._parent[i]
    let checked = cur.checked
    cur[this.childKey].forEach(child => {
      if (child.checked || child._halfelEction) halfelEction = true
      if (!child.checked) checked = false
    })
    this.$set(cur, 'checked', checked)
    this.$set(cur, '_halfelEction', halfelEction)
    !checked
      ? this.commonData.checkedNode = this.commonData.checkedNode.filter(cNode => cNode[this.nodeKey] !== cur[this.nodeKey])
      : this.commonData.checkedNode.push(cur)
  }
}
  1. 子组件状态变更时,根据变更的状态以及节点自身的属性(是否是父级节点)触发不同的事件,做到父节点向下捕获,子节点向上冒泡
// Node.js
checked (val) {
  /**
   * 根据自身的状态不同,向上 && 向下发送事件不同,根据事件类别,减少计算量(考虑子节点含有禁用)
   * 0. 自身是半选 ---- 被动
   *    a: 子级更新,才会导致父级变更为半选的状态
   * 1. 自身是选中 ---- 主动
   *    a: 根据平级所有节点选中状态,判断是否向上发送被动选中事件
   *    b: 向下发送被动选中事件
   * 2. 自身是选中 ---- 被动
   *    a: 重置半选状态
   * 3. 自身是取消 ---- 主动
   *    a: 向上发送半选事件
   * 4. 自身是取消 ---- 被动
   *    a: 重置半选状态
   */
  // !val
  //   ? this.commonData.checkedNode = this.commonData.checkedNode.filter(cNode => cNode[this.nodeKey] !== this.node[this.nodeKey])
  //   : this.commonData.checkedNode.push(this.node)
  // this.node._halfelEction && this.$set(this.node, '_halfelEction', false)
  /**
   * 是否是末级节点
   * 1. 是:
   *     a:直接向上冒泡
   *     b:逐层更新父级节点状态
   * 2. 否:
   *     a:传递捕获找到最末级节点根据此次状态,被动更新末级所有节点状态,
   *     b:逐层更新父级节点状态
   */
  if (this.hasChild(this.node)) {
    this.parentChecked(val)
  } else {
    this.emitEvent('on-child-checked')
  }
  this.emitEvent('on-checked', this.node, this.index)
}

拓展

  1. 使用v-if去渲染子组件,懒加载
  2. 懒加载时组件的状态怎么同步,何时同步,怎么优化同步?

链接

optimization-ui