antd Cascader全量数据性能优化,支持回显

1,480 阅读4分钟
  • 问题:Cascader因为要回显所以要渲染全量数据,结果导致组件渲染全量数据特别卡
  • 方案:把全量数据中暂时不需要的数据去掉,需要的时候再加上 具体做法分四步
  1. 根据回显的值,从树中把包含值的节点及其祖先节点全拿出来
  2. 把第一级的节点全拿出来,去掉二级及以下的全部节点
  3. 求1和2的并集 作为Cascader的数据源
  4. 点击某个菜单时,再获取这个菜单的子菜单,并把后代菜单全去掉,再与所点菜单的已有子菜单求并集 封装的组件主要包括两个文件
  5. CascaderTree.js 显示级联菜单的组件
  6. treeUtil.js 处理数据的工具

CascaderTree.js

import { Cascader, Tooltip, message } from 'antd'
import { connect } from 'react-redux'
import { cloneDeep } from 'lodash'
import styles from './style/index.module.less'
import {
  findValuedTree,
  unionSet,
  removeSubTree,
  findNodesWithoutSubTree,
  delEmptyTreeNode,
  initTree,
} from '@/utils/treeUtil'

/**
 * 处理默认数据
 * @param {*} value
 * @param {*} multiple 是否多选
 * @returns 根据multiple返回一维或二维数组 例如:["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
 */
const formatDefaultValue = (value, multiple) => {
  let result
  const defaultValue = cloneDeep(value)
  if (multiple) {
    if (defaultValue) {
      if (Array.isArray(defaultValue)) {
        if (Array.isArray(defaultValue[0])) {
          // 二维数组
          result = defaultValue
        } else {
          // 一维数组
          if (defaultValue.length) {
            result = defaultValue?.map(item => item?.split?.('-'))
          } else {
            //及联多选只支持一维空数组
            result = defaultValue
          }
        }
      } else {
        result = [defaultValue?.split?.('-')]
      }
    } else {
      result = undefined
    }
  } else {
    if (defaultValue) {
      if (Array.isArray(defaultValue)) {
        if (Array.isArray(defaultValue[0])) {
          ;[result] = defaultValue
        } else {
          result = defaultValue
        }
      } else {
        result = defaultValue?.split?.('-')
      }
    } else {
      result = undefined
    }
  }
  return result
}
/**
 * 处理要提交的数据
 * @param {*} value
 * @param {*} multiple 是否多选
 */
const formatSubmitValue = (value, multiple) => {
  if (multiple) {
    return value?.map?.(item => item?.join?.('-'))
  } else {
    return value?.join?.('-')
  }
}
const CascaderTree = props => {
  const { getInterfaceTypeData, dataItem } = props
  const renderFields = dataItem?.props?.response
  const children = renderFields?.children ?? 'children'
  const { multiple, requireFull = true } = dataItem?.props
  const defalutValues = getJSONParseData(dataItem?.defalutValues)
  const cityDefaultValue = formatDefaultValue(defalutValues, multiple)
  /**
   * 获取过数据
   */
  const [manualChange, setManualChange] = useState(false)
  const [treeData, setTreeData] = useState([])

  /**
   * 映射字段对应的key
   */
  const fieldNames = {
    label: renderFields?.label ?? 'label',
    value: renderFields?.value ?? 'value',
    children: children,
  }
  const [options, setOptions] = useState([])
  const [selectValue, setSelectValue] = useState(cityDefaultValue)
  /**
   * 获取数据
   */
  useEffect(() => {
    if (!manualChange) {
      const { config } = dataItem?.props?.request
      const { structure } = dataItem?.props?.response
      getInterfaceTypeData(config).then(data => {
        setManualChange(true)
        // 接口返回的树数据(多根树)
        let tree = data
        initTree(tree, children)
        // 把树的空子树移除
        const allTree = delEmptyTreeNode(tree, children)
        setTreeData(allTree)
        /**
         * 包含选中节点的树 例如:
         * [
         *   {
         *    "id":"8","name":"北京市",
         *    "children":[
         *       {"id":"10000","name":"北京市",
         *        "children":[
         *          {"id":"35001","name":"西城区"},
         *          {"id":"35002","name":"朝阳区"}
         *         ]
         *       }
         *     ]
         *   }
         * ]
         */
        const valueTree = findValuedTree(allTree, cityDefaultValue, renderFields?.label)
        // 只包含第一级节点的树
        const nodes = removeSubTree(allTree)
        // 求只包含第一级节点的树与包含选中节点的树 的并集
        const tree = unionSet(valueTree, nodes)

        setOptions(tree)
      })
    }
  }, [
    getInterfaceTypeData,
    dataItem,
    children,
    manualChange,
    cityDefaultValue,
    renderFields?.label,
  ])

  const loadData = selectedOptions => {
    const targetOption = selectedOptions[selectedOptions.length - 1]
    const ids = selectedOptions.map(item => item[renderFields?.id])
    targetOption.loading = true
    // 只包含第一级子节点的树
    const nodes = findNodesWithoutSubTree(treeData, ids, renderFields?.id, children)
    // 求只包含第一级子节点的树与包含选中节点的树 的并集
    targetOption[children] = unionSet(targetOption[children], nodes)
    setOptions([...options])
  }

  //提交数据接口
  const putCustomDetailItemFn = (item, value) => {
    const flatValue = formatSubmitValue(value, multiple)
    let dealValue = flatValue
    if (multiple) {
      dealValue = flatValue?.length ? JSON.stringify(flatValue) : ''
    }
    const params = {
      id: props.customerInfo?.id,
      [item.name]: dealValue,
    }
    props.putCustomDetailItem(params).then(res => {
      if (res?.status === 1) {
        message.success('修改成功')
      } else {
        message.error('修改失败')
      }
    })
  }
  const onChange = (value, selectedOptions) => {
    if (!value?.length && selectValue?.length) {
      message.warn('不可清空已设置信息!')
    } else {
      setSelectValue(value)
      putCustomDetailItemFn(dataItem, value)
    }
  }

  const titleLength = dataItem.title.split('')
  const TooltipInfo =
    titleLength.length > 6 ? (
      <span style={{ color: '#777', fontSize: '14px' }}>{dataItem.title}</span>
    ) : null
  return (
    <div className={styles.intptName}>
      <Tooltip placement="bottom" title={TooltipInfo} color={'#eee'}>
        <span className={styles.lableName}>{dataItem.title}:</span>
      </Tooltip>
      <Cascader
        allowClear={false}
        options={options}
        onChange={onChange}
        className={styles.inpt}
        placeholder={!dataItem.readonly ? `请选择${dataItem.title}` : `暂无`}
        // showSearch={{ filter }}
        fieldNames={fieldNames}
        value={selectValue}
        getPopupContainer={triggerNode => triggerNode.parentElement}
        changeOnSelect={!requireFull}
        multiple={multiple}
        loadData={loadData}
      />
    </div>
  )
}

export default connect(
  ({ customer }) => ({ ...customer }),
  ({ customer }) => ({ ...customer }),
)(CascaderTree)

treeUtil.js

/**
 *  广度优先查找树
 * @param {*} treeArr 多根树
 * @param {*} subKey 子树的key
 * @param {*} func 回调函数,参数是 树节点 返回是否找到
 * @returns 返回找到的节点
 */
export const wideSearchTree = (treeArr, subKey = 'children', func) => {
  let node,
    queue = treeArr?.slice() ?? []
  while ((node = queue.shift())) {
    if (func(node)) {
      return node
    }
    let { [subKey]: children } = node
    children?.length && queue.push(...children)
  }
  return null
}
/**
 *  广度优先遍历树
 * @param {*} tree 多根树
 * @param {*} subKey 子树的key
 * @param {*} func 回调函数,参数是 树节点
 * @returns
 */
export function bfsTreeEach(tree, subKey = 'children', func) {
  let node,
    nodes = tree?.slice() ?? []
  while ((node = nodes.shift())) {
    func(node)
    let { [subKey]: children } = node
    if (children?.length) {
      nodes.push(...children)
    }
  }
}
/**
 * 添加或修改节点的属性
 * @param {*} node
 * @param {*} key
 * @param {*} value
 * @param {*} predicate
 */
function updateNode(node, key, value, predicate) {
  if (predicate(node)) {
    node[key] = value
  }
}
/**
 * 是否是叶子节点
 * @param {*} node
 * @param {*} subKey
 * @returns
 */
function isLeaf(node, subKey = 'children') {
  return !node?.[subKey]?.length
}
/**
 * 初始化树
 * @param {*} tree
 * @param {*} subKey
 */
export const initTree = (tree, subKey = 'children') => {
  bfsTreeEach(tree, subKey, node => {
    // 标记节点是否是叶子节点
    updateNode(node, 'isLeaf', false, node => !isLeaf(node, subKey))
  })
}

/**
 * @param tree 要过滤的树(多根)
 * @param predicate 过滤条件,返回 `true` (node, parent, deep) => boolean
 * @returns 过滤后的树节点集
 */
export const filterTree = (tree, subKey = 'children', predicate) => {
  let copyTree = cloneDeep(tree)
  return filter(copyTree) ?? []
  // deep 层深
  function filter(copyTree, parent, deep = 0) {
    if (!copyTree?.length) {
      return copyTree
    }

    return copyTree.filter(node => {
      if (!predicate(node, parent, deep)) {
        return false
      }
      // 递归调用不需要再传入 predicate
      node[subKey] = filter(node[subKey], node, deep + 1)
      return true
    })
  }
}
const isTwoDimension = arr => Array.isArray(arr?.[0])
/**
 * 判断节点是否在值数组内
 * @param {*} node
 * @param {*} deep
 * @param {*} values 值组成的一维或二维数组 ["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
 */
const nodeInValues = (node, deep, key = 'id', values = []) => {
  const value = node[key]
  return isTwoDimension(values) ? values.find(item => item[deep] === value) : values[deep] === value
}
/**
 * 判断节点是否需要留在树内(结果是1和2的并集:1、节点值在values内且是最后一级 2、节点值不在values内但父节点值在values内)
 * @param {*} node
 * @param {*} parent 父节点
 * @param {*} deep
 * @param {*} values 值组成的一维或二维数组 ["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
 * @returns true false
 */
const nodeIsRetainInTree = (node, parent, deep, key = 'id', values = []) => {
  const value = node[key]
  const pvalue = parent?.[key] ?? '$&123&$'
  let valuePath, pValuePath
  if (isTwoDimension(values)) {
    valuePath = values.find(item => item[deep] === value)
    pValuePath = values.find(item => item[deep - 1] === pvalue)
  } else {
    valuePath = values[deep] === value ? values : null
    pValuePath = values[deep - 1] === pvalue ? values : null
  }
  // 节点在值数组内 返回是否是最后一级
  if (valuePath) {
    return deep === valuePath.length
  } else if (pValuePath) {
    return true
  }
  // 节点不在值数组内,也算作不是最后一级
  return false
}

/**
 * 根据给定的值获取树,树中只保留给定值的节点,去掉其他节点
 * @param {*} treeArr
 * @param {*} values 值组成的一维或二维数组 ["大专专业","农林牧渔","农业类","种子生产与经营"] 或 [["北京市"],["天津市","天津市","和平区"]]
 * @param {*} key 字段名
 * @returns
 */
export const findValuedTree = (treeArr, values, key = 'id', subKey = 'children') => {
  return filterTree(
    treeArr,
    subKey,
    (node, parent, deep) =>
      nodeIsRetainInTree(node, parent, deep, key, values) || nodeInValues(node, deep, key, values),
  )
}

/**
 * 差集 a - b
 * @param {*} a
 * @param {*} b
 * @param {*} key 字段名
 * @returns
 */
export const differenceSet = (a, b, key = 'id') => {
  return a.filter(item => !b.find(e => e[key] === item[key]))
}
/**
 * 并集 a + b
 * @param {*} a
 * @param {*} b
 * @param {*} key
 * @returns
 */
export function unionSet(a = [], b = [], key = 'id') {
  const dset = differenceSet(b, a, key)
  return [...a, ...dset]
}
/**
 * 移除子树
 * @param {*} treeArr
 * @param {*} subKey 子树名
 * @returns
 */
export const removeSubTree = (treeArr, subKey = 'children') => {
  return treeArr?.map?.(item => ({ ...item, [subKey]: [] })) ?? []
}
/**
 * 根据父节点值获取子节点列表,不包含子节点一下的子树
 * @param {*} treeArr 要查找的树
 * @param {*} valuePath 值的路径 ['北京市', '北京市', '海淀区']
 * @param {*} key 搜索的字段
 * @param {*} subKey 子树的key
 * @returns
 */
export const findNodesWithoutSubTree = (treeArr, valuePath, key = 'id', subKey = 'children') => {
  let tempNode,
    tree = treeArr
  valuePath.forEach(value => {
    tempNode = wideSearchTree(tree, subKey, node => {
      if (node[key] === value) {
        return true
      }
      return false
    })
    if (tempNode) {
      tree = tempNode[subKey]
    } else {
      tree = []
    }
  })
  const children = removeSubTree(tempNode[subKey], subKey)
  return children
}
/**
 * 移除空子树
 * @param {*} tree
 * @param {*} key
 * @returns
 */
export const delEmptyTreeNode = (tree, key = 'children') => {
  if (!key && String(key) !== '0') {
    return tree
  }
  return tree?.map?.(node => {
    if (node?.[key]?.length) {
      delEmptyTreeNode(node?.[key], key)
    } else {
      Reflect.deleteProperty(node, key)
    }
    return node
  })
}