本文为分析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类的方法很多。从操作上可分为有过滤节点的,插入节点的,删除节点的,更新节点的;从节点状态方面讲可分为更新节点的选中状态,更新节点的展开状态,设置为当前节点等。我们用一张图来总结一下: