今天来搞一搞 tree 树组件啊 朋友们 感觉这个还是有点复杂 代码有点多 OK 不用着急 先来分析一波最简单的示例 渲染流程
示例代码
<template>
<div>
<el-tree ref="tree" :data="data"></el-tree>
</div>
</template>
<script>
export default {
data() {
return {
data: [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1',
}],
}],
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1',
}],
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1',
}],
}],
}, {
label: '一级 3',
children: [{
label: '二级 3-1',
children: [{
label: '三级 3-1-1',
}],
}, {
label: '二级 3-2',
children: [{
label: '三级 3-2-1',
}],
}],
}],
};
},
};
</script>
- tree组件 会对你传进去的数据 做一层处理 变成好用的数据结构
tree.vue
created() {
this.store = new TreeStore({
// ....
})
}
- OK 朋友们这边的处理封装的流程是怎么样的 我这边先搞一波简版流程来梳理下
const data = [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1',
}],
}],
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1',
}],
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1',
}],
}],
}];
let index = 0;
class Node {
constructor(options) {
this.id = index++;
this.parent = null;
this.data = options.data;
this.store = options.data;
if (options.parent) {
this.parent = options.parent;
}
this.level = 0;
this.childNode = [];
if (this.parent) {
this.level = this.parent.level + 1;
}
this.setData();
}
setData() {
let children;
if (this.level === 0) {
children = this.data;
} else {
children = this.data.children || [];
}
for (let i = 0, j = children.length; i < j; i++) {
this.insertChild({ data: children[i] });
}
}
insertChild(child) {
const children = this.getChildren();
child.parent = this;
child.store = this.store;
child = new Node(child);
this.childNode.push(child);
}
getChildren() {
if (this.level === 0) return this.data;
return this.data.children;
}
}
class TreeStore {
constructor(options) {
this.data = options.data;
// TreeStore 传进去 能让Node 能方便得到 Store里面的配置
this.root = new Node({
data: this.data,
store: this.store,
});
}
}
const store = new TreeStore({
data,
});
console.log(store);
- 这边理顺了之后那么第一次
- 经过这一层的 数据组装 那么数据就形成了
- this.root = this.store.root
- 顶层数据结构
// 存放顶层数据 配置啥的
this.store = new TreeStore({})
// Node 控制每一层数据的状态 勾选 展开啥的
class Node {}
//
- 然后这边渲染
<template>
<!-- // 模板先来看下 -->
<div
class="el-tree"
:class="{
'el-tree--highlight-current': highlightCurrent,
'is-dragging': !!dragState.draggingNode,
'is-drop-not-allow': !dragState.allowDrop,
'is-drop-inner': dragState.dropType === 'inner'
}"
role="tree"
>
<!-- OK的 这边一波 循环 -->
<el-tree-node
v-for="child in root.childNodes"
:node="child"
:props="props"
:render-after-expand="renderAfterExpand"
:show-checkbox="showCheckbox"
:key="getNodeKey(child)"
:render-content="renderContent"
@node-expand="handleNodeExpand">
</el-tree-node>
<!-- OK的 如果是空的 占位符 -->
<div class="el-tree__empty-block" v-if="isEmpty">
<span class="el-tree__empty-text">{{ emptyText }}</span>
</div>
<!-- OK 暂时不懂是什么先不管 -->
<!-- 然后来看看初始化 created mounted -->
<div
v-show="dragState.showDropIndicator"
class="el-tree__drop-indicator"
ref="dropIndicator">
</div>
</div>
</template>
el-tree-node
<template>
<!-- 一些七七八八的属性 没什么 -->
<div
class="el-tree-node"
@click.stop="handleClick"
@contextmenu="($event) => this.handleContextMenu($event)"
v-show="node.visible"
:class="{
'is-expanded': expanded,
'is-current': node.isCurrent,
'is-hidden': !node.visible,
'is-focusable': !node.disabled,
'is-checked': !node.disabled && node.checked
}"
role="treeitem"
tabindex="-1"
:aria-expanded="expanded"
:aria-disabled="node.disabled"
:aria-checked="node.checked"
:draggable="tree.draggable"
@dragstart.stop="handleDragStart"
@dragover.stop="handleDragOver"
@dragend.stop="handleDragEnd"
@drop.stop="handleDrop"
ref="node"
>
<!-- 层级这边来控制padding -->
<div class="el-tree-node__content"
:style="{ 'padding-left': (node.level - 1) * tree.indent + 'px' }">
<span
@click.stop="handleExpandIconClick"
:class="[
{ 'is-leaf': node.isLeaf, expanded: !node.isLeaf && expanded },
'el-tree-node__expand-icon',
tree.iconClass ? tree.iconClass : 'el-icon-caret-right'
]"
>
</span>
<el-checkbox
v-if="showCheckbox"
v-model="node.checked"
:indeterminate="node.indeterminate"
:disabled="!!node.disabled"
@click.native.stop
@change="handleCheckChange"
>
</el-checkbox>
<span
v-if="node.loading"
class="el-tree-node__loading-icon el-icon-loading">
</span>
<!-- // 这边渲染要看下 看他是怎么渲染的 在这里的components直接定义 -->
<node-content :node="node"></node-content>
</div>
<el-collapse-transition>
<!-- // 这边就是子级的 展开折叠 -->
<div
class="el-tree-node__children"
v-if="!renderAfterExpand || childNodeRendered"
v-show="expanded"
role="group"
:aria-expanded="expanded"
>
<!-- // 递归渲染这个组件 -->
<el-tree-node
:render-content="renderContent"
v-for="child in node.childNodes"
:render-after-expand="renderAfterExpand"
:show-checkbox="showCheckbox"
:key="getNodeKey(child)"
:node="child"
@node-expand="handleChildNodeExpand">
</el-tree-node>
</div>
</el-collapse-transition>
</div>
</template>
<script>
// 先看下初始化吧 created
// 这个展开折叠动画组件 之前砸门也有分析过
import ElCollapseTransition from '@/transitions/collapse-transition';
import ElCheckbox from '@/components/checkbox';
import emitter from '@/mixins/emitter';
import { getNodeKey } from './model/util';
export default {
name: 'ElTreeNode',
componentName: 'ElTreeNode',
mixins: [emitter],
props: {
node: {
default() {
return {};
},
},
props: {},
renderContent: Function,
renderAfterExpand: {
type: Boolean,
default: true,
},
showCheckbox: {
type: Boolean,
default: false,
},
},
components: {
ElCollapseTransition,
ElCheckbox,
// 来看 先看下
NodeContent: {
props: {
node: {
required: true,
},
},
render(h) {
const parent = this.$parent;
const { tree } = parent;
const { node } = this;
const { data, store } = node;
// 这边渲染方式有三种
// 传入renderContent 函数 parent._renderProxy 其实就是parent组件 如果直接Es6 Proxy parent._renderProxy就是对组件做了一层 proxy包装
// 比如说<el-tree><span>111</span></el-tree> 通过 tree.$scopedSlots.default 获取也就是span那部分 然后调用传入props
// 默认的模板
// 那一次的渲染逻辑大概就是这样
return (
parent.renderContent
? parent.renderContent.call(parent._renderProxy, h, {
_self: tree.$vnode.context, node, data, store,
})
: tree.$scopedSlots.default
? tree.$scopedSlots.default({ node, data })
: <span class="el-tree-node__label">{ node.label }</span>
);
},
},
},
data() {
return {
tree: null,
expanded: false,
childNodeRendered: false,
oldChecked: null,
oldIndeterminate: null,
};
},
watch: {
// ...
},
methods: {
// ...
},
created() {
const parent = this.$parent;
// 最高一级的 也就是tree 有个标识符 isTree = true
if (parent.isTree) {
this.tree = parent;
} else {
// 一级一级往上取 类似于 $store $router 的赋值方式
this.tree = parent.tree;
}
const { tree } = this;
if (!tree) {
console.warn('Can not find node\'s tree.');
}
const props = tree.props || {};
const childrenKey = props.children || 'children';
// 监听当前这个数据的子级变化
this.$watch(`node.data.${childrenKey}`, () => {
this.node.updateChildren();
});
// 是否是展开 状态
if (this.node.expanded) {
this.expanded = true;
this.childNodeRendered = true;
}
// 手风琴模式做的处理 展示先不用管 回到 NodeContent 的渲染
if (this.tree.accordion) {
this.$on('tree-node-expand', (node) => {
if (this.node !== node) {
this.node.collapse();
}
});
}
},
};
初次渲染大概就是这样 后面我在一个个 功能分析下
- 今天有空 在来分析下拖拽实现吧
拖拽的实现
- 回顾下 树组件 总组件是tree 子组件是tree-node递归组件
- 使用代码
<template>
<div>
<!-- 设置了 draggable 属性变成可拖拽 -->
<!-- 监听拖拽过程的一些事件 -->
<!-- allow-drop 判断节点能否被拖拽 -->
<!-- allow-drop 拖拽时判定目标节点能否被放置。type 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后 -->
<!-- 那我们 draggable 入手看看做了些什么事 -->
<el-tree
:data="data"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@node-drag-leave="handleDragLeave"
@node-drag-over="handleDragOver"
@node-drag-end="handleDragEnd"
@node-drop="handleDrop"
draggable
:allow-drop="allowDrop"
:allow-drag="allowDrag">
</el-tree>
</div>
</template>
tree-node子组件
<template>
<!-- 可以看到这边是元素设置为可拖拽 然后监听了 拖拽的事件
然后来看下每个事件做了什么 -->
<div
class="el-tree-node"
@click.stop="handleClick"
@contextmenu="($event) => this.handleContextMenu($event)"
v-show="node.visible"
:class="{
'is-expanded': expanded,
'is-current': node.isCurrent,
'is-hidden': !node.visible,
'is-focusable': !node.disabled,
'is-checked': !node.disabled && node.checked
}"
role="treeitem"
tabindex="-1"
:aria-expanded="expanded"
:aria-disabled="node.disabled"
:aria-checked="node.checked"
:draggable="tree.draggable"
@dragstart.stop="handleDragStart"
@dragover.stop="handleDragOver"
@dragend.stop="handleDragEnd"
@drop.stop="handleDrop"
ref="node"
>
<!---->
</div>
</template>
<script>
// ...
export default {
methods: {
// OK 开始拖动
handleDragStart(event) {
if (!this.tree.draggable) return;
// 这边就直接让父组件tree触发这个事件 传出去组件本身 我们看看
this.tree.$emit('tree-node-drag-start', event, this);
},
handleDragOver(event) {
if (!this.tree.draggable) return;
// OK 触发tree 监听的这个事件 往下面看 监听做了啥
this.tree.$emit('tree-node-drag-over', event, this);
event.preventDefault();
},
handleDragEnd(event) {
if (!this.tree.draggable) return;
this.tree.$emit('tree-node-drag-end', event, this);
},
}
}
</script>
tree 组件
<script>
// ...
export default {
created() {
const { dragState } = this;
this.$on('tree-node-drag-start', (event, treeNode) => {
// allowDrag 判断是否能被拖动 如果不能在这边阻止
if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) {
event.preventDefault();
return false;
}
// 属性指定拖放操作所允许的一个效果
event.dataTransfer.effectAllowed = 'move';
// wrap in try catch to address IE's error when first param is 'text/plain'
try {
// setData is required for draggable to work in FireFox
// the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
event.dataTransfer.setData('text/plain', '');
// eslint-disable-next-line no-empty
} catch (e) {}
// dragState 拖拽的状态都放这边
// 设置正在拖拽的节点
dragState.draggingNode = treeNode;
// 然后就是发射事件出去 基本也没啥 在往上看handleDragOver
this.$emit('node-drag-start', treeNode.node, event);
});
// 这个事件 处理就有点多了
this.$on('tree-node-drag-over', (event, treeNode) => {
// 找到要放入的节点组件 如果拖动不在其他节点范围上就是自己 返回的是个vue组件对象
// export const findNearestComponent = (element, componentName) => {
// let target = element;
// // target 不一定是节点的最外层 这个就是找到这个节点的最外层 也就是 tree-node的$el
// while (target && target.tagName !== 'BODY') {
// if (target.__vue__ && target.__vue__.$options.name === componentName) {
// return target.__vue__;
// }
// target = target.parentNode;
// }
// return null;
// };
const dropNode = findNearestComponent(event.target, 'ElTreeNode');
// dropNode 可以看做是手上拖拽的节点 跟目标节点 进行某个操作
// 这个是目标节点
// 老目标节点
const oldDropNode = dragState.dropNode;
// 老目标节点不是前面要操作的节点 那就去掉老目标节点的class
if (oldDropNode && oldDropNode !== dropNode) {
removeClass(oldDropNode.$el, 'is-drop-inner');
}
// 当前手上拖拽的
const { draggingNode } = dragState;
if (!draggingNode || !dropNode) return;
// 默认都可以操作 放上放下放里面去
let dropPrev = true;
let dropInner = true;
let dropNext = true;
let userAllowDropInner = true;
// 如果有传进来位置限制函数allowDrop 对这些限制在做赋值
if (typeof this.allowDrop === 'function') {
dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev');
userAllowDropInner = dropInner = this.allowDrop(draggingNode.node, dropNode.node, 'inner');
dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next');
}
// 如果遇到不能放入的 设置下鼠标系统样式
event.dataTransfer.dropEffect = dropInner ? 'move' : 'none';
// 有一个位置允许放入 并且这个不是刚刚的那个就比如说 1放到2上面 在放到3上面 2就是老节点 3就是新的
if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
if (oldDropNode) {
// 如果有的话 2老节点 就要发射这个离开事件
this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event);
}
// 新的这个就是进入
this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event);
}
// 赋值当前要操作的目标节点
if (dropPrev || dropInner || dropNext) {
dragState.dropNode = dropNode;
}
// dropNode.node 是组件构造的一个类 class Node{}
// get nextSibling() {
// const { parent } = this;
// if (parent) {
// const index = parent.childNodes.indexOf(this);
// if (index > -1) {
// return parent.childNodes[index + 1];
// }
// }
// return null;
// }
// 要放的下个是正拖拽的这个 那么久不能放下面
if (dropNode.node.nextSibling === draggingNode.node) {
dropNext = false;
}
// 上面
if (dropNode.node.previousSibling === draggingNode.node) {
dropPrev = false;
}
// 是否包括这个 不能放里面 也就是说比如3是1的children 那么1就包含3
if (dropNode.node.contains(draggingNode.node, false)) {
dropInner = false;
}
// 正在拖拽 放入的是自己 正在拖拽的要放入自己的下级
if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
dropPrev = false;
dropInner = false;
dropNext = false;
}
// 要放到的这个元素位置
const targetPosition = dropNode.$el.getBoundingClientRect();
// 树元素 最外面的元素
const treePosition = this.$el.getBoundingClientRect();
let dropType;
// 得到百分比 上下的百分比
// 比如说 这个元素高度是 40 那么pre系数是0.25 那么就是距离顶部10的位置就是向这个元素的前面插入
const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1;
const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1;
// 指示器的高度 就是一根蓝色的横线
let indicatorTop = -9999;
const distance = event.clientY - targetPosition.top;
// 距离小于 这个元素高度的这个百分比 那就当做是插入上面
if (distance < targetPosition.height * prevPercent) {
dropType = 'before';
// 后面
} else if (distance > targetPosition.height * nextPercent) {
dropType = 'after';
// 里间
} else if (dropInner) {
dropType = 'inner';
} else {
dropType = 'none';
}
// 得到这个位置
const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect();
// 指示器元素
const { dropIndicator } = this.$refs;
// 设置指示器的位置
if (dropType === 'before') {
indicatorTop = iconPosition.top - treePosition.top;
} else if (dropType === 'after') {
indicatorTop = iconPosition.bottom - treePosition.top;
}
dropIndicator.style.top = `${indicatorTop}px`;
dropIndicator.style.left = `${iconPosition.right - treePosition.left}px`;
// 设置这个元素的放入样式
if (dropType === 'inner') {
addClass(dropNode.$el, 'is-drop-inner');
} else {
removeClass(dropNode.$el, 'is-drop-inner');
}
// 这个 拖拽主要就是设置 样式交互
dragState.showDropIndicator = dropType === 'before' || dropType === 'after';
dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner;
dragState.dropType = dropType;
this.$emit('node-drag-over', draggingNode.node, dropNode.node, event);
// 再看看上面的 handleDragEnd
});
// 拖拽结束时
this.$on('tree-node-drag-end', (event) => {
// 获取状态
const { draggingNode, dropType, dropNode } = dragState;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
// 两个都有才行
if (draggingNode && dropNode) {
const draggingNodeCopy = { data: draggingNode.node.data };
if (dropType !== 'none') {
// 删除掉自己 在父亲的children及childNodes删除
draggingNode.node.remove();
}
// 放在哪里更新数据 更新下这个树组件的数据了
if (dropType === 'before') {
// 比如说这个插入前面的
// insertBefore(child, ref) {
// let index;
// if (ref) {
// index = this.childNodes.indexOf(ref);
// }
// this.insertChild(child, index);
// }
// 传入当前这个删除的 以及要操作的目标 找到要操作目标的位置 然后插入这个新的 其他也是差不多了
dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node);
} else if (dropType === 'after') {
dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node);
} else if (dropType === 'inner') {
dropNode.node.insertChild(draggingNodeCopy);
}
if (dropType !== 'none') {
this.store.registerNode(draggingNodeCopy);
}
removeClass(dropNode.$el, 'is-drop-inner');
this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event);
if (dropType !== 'none') {
this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event);
}
}
// 有正在拖拽的没有 要操作的对象
if (draggingNode && !dropNode) {
this.$emit('node-drag-end', draggingNode.node, null, dropType, event);
}
// 重置拖拽对象
dragState.showDropIndicator = false;
dragState.draggingNode = null;
dragState.dropNode = null;
dragState.allowDrop = true;
});
}
}
</script>
后面继续 更新 大家要是觉得有啥问题 可以留言 交流
或者点个赞啥的表示下鼓励