element-ui源码分析:剖析el-tree源码,看看实现一个树组件有多么复杂(2)

2,086 阅读11分钟

本文为分析el-tree源码的第二篇文章,在上一篇文章中分析了tree组件所需要的工具方法和树节点(Node类)的实现,本文分析tree-store.js文件中的方法。看完本文你将了解(1)一个树类(TreeStore)应该具有哪些方法 (2)整棵树和树的节点是如何通信的 (3)递归思想在树组件中的使用

tree-store.js文件分析

tree-store.js中定义了和整个树有关的方法和数据,内容也比较多,我们逐一来分析一下。

1 TreeStore类的constructor方法

constructor(options) {
  this.currentNode = null;
  this.currentNodeKey = null;

  for (let option in options) {
    if (options.hasOwnProperty(option)) {
      this[option] = options[option];
    }
  }

  this.nodesMap = {};

  this.root = new Node({
    data: this.data,
    store: this
  });

  if (this.lazy && this.load) {
    const loadFn = this.load;
    loadFn(this.root, (data) => {
      this.root.doCreateChildren(data);
      this._initDefaultCheckedNodes();
    });
  } else {
    this._initDefaultCheckedNodes();
  }
}

constructor构造方法主要完成初始化操作。初始化当前节点和当前节点的key都为空;把options中的对象中的属性赋值给TreeStore;声明一个nodesMap,用于保存当前有哪些node,在registerNode方法调用时会给nodesMap设置新的键值对;root属性引用了Node类的实例,代表树的根节点;如果lazy属性为真,并且load方法存在则调用加载子节点,创建子节点,然后初始化默认勾选;否则lazy为假,则直接初始化默认勾选。

2 TreeStore类的filter方法

filter(value) {
  const filterNodeMethod = this.filterNodeMethod;
  const lazy = this.lazy;
  const traverse = function(node) {
    const childNodes = node.root ? node.root.childNodes : node.childNodes;

    childNodes.forEach((child) => {
      child.visible = filterNodeMethod.call(child, value, child.data, child);

      traverse(child);
    });

    if (!node.visible && childNodes.length) {
      let allHidden = true;
      allHidden = !childNodes.some(child => child.visible);

      if (node.root) {
        node.root.visible = allHidden === false;
      } else {
        node.visible = allHidden === false;
      }
    }
    if (!value) return;

    if (node.visible && !node.isLeaf && !lazy) node.expand();
  };

  traverse(this);
}

filterNodeMethod是过滤节点的方法,文档中有说明,见下图:

traverse方法是一个递归方法,方法中首先获取子节点,然后遍历子节点,对子节点child调用filterNodeMethod方法,三个参数分别是value代表用于过滤的值,child.data代表子节点的数据, child代表子节点本身。我们通过阅读源码知道了filterNodeMethod三个参数的含义,在官方文档中并没有详细说明。

filterNodeMethod方法会返回true或者false,这个值会赋值给child的visible属性。借助递归调用traverse方法处理子节点。然后判断如果一个节点的visible属性为假并且childNodes长度不为0(拥有子节点),则设置allHidden为true,标记子节点是否应该隐藏,这样标记完之后还要根据子节点是否由不隐藏的来更正allHidden的值,用了数组的some方法。之后根据allHidden修正node.root或者node的visible属性。

最后确定了node的visible属性之后,如果visible为true,并且不是叶子节点不是懒加载则调用expand方法展开node节点的子节点。我们看一下在tree-node.vue中是如何使用visible属性的:

3 TreeStore类的setData方法

setData(newVal) {
  const instanceChanged = newVal !== this.root.data;
  if (instanceChanged) {
    this.root.setData(newVal);
    this._initDefaultCheckedNodes();
  } else {
    this.root.updateChildren();
  }
}

setData是一个setter,给data属性赋值时就会调用。首先检查newValue和root的data是否相等(是否是一个实例,比较的是引用地址),如果不相等则调用root的setData方法(node.js中定义的setData方法)然后初始化默认勾选的节点;如果相等则调用updateChildren方法更新。

4 TreeStore类的getNode方法

getNode(data) {
  if (data instanceof Node) return data;
  const key = typeof data !== 'object' ? data : getNodeKey(this.key, data);
  return this.nodesMap[key] || null;
}

根据参数data返回Node类型的节点。如果data是Node类型的实例则返回data。如果不是则接着判断data是不是对象,如果不是对象就是字符串则直接赋值给key;如果data是对象则以key属性和data作为参数调用util.js定义的getNodeKey方法。确定key之后则从nodesMap中查找给定key值的节点。

5 TreeStore类的insertBefore方法

insertBefore(data, refData) {
  const refNode = this.getNode(refData);
  refNode.parent.insertBefore({ data }, refNode);
}

insertBefore接收两个参数data代表要插入新节点的数据,refData是指在谁之前插入的目标位置节点。先调用getNode方法获目标位置节点,然后获取目标位置节点的父节点,在调用insertBefore方法(Node类的insertBefore方法)。

6 TreeStore类的insertAfter方法

insertAfter(data, refData) {
  const refNode = this.getNode(refData);
  refNode.parent.insertAfter({ data }, refNode);
}

insertAfter类似insertBefore,最终调用的是Node类的insertAfter方法。

7 TreeStore类的remove方法

remove(data) {
  const node = this.getNode(data);

  if (node && node.parent) {
    if (node === this.currentNode) {
      this.currentNode = null;
    }
    node.parent.removeChild(node);
  }
}

根据data寻找对应的node节点,然后寻找其父节点,父节点存在则可以删除。如果要删除的节点是当期节点,则将当前节点设置为null。最后由node的父节点调用removeChild移出node节点。

8 TreeStore类的append方法

append(data, parentData) {
  const parentNode = parentData ? this.getNode(parentData) : this.root;

  if (parentNode) {
    parentNode.insertChild({ data });
  }
}

根据参数parentData寻找父节点parentNode,然候parentNode调用insertChild方法。

9 TreeStore类的_initDefaultCheckedNodes方法

_initDefaultCheckedNodes() {
  const defaultCheckedKeys = this.defaultCheckedKeys || [];
  const nodesMap = this.nodesMap;

  defaultCheckedKeys.forEach((checkedKey) => {
    const node = nodesMap[checkedKey];

    if (node) {
      node.setChecked(true, !this.checkStrictly);
    }
  });
}

_initDefaultCheckedNodes用于初始化默认勾选的节点。首先获取默认勾选节点的key值。然候遍历这些key值,根据key值在nodesMap中查找对应的节点。查找到节点后调用setChecked方法。

10 TreeStore类的_initDefaultCheckedNode方法

_initDefaultCheckedNode(node) {
  const defaultCheckedKeys = this.defaultCheckedKeys || [];

  if (defaultCheckedKeys.indexOf(node.key) !== -1) {
    node.setChecked(true, !this.checkStrictly);
  }
}

_initDefaultCheckedNode用于初始化某个节点node的勾选状态。这个方法并没有被用到。

11 TreeStore类的setDefaultCheckedKey方法

setDefaultCheckedKey(newVal) {
  if (newVal !== this.defaultCheckedKeys) {
    this.defaultCheckedKeys = newVal;
    this._initDefaultCheckedNodes();
  }
}

setDefaultCheckedKey用于设置默认勾选,调用了_initDefaultCheckedNodes方法。

12 TreeStore类的registerNode方法

registerNode(node) {
  const key = this.key;
  if (!key || !node || !node.data) return;

  const nodeKey = node.key;
  if (nodeKey !== undefined) this.nodesMap[node.key] = node;
}

registerNode方法用于注册节点,也就是在nodesMap中记录一下。

13 TreeStore类的deregisterNode方法

deregisterNode(node) {
  const key = this.key;
  if (!key || !node || !node.data) return;

  node.childNodes.forEach(child => {
    this.deregisterNode(child);
  });

  delete this.nodesMap[node.key];
}

deregisterNode是一个递归方法,用于注销节点。当一个节点拥有子节点时,则对每一个子节点递归调用。注销节点的实质就是从nodesMap中将之删除。

14 TreeStore类的getCheckedNodes方法

getCheckedNodes(leafOnly = false, includeHalfChecked = false) {
  const checkedNodes = [];
  const traverse = function(node) {
    const childNodes = node.root ? node.root.childNodes : node.childNodes;

    childNodes.forEach((child) => {
      if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) {
        checkedNodes.push(child.data);
      }

      traverse(child);
    });
  };

  traverse(this);

  return checkedNodes;
}

getCheckedNodes用于获取已经勾选的节点。如果当前节点的根节点存在则优选遍历当前节点根节点的childNodes,如果当前节点的根节点不存在则遍历当前节点的childNodes。对于childNodes中的每一个元素做判断,判断条件比较复杂,我们从两个方面来理解:(1)勾选状态(2)考虑叶子节点。什么是勾选状态呢?当child的checked属性为true或者在允许半选的情况下,child的状态是未决定。对于叶子节点的条件则考虑leafOnly为真值和假值的情况,如果leafonly为false或者leafOnly为true并且当前节点正好是叶子节点那么就是满足条件的。

15 TreeStore类的getCheckedKeys方法

getCheckedKeys(leafOnly = false) {
  return this.getCheckedNodes(leafOnly).map((data) => (data || {})[this.key]);
}

getCheckedKeys用于获取勾选节点的key属性。首先调用getCheckedNodes获取已经勾选的节点,然后使用map方法获取节点的key属性。这个key也就是使用el-tree组件时所所指定的node-key。见下图:

上图说明TreeStore所指定的key来源于nodeKey。

上图说明nodeKey是el-tree定义的属性(props)。

16 TreeStore类的getHalfCheckedNodes方法

getHalfCheckedNodes() {
  const nodes = [];
  const traverse = function(node) {
    const childNodes = node.root ? node.root.childNodes : node.childNodes;

    childNodes.forEach((child) => {
      if (child.indeterminate) {
        nodes.push(child.data);
      }

      traverse(child);
    });
  };

  traverse(this);

  return nodes;
}

getHalfCheckedNodes用于获取半选状态的节点。函数内部定义了一个递归遍历的函数traverse。和getCheckedNodes类似,这里也是优先遍历当前节点的父节点的childNodes。如果childNodes中的元素的indeterminate属性为true则放入到结果结合中,最终返回结果。

17 TreeStore类的getHalfCheckedKeys方法

getHalfCheckedKeys() {
  return this.getHalfCheckedNodes().map((data) => (data || {})[this.key]);
}

getCheckedKeys用于获取半选节点的key属性。逻辑和getCheckedKeys方法的逻辑完全一样。

18 TreeStore类的_getAllNodes方法

_getAllNodes() {
  const allNodes = [];
  const nodesMap = this.nodesMap;
  for (let nodeKey in nodesMap) {
    if (nodesMap.hasOwnProperty(nodeKey)) {
      allNodes.push(nodesMap[nodeKey]);
    }
  }

  return allNodes;
}

_getAllNodes用于获取当前树的所有节点。遍历nodesMap中的key,获取值放入结果结合allNodes中。

19 TreeStore类的updateChildren方法

updateChildren(key, data) {
  const node = this.nodesMap[key];
  if (!node) return;
  const childNodes = node.childNodes;
  for (let i = childNodes.length - 1; i >= 0; i--) {
    const child = childNodes[i];
    this.remove(child.data);
  }
  for (let i = 0, j = data.length; i < j; i++) {
    const child = data[i];
    this.append(child, node.data);
  }
}

updateChildren用于更新某个节点的孩子节点。根据key值从nodesMap中获取节点node,node不存在则返回。获取node的childNodes,然后是两个for循环。第一个for循环用于删除原来的节点,第二个for循环用于创建新的节点。这两个for循环中分别调用了上文分析过的remove和append方法。整个方法调用示意图如下:

20 TreeStore类的_setCheckedKeys方法

_setCheckedKeys(key, leafOnly = false, checkedKeys) {
  const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level);
  const cache = Object.create(null);
  const keys = Object.keys(checkedKeys);
  allNodes.forEach(node => node.setChecked(false, false));
  for (let i = 0, j = allNodes.length; i < j; i++) {
    const node = allNodes[i];
    const nodeKey = node.data[key].toString();
    let checked = keys.indexOf(nodeKey) > -1;
    if (!checked) {
      if (node.checked && !cache[nodeKey]) {
        node.setChecked(false, false);
      }
      continue;
    }

    let parent = node.parent;
    while (parent && parent.level > 0) {
      cache[parent.data[key]] = true;
      parent = parent.parent;
    }

    if (node.isLeaf || this.checkStrictly) {
      node.setChecked(true, false);
      continue;
    }
    node.setChecked(true, true);

    if (leafOnly) {
      node.setChecked(false, false);
      const traverse = function(node) {
        const childNodes = node.childNodes;
        childNodes.forEach((child) => {
          if (!child.isLeaf) {
            child.setChecked(false, false);
          }
          traverse(child);
        });
      };
      traverse(node);
    }
  }
}

_setCheckedKeys用于设置节点的勾选,参数key是节点的key属性,参数checkedKeys是传入的key值对象(例如: {1:true, 3: true})。首先调用_getAllNodes获取所有节点,然后调用sort方法按照节点的level值从大到小排序(也就是要先处理子节点),赋值给allNodes。sort方法的一个例子如下:

[1,2,3,4,5].sort((a, b) => b - a)
// [5, 4, 3, 2, 1]

接着创建一个cache对象,如果当前节点是应该勾选的,则cached用于保存当前正在处理节点的祖先节点。Object.keys(checkedKeys)将参数checkedKeys的key值提取出来作为一个数组赋值给keys。

然后遍历allNodes的每一节点,全都初始化为不勾选。接着遍历allNodes中的每一节点,检查节点的key值是否在keys中。

如果当前节点不应该被选中则判断其checked属性是否为真和判断它之前是否没有被缓存在cache中,如果条件成立则说明此时node节点应该设置为不勾选的,之后进入下次循环。如果条件不成立也直接进入下次循环,情况有node.checked为false(节点原来不是勾选的状态),或者因为节点的子节点应该被勾选导致节点被缓存了。

如果当前节点应该被选中则将当前节点的所有祖先节点都缓存在cache中。

以上是判断是否应该被勾选,判断完后应该设置节点被勾选状态。设置的时候检查当前节点是否是叶子节点或者和子节点是否不关联,如果是的话则setChecked的第二个参数deep要传false。回顾一下setChecked的方法签名:

setChecked(value, deep, recursion, passValue)

最后是根据leafOnly对勾选进行修正,代码能执行到这里说明node不是叶子节点,首先先将node的勾选状态设置为false,然后用递归函数traverse递归处理子节点。

总体说来_setCheckedKeys方法还是比较复杂的,下面用一张图从宏观上描述这个函数所做的工作:

21 TreeStore类的setCheckedNodes方法

setCheckedNodes(array, leafOnly = false) {
  const key = this.key;
  const checkedKeys = {};
  array.forEach((item) => {
    checkedKeys[(item || {})[key]] = true;
  });

  this._setCheckedKeys(key, leafOnly, checkedKeys);
}

setCheckedNodes用于设置勾选的节点。参数array是节点数组(Node类型),遍历获取key值,存放于checkedKeys。最后调用刚刚分析完的_setCheckedKeys方法。

22 TreeStore类的setCheckedKeys方法

setCheckedKeys(keys, leafOnly = false) {
  this.defaultCheckedKeys = keys;
  const key = this.key;
  const checkedKeys = {};
  keys.forEach((key) => {
    checkedKeys[key] = true;
  });

  this._setCheckedKeys(key, leafOnly, checkedKeys);
}

setCheckedKeys方法也是用于勾选节点。参数是要勾选的节点的key所组成的数组,遍历数组的每一项作为checkedKeys对象的属性。最后调用_setCheckedKeys方法。顺便看一下文档对这个方法的说明:

23 TreeStore类的setDefaultExpandedKeys方法

setDefaultExpandedKeys(keys) {
  keys = keys || [];
  this.defaultExpandedKeys = keys;

  keys.forEach((key) => {
    const node = this.getNode(key);
    if (node) node.expand(null, this.autoExpandParent);
  });
}

setDefaultExpandedKeys用于设置默认展开的节点,遍历数组的每一项根据key获取node,node再调用expand方法。在el-treee的defaultExpandedKeys属性变化时调用这个方法,如下图所示:

24 TreeStore类的setChecked方法

setChecked(data, checked, deep) {
  const node = this.getNode(data);

  if (node) {
    node.setChecked(!!checked, deep);
  }
}

setChecked方法用于设置某个节点的勾选状态。首先调用getNode方法获取节点,然后调用节点的setChecked方法。

25 TreeStore类的getCurrentNode方法

getCurrentNode() {
  return this.currentNode;
}

getCurrentNode用于返回当前被选中的节点。

26 TreeStore类的setCurrentNode方法

setCurrentNode(currentNode) {
  const prevCurrentNode = this.currentNode;
  if (prevCurrentNode) {
    prevCurrentNode.isCurrent = false;
  }
  this.currentNode = currentNode;
  this.currentNode.isCurrent = true;
}

getCurrentNode用于设置某个节点的当前选中状态。首先获取原来的选中节点prevCurrentNode,判断是否存在,如果存在则将其isCurrent属性设置为false。然后将当前选中节点赋值为参数currentNode,并将其isCurrent属性设置为true。

27 TreeStore类的setUserCurrentNode方法

setUserCurrentNode(node) {
  const key = node[this.key];
  const currNode = this.nodesMap[key];
  this.setCurrentNode(currNode);
}

setUserCurrentNode是根据用户传入的node数据,获取其key值。再根据其key值从nodesMap获取节点。最后调用setCurrentNode方法。

28 TreeStore类的setCurrentNodeKey方法

setCurrentNodeKey(key) {
  if (key === null || key === undefined) {
    this.currentNode && (this.currentNode.isCurrent = false);
    this.currentNode = null;
    return;
  }
  const node = this.getNode(key);
  if (node) {
    this.setCurrentNode(node);
  }
}

setCurrentNodeKey用于根据key设置当前节点。如果参数key为null或者undefined则取消当前节点。如果key有值则根据key获取节点node,将node设置为当前节点。

29 TreeSotre类总结

tree-store.js即TreeSotre类的方法很多。从操作上可分为有过滤节点的,插入节点的,删除节点的,更新节点的;从节点状态方面讲可分为更新节点的选中状态,更新节点的展开状态,设置为当前节点等。我们用一张图来总结一下: