IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> element-ui源码分析:剖析el-tree源码,看看实现一个树组件有多么复杂(1) -> 正文阅读

[JavaScript知识库]element-ui源码分析:剖析el-tree源码,看看实现一个树组件有多么复杂(1)

elment-ui中tree木块相关文件如下图:

下图梳理一下各个文件之间的引用关系(箭头的方向表示使用)

1 uti.js

1.1 markNodeData 标记节点

export const NODE_KEY = '$treeNodeId';

export const markNodeData = function(node, data) {if (!data || data[NODE_KEY]) return;Object.defineProperty(data, NODE_KEY, {value: node.id,enumerable: false,configurable: false,writable: false});
}; 

定义常量NODE_KEY; 判断节点是否存在NODE_KEY 属性,存在则返回;否则使用Object.defineProperty定义节点的NODE_KEY 属性,属性是只读的。

1.2 getNodeKey 获取节点NODE_KEY属性

export const getNodeKey = function(key, data) {if (!key) return data[NODE_KEY];return data[key];
}; 

如果没有指定key, 则返回NODE_KEY 。

1.3 findNearestComponent 寻找最近的组件

export const findNearestComponent = (element, componentName) => {let target = element;while (target && target.tagName !== 'BODY') {if (target.__vue__ && target.__vue__.$options.name === componentName) {return target.__vue__;}target = target.parentNode;}return null;
}; 

如果target存在__vue__属性(代表对应的vue组件),并且组件名等于componentName则返回这个vue组件,否则向上判断target的父节点。

__vue__属性是在vue源码中定义的属性:

if (prevEl) {prevEl.__vue__ = null
}
if (vm.$el) {vm.$el.__vue__ = vm
} 

详见vue源码 (路径为: src\core\instance\lifecycle.js)

2.node.js分析

node.js中定义了Node类和获取子节点状态、重新初始化节点选中状态以及获取节点属性的方法,我们逐一了解一下:

2.1 getChildState 获取子节点状态

export const getChildState = node => {let all = true;let none = true;let allWithoutDisable = true;for (let i = 0, j = node.length; i < j; i++) {const n = node[i];if (n.checked !== true || n.indeterminate) {all = false;if (!n.disabled) {allWithoutDisable = false;}}if (n.checked !== false || n.indeterminate) {none = false;}}return { all, none, allWithoutDisable, half: !all && !none };
}; 

代码中的单词indeterminate表示“不确定的”,getChildState判断一个节点的子节点状态, all代表全部都勾选;none代表一个都没勾选;allWithoutDisable代表所有的都禁用;half表示有勾选的有没勾选的。

all, none, allWithoutDisable初始值全是true; 循环遍历子节点列表,如果有一个节点的checked不为true或者为indeterminate则all为false;如果某个节点没有禁用则allWithoutDisable为false; 如果某个节点勾选了或者indeterminate则none为假。

2.2 reInitChecked 根据子节点状态重置节点勾选状态

const reInitChecked = function(node) {if (node.childNodes.length === 0) return;const {all, none, half} = getChildState(node.childNodes);if (all) {node.checked = true;node.indeterminate = false;} else if (half) {node.checked = false;node.indeterminate = true;} else if (none) {node.checked = false;node.indeterminate = false;}const parent = node.parent;if (!parent || parent.level === 0) return;if (!node.store.checkStrictly) {reInitChecked(parent);}
}; 

如果子节点列表为空则直接返回;否则获取子节点状态进行判断:如果子节点全选了,那么设置当前节点为选中状态(checked为true, indeterminate为false);如果子节点为half即有选的有没选的则设置当前节点为不选中状态(checked为false, indeterminate为true);如果子节点全都没选中,则设置当前节点为不选中状态(checked为false, indeterminate为false)。

设置完当前节点后则要处理当前节点的父节点,如果父节点不存在或者层级为第0层(根节点)则返回,否则判断父子节点之间是否严格不关联,如果不是的话,则递归检查父节点。关于checkStrictly,文档中有如下说明:

下图reInitChecked方法的调用关系(箭头方向表示被调用):

2.3 getPropertyFromData从数据中获取属性

const getPropertyFromData = function(node, prop) {const props = node.store.props;const data = node.data || {};const config = props[prop];if (typeof config === 'function') {return config(data, node);} else if (typeof config === 'string') {return data[config];} else if (typeof config === 'undefined') {const dataProp = data[prop];return dataProp === undefined ? '' : dataProp;}
}; 

首先通过node.store.props获取props对象,通过node.data获取data,然后通过props[prop]获取prop对应的定义config;接着判断config的类型,如果是函数则返回调用结果,如果是字符串则返回对应的值,如果未定义则直接从data中获取prop对应的值,如果值为undefined则返回空串,否则返回值。

获取属性的值,为什么要这么做呢?感觉很折腾,后面分析tree-store.js就会知道啦~

2.4 Node类的constructor构造方法

constructor(options) {// 属性初始化this.id = nodeIdSeed++;this.text = null;this.checked = false;this.indeterminate = false;this.data = null;this.expanded = false;this.parent = null;this.visible = true;this.isCurrent = false;
		// 遍历options进行属性初始化for (let name in options) {if (options.hasOwnProperty(name)) {this[name] = options[name];}}// internalthis.level = 0;this.loaded = false;this.childNodes = [];this.loading = false;// 如果存在parent,则节点层级是在parent上加1if (this.parent) {this.level = this.parent.level + 1;}// 检查是否存在store属性const store = this.store;if (!store) {throw new Error('[Node]store is required!');}// 调用registerNode根据key,注册当前节点(让当前节点和整个树关联)store.registerNode(this);
		// 根据isLeaf给isLeafByUser属性赋值const props = store.props;if (props && typeof props.isLeaf !== 'undefined') {const isLeaf = getPropertyFromData(this, 'isLeaf');if (typeof isLeaf === 'boolean') {this.isLeafByUser = isLeaf;}}// 如果不是懒加载则直接设置数据if (store.lazy !== true && this.data) {this.setData(this.data);// 如果默认展开全部则expandedif (store.defaultExpandAll) {this.expanded = true;}} else if (this.level > 0 && store.lazy && store.defaultExpandAll) {// 如果是懒加载 并且默认展开全部则调用expand方法this.expand();}// 标记节点数据 根据id设置 $treeNodeIdif (!Array.isArray(this.data)) {markNodeData(this, this.data);}if (!this.data) return;// 获取默认展开的keysconst defaultExpandedKeys = store.defaultExpandedKeys;const key = store.key;// 如果整个tree的默认展开key包含本节点(node)的key,则展开if (key && defaultExpandedKeys && defaultExpandedKeys.indexOf(this.key) !== -1) {this.expand(null, store.autoExpandParent);}// 本节点是不是整个树的当前选中的节点if (key && store.currentNodeKey !== undefined && this.key === store.currentNodeKey) {store.currentNode = this;store.currentNode.isCurrent = true;}// 如果懒加载lazy,则初始化默认勾选的节点if (store.lazy) {store._initDefaultCheckedNode(this);}// 更新叶子节点状态this.updateLeafState();} 

constructor主要是做初始化工作。首先包括属性的初始化,以及参数options属性初始化;然后是根据父节点层次信息初始化本节点的层次信息;接着是在store上注册本节点,让当前这个节点和整棵树关联起来;最后是叶节点信息的判断,默认展开和懒加载的处理。

2.5 Node类的setData方法

setData(data) {if (!Array.isArray(data)) {markNodeData(this, data);}this.data = data;this.childNodes = [];let children;if (this.level === 0 && this.data instanceof Array) {children = this.data;} else {children = getPropertyFromData(this, 'children') || [];}for (let i = 0, j = children.length; i < j; i++) {this.insertChild({ data: children[i] });}
} 

首先对节点数据调用markNodeData方法标记数据;给data属性和childNodes属性赋值;如果当前节点的层次是0并且data是数组则给children赋值为data,否则调用getPropertyFromData获取children;最后遍历children,调用insertChild方法插入子节点。为什么层次是0就直接给children赋值,不是的话需要调用getPropertyFromData方法?原因之后分析。

2.6 Node类的getter属性

//获取label
get label() {return getPropertyFromData(this, 'label');
}
//获取key
get key() {const nodeKey = this.store.key;if (this.data) return this.data[nodeKey];return null;
}
// 获取disabled
get disabled() {return getPropertyFromData(this, 'disabled');
}
// 获取下一个兄弟节点
get nextSibling() {const parent = this.parent;if (parent) {const index = parent.childNodes.indexOf(this);if (index > -1) {return parent.childNodes[index + 1];}}return null;
}
// 获取上一个兄弟节点
get previousSibling() {const parent = this.parent;if (parent) {const index = parent.childNodes.indexOf(this);if (index > -1) {return index > 0 ? parent.childNodes[index - 1] : null;}}return null;
} 

获取下一个兄弟节点的思路是获取当前节点的父节点,获取当前节点在父节点的childNodes中的索引,然后获取下一个索引对应的数据;获取上一个兄弟节点的思路类似。

2.7 Node类的contains方法

contains(target, deep = true) {const walk = function(parent) {const children = parent.childNodes || [];let result = false;for (let i = 0, j = children.length; i < j; i++) {const child = children[i];if (child === target || (deep && walk(child))) {result = true;break;}}return result;};return walk(this);
} 

这里定义了一个递归函数walk,在walk函数里面首先获取当前节点的childNodes,然后遍历判断子节点是否匹配目标target。如果deep为true,代表深度检查,则需要递归检查当前节点的子节点是否包含target。

2.8 Node类的remove方法

remove() {const parent = this.parent;if (parent) {parent.removeChild(this);}
} 

获取当前节点的父节点,如果父节点存在则调用removeChild方法移除当前节点。

2.9 Node类的removeChild方法

removeChild(child) {const children = this.getChildren() || [];const dataIndex = children.indexOf(child.data);if (dataIndex > -1) {children.splice(dataIndex, 1);}const index = this.childNodes.indexOf(child);if (index > -1) {this.store && this.store.deregisterNode(child);child.parent = null;this.childNodes.splice(index, 1);}this.updateLeafState();
} 

首先是从children中移除,获取child的data属性的索引,使用数组的splice方法删除;然后是从childNodes中删除,删除的时候也是从树里面删除所以调用了store的deregisterNode方法,清除整个树对这个节点的引用;最后 是调用updateLeafState方法更新叶节点状态,因为删除一个节点的子节点,可能使这个节点成为叶节点。

2.10 Node类的 insertChild方法

// 参数batch表示批量,布尔值
insertChild(child, index, batch) {if (!child) throw new Error('insertChild error: child is required.');// 如果child不是Node实例,则构造为node实例if (!(child instanceof Node)) {// 不是批量if (!batch) {// 获取childrenconst children = this.getChildren(true) || [];// 如果不在children里面if (children.indexOf(child.data) === -1) {// index为undefined或者小于0的数则放到childern的末尾if (typeof index === 'undefined' || index < 0) {children.push(child.data);} else {// 否则插入到index后面children.splice(index, 0, child.data);}}}// 给child增加parent属性与父节点建立联系,增加store属性和整个树结构建立联系objectAssign(child, {parent: this,store: this.store});// 将child构造为Node实例child = new Node(child);}// 层级加1child.level = this.level + 1;// 更新当前节点的childNodesif (typeof index === 'undefined' || index < 0) {this.childNodes.push(child);} else {this.childNodes.splice(index, 0, child);}// 更新叶子节点状态this.updateLeafState();
} 

(1)insertChild方法首先判断child是否为Node实例,如果不是则构造为Node实例。(2)在构造为Node实例之前先判断是否为批量插入,如果不是则将child的data存放于当前节点的children属性;然后给hild增加parent属性与父节点建立联系,增加store属性和整个树结构建立联系;最后构造为Node实例。(3)child的level属性加1,更新当前节点的childNodes属性,将child放入其中。(4)更新叶子节点状态

2.11Node类的insertBefore和insertAfter

insertBefore(child, ref) {let index;if (ref) {index = this.childNodes.indexOf(ref);}this.insertChild(child, index);
}

insertAfter(child, ref) {let index;if (ref) {index = this.childNodes.indexOf(ref);if (index !== -1) index += 1;}this.insertChild(child, index);
} 

insertBefore在某一节点之前插入child,insertAfter在某一节点之后插入child,两个函数都调用了insertChild函数。共同的逻辑是寻找某一节点ref在childNodes中的位置。

2.12 Node类的removeChildByData

removeChildByData(data) {let targetNode = null;for (let i = 0; i < this.childNodes.length; i++) {if (this.childNodes[i].data === data) {targetNode = this.childNodes[i];break;}}if (targetNode) {this.removeChild(targetNode);}
} 

根据数据移出子节点,在childNodes中寻找数据data属性和传入参数相等的目标节点,然后调用removeChild移除节点。

至此,我们可以总结,在Node类中调用removeChild函数的有两个,一个是removeChildByData,一个是remove,如下图所示:

2.13 Node类的expand方法

expand(callback, expandParent) {// done函数const done = () => {// 如果父节点也展开则不断向上获取父节点,设置expanded属性为trueif (expandParent) {// 获取当前节点的父节点let parent = this.parent;while (parent.level > 0) {// 标记父节点parent.expanded = true;parent = parent.parent;}}this.expanded = true;// 如果有回调则执行回调if (callback) callback();};// 展开的同时判断是否需要加载数据if (this.shouldLoadData()) {this.loadData((data) => {if (data instanceof Array) {// 如果节点为选中状态,展开之后也要处理选中if (this.checked) {this.setChecked(true, true);} else if (!this.store.checkStrictly) {reInitChecked(this);}done();}});} else {done();}
} 

expand方法用于展开节点。在展开节点的同时要判断是否需要加载数据。如果需要加载数据,这在获取完数据之后根据节点的选中状态更新子节点选中状态。当节点展开后,expended属性要更新为true。在done函数中对参数expandParent进行了判断,如果需要更新父节点展开状态则不断向上获取父节点,更新父节点展开状态。

2.14 Node类的doCreateChildren方法

 doCreateChildren(array, defaultProps = {}) { array.forEach((item) => { this.insertChild(objectAssign({ data: item }, defaultProps), undefined, true); });
 } 

doCreateChildren用于创建子节点,参数为子节点的数据数组,循环遍历此数组,并调用insertChild方法。

2.15 Node类的collapse方法

collapse() {this.expanded = false;
} 

collapse即折叠,将节点的展开属性(expanded)设置为false。

2.16 Node类的shouldLoadData方法

shouldLoadData() {return this.store.lazy === true && this.store.load && !this.loaded;
} 

shouldLoadData用于判断是否应该加载数据,如果store的懒加载为true, store存在load方法,并且当前节点loaded属性为假,则说明可以加载数据。

2.17 Node类的updateLeafState方法

updateLeafState() {// 根据isLeafByUser设置if (this.store.lazy === true && this.loaded !== true && typeof this.isLeafByUser !== 'undefined') {this.isLeaf = this.isLeafByUser;return;}// 根据childNodes判断const childNodes = this.childNodes;if (!this.store.lazy || (this.store.lazy === true && this.loaded === true)) {this.isLeaf = !childNodes || childNodes.length === 0;return;}this.isLeaf = false;
} 

updateLeafState用于更新节点是否为叶子节点。优先使用isLeafByUser判断,然后根据childNodes判断,如果childNodes为假值或者长度为0则isLeaf属性为真,表示当前节点为叶子节点。

2.18 Node类的setChecked方法

setChecked(value, deep, recursion, passValue) {this.indeterminate = value === 'half';// 根据value设置当前节点的选中状态this.checked = value === true;
	// 如果父子不关联则返回if (this.store.checkStrictly) return;
	// 不加载数据和检查后代if (!(this.shouldLoadData() && !this.store.checkDescendants)) {// 获取子节点的选中状态let { all, allWithoutDisable } = getChildState(this.childNodes);
		// 如果当前节点不是叶子节点并且子节点不是全选,则当前节点也不是全选if (!this.isLeaf && (!all && allWithoutDisable)) {this.checked = false;value = false;}// 处理后代节点const handleDescendants = () => {// 如果是深度监听,则检查子节点if (deep) {const childNodes = this.childNodes;for (let i = 0, j = childNodes.length; i < j; i++) {const child = childNodes[i];passValue = passValue || value !== false;const isCheck = child.disabled ? child.checked : passValue;// 设置子节点选中状态child.setChecked(isCheck, deep, true, passValue);}// 根据子节点更新当前节点const { half, all } = getChildState(childNodes);if (!all) {this.checked = all;this.indeterminate = half;}}};
		// 是否要加载数据if (this.shouldLoadData()) {// Only work on lazy load data.// 加载数据完成之后this.loadData(() => {// 子节点处理完后重新更新当前节点的选中状态handleDescendants();reInitChecked(this);}, {checked: value !== false});return;} else {handleDescendants();}}const parent = this.parent;if (!parent || parent.level === 0) return;// 是否要改变父节点的勾选状态if (!recursion) {reInitChecked(parent);}
} 

setChecked用于设置节点的选中状态。考虑到节点可能是一种“上有老,下有小”的局面,所以设置当前节点选中状态时还要考虑当前节点的子节点和当前节点的父节点。参数deep和passValue和子节点相关;参数recursion和父节点相关。

设置子节点选中状态时,要考虑父子是否互不关联,如果父子互不关联则直接返回,不再处理子节点。

如果父子关联,则需要处理子节点的选中状态。那么要分为子节点是否存在,如果存在则处理;不存在需要加载出来。handleDescendants方法是专门处理子节点选中状态的函数。首先根据deep参数判断是否需要处理,如果需要处理则循环遍历子节点,设置选中状态。设置完子节点选中状态后还要更新当前节点。

根据recursion判断是否需要更新父节点的选中状态,如果需要则调用reInitChecked方法。注意更新父节点选中状态是放在最后进行的,原因是显而易见的。

来一张图帮助理解setChecked的流程:

2.19 Node类的getChildren方法

getChildren(forceInit = false) { // this is dataif (this.level === 0) return this.data;const data = this.data;if (!data) return null;const props = this.store.props;let children = 'children';if (props) {children = props.children || 'children';}if (data[children] === undefined) {data[children] = null;}if (forceInit && !data[children]) {data[children] = [];}return data[children];
} 

用于获取子节点的数据。如果当前节点的层级是0,则直接返回data属性。获取节点的数据data,然后判断children使用什么字符串表示的,默认为"children"。如果data[children]为undefined,则设置为null。如果强制初始化参数forceInit为true则初始化为空数组。最后返回children。

2.20 Node类的updateChildren方法

updateChildren() {const newData = this.getChildren() || [];const oldData = this.childNodes.map((node) => node.data);const newDataMap = {};const newNodes = [];newData.forEach((item, index) => {const key = item[NODE_KEY];const isNodeExists = !!key && arrayFindIndex(oldData, data => data[NODE_KEY] === key) >= 0;if (isNodeExists) {newDataMap[key] = { index, data: item };} else {newNodes.push({ index, data: item });}});if (!this.store.lazy) {oldData.forEach((item) => {if (!newDataMap[item[NODE_KEY]]) this.removeChildByData(item);});}newNodes.forEach(({ index, data }) => {this.insertChild({ data }, index);});this.updateLeafState();
} 

要理解充分理解updateChildren函数,需要知道它在什么时候被调用的。调用处是在tree-store.js中:

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

给store的data属性赋值时会调用root的updateChildren,root是store构造函数中定义的一个属性:

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

可以看到root就是一个Node类的实例,代表整个树的根节点。回到updateChildren函数,从getChildren方法获得新数据,从childNodes方法获取老数据;newDataMap用于区分哪些老数据还在新数据中(例如 old=[2,3,4,5] new=[2,3,7,8] 那newDataMap存放的是2和3);newNodes是新节点集合;遍历新数据,检查老数据中有没有这个新数据,如果有放到newDataMap中来记录一下,没有则直接放到新节点集合newNodes中。接下来从当前节点的子节点中删除不在新数据集合中节点,并把新数据插入。

2.21 Node类的loadData方法

loadData(callback, defaultProps = {}) {//条件判断if (this.store.lazy === true && this.store.load && !this.loaded && (!this.loading || Object.keys(defaultProps).length)) {this.loading = true;// 加载完要做的工作const resolve = (children) => {this.loaded = true;this.loading = false;this.childNodes = [];//创建子节点this.doCreateChildren(children, defaultProps);// 更新叶子节点this.updateLeafState();if (callback) {callback.call(this, children);}};this.store.load(this, resolve);} else {if (callback) {callback.call(this);}}
} 

loadData方法用来加载数据,最终通过调用store上的load方法来完成的。首先判断条件是否满足,即lazy为true、load方法存在、当前节点loaded属性为假、loading状态为假;然后开始加载数据的时候把loading设置为true;resolve表示加载完要做的工作,包括更新状态和创建子节点以及更新叶子节点。

2.22 node.js文件小结

以上就是node.js文件中的全部内容,方法还是蛮多的。主要都是围绕节点的初始化,节点的插入,节点的删除,节点的更新,节点状态的获取和更新来实现的。我们分类总结一下:

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-25 23:09:09  更:2022-09-25 23:10:46 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 9:36:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码