rc-tree

该文章主要对 TreeNodeonChecked 时,如何跟上级还有下级做联动所记录的

先看传入到tree下的 treeData 结构

const treeData = [
  { 
    title: '1', 
    key: '1', 
    children: [
      { 
        title: '1-1', 
        key: '1-1',
        children: [
          {
            title: '1-1-1', 
            key: '1-1-1',
          }
        ] 
      }
    ]
  },
  {
    title: '2', 
    key: '2',
    children: [
      { 
        title: '2-1', 
        key: '2-1',
        children: [
          {
            title: '2-1-1', 
            key: '2-1-1',
          }
        ] 
      }
    ]
  }
]

Tree 使用了Provider 向下级组件传递onNodeChecked 事件

源码中用 class 组件的形式写的, 下面给个例子:

export const TreeContext = React.createContext(null);
const Tree = () => {
  const onNodeChecked = () => {}

  return (
    <TreeContext.Provider
      value={{
        onNodeChecked
      }}
    >
      <NodeList>
    <TreeContext.Provider>
  )
}

消费provider, 类组件例子

const ContextTreeNode: React.FC<TreeNodeProps> = props => (
  <TreeContext.Consumer>
    {context => <InternalTreeNode {...props} context={context} />}
  </TreeContext.Consumer>
);

函数组件,消费context

const TreeNode = () => {
  const context = useContext(TreeContext)

  return (
    <div />
  )
}

在Tree 接收到treeData 这个props 的时候会进行数据转换

在源码中,在 getDerivedStateFromProps 这个生命周期对treeData 进行转换。

class Tree extends Component {
  static getDerivedStateFromProps(props, prevState) {
    const { treeData, fieldNames } = props;
    const newState = {  }

    if (treeData) {
      newState.treeData = treeData;
      const entitiesMap = convertDataToEntities(treeData, { fieldNames });
      newState.keyEntities = {
        ...entitiesMap.keyEntities,
      };
    }
  }

}
convertDataToEntities 将 treeData 的key 当作键名,value 是当前节点,另外添加他的父节点,最后数据结构应该是这样:

以上面treeData 为例子

{
  1: { 
    title: '1', 
    key: '1', 
    parentNode: null,
    children: [
      { 
        title: '1-1', 
        key: '1-1',
        children: [
          {
            title: '1-1-1', 
            key: '1-1-1',
          }
        ] 
      }
    ]
  },
  '1-1':  { 
    title: '1-1', 
    parentNode: '1',
    key: '1-1',
    children: [
      {
        title: '1-1-1', 
        key: '1-1-1',
      }
    ] 
  }
  '1-1-1': { 
    parentNode: '1-1',
    title: '1-1-1', 
    key: '1-1-1',
  }
}
  • 我们尝试自己写一下这个方法
// 建立双链表形式的map数据, 
const convertDataToEntities = (treeData) => {
  const result = {};

  formatedTreeData(treeData, result, 0, 0);
  console.log(result);

  return {
    keyEntities: result
  }
}

const formatedTreeData = (treeData, hash, parentKey, level) => {
  const parentNode = hash[parentKey] || null;

  treeData.forEach((_item) => {
    hash[_item.key] = {
      ..._item,
      parentNode: parentNode,
      level: level,
    }
    if (_item.children) {
      formatedTreeData(_item.children, hash, _item.key, level + 1)
    }
  })
}

// const { keyEntities } = convertDataToEntities(treeData)

接下来实现 onNodeChecked, 选中当前节点时他的下级节点,以及上级节点联动关系

  • 先根据 keyEntities 获取当前树的最大深度, 因为我们当前keyEntities 已经保存有树的深度了,所以只要一次遍历就可以获取到最大的深度
    在遍历过程中同时对每一层节点进行保存, 即第一层有哪些节点,第二层有哪些节点,我们命名为levelMap

  • 然后维护chekedKeys 这个已选中的节点

  • 从最开始那层 从上而下,遍历每一层节点, 如果当前checkedKeys 包含 当前levelMap[level][item] 那么他的children 应该也需要添加到checkedKeys 里面

  • 从最后那层 由下而上,遍历每层节点,如果当前checkedKeys 包含当前节点的所有children,那么他的父级节点需要被添加到checkedKeys 里面

const onNodeChecked = (e, checkedNode, isChecked) => {
  const { levelMap, maxLevel } = getLevelEntities(keyEntities);

  if (isChecked) {
    // const keys = [...checkedKeys, checkedNode.key];
    const keys = ['1-2']
    const { halfChecked, checkedKeys } = fillConductCheck(keys, levelMap, maxLevel);
    console.log(checkedKeys); // 会发现 [1-1-1, 1-1, 1]
  }

}

const getLevelEntities = (keyEntities) => {
  let maxLevel = 0

  const levelMap = {}

  Object.keys(keyEntities).forEach((_item) => {
    const current = keyEntities[_item];
    const { level } = current;
    maxLevel = Math.max(maxLevel, level);

    if (typeof levelMap[level] === 'undefined') {
      levelMap[level] = [];
    }
    levelMap[level].push(current);
  })

  return { levelMap, maxLevel }
}

getLevelEntities结果

const fillConductCheck = (keys, levelEntities, maxLevel) => {
  const checkedKeys = new Set(keys);
  const halfCheckedKeys = new Set();

  // 从上而下 勾选
  for (let level = 0; level <= maxLevel; level += 1) {
    const entities = levelEntities[level] || new Set();
    entities.forEach(entity => {
      const { key, children = [] } = entity;

      if (checkedKeys.has(key)) {
        children
          .forEach(childEntity => {
            checkedKeys.add(childEntity.key);
          });
      }
    });
  }

  const visitedKeys = new Set();
  for (let level = maxLevel; level >= 0; level -= 1) {
    const entities = levelEntities[level] || new Set();
    entities.forEach(entity => {
      const { parentNode, node } = entity;

      // Skip if no need to check
      if (!entity.parentNode || visitedKeys.has(entity.parentNode.key)) {
        return;
      }

      let allChecked = true;
      let partialChecked = false;

      (parentNode.children || [])
        .forEach(({ key }) => {
          const checked = checkedKeys.has(key);
          if (allChecked && !checked) {
            allChecked = false;
          }
          if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
            partialChecked = true;
          }
        });

      if (allChecked) {
        checkedKeys.add(parentNode.key);
      }
      if (partialChecked) {
        halfCheckedKeys.add(parentNode.key);
      }

      visitedKeys.add(parentNode.key);
    });
  }

  return {
    checkedKeys: Array.from(checkedKeys),
    halfCheckedKeys: halfCheckedKeys,
  };
}