iview树组件源码阅读笔记

好久没有记一些东西了,本菜鸡又来水文章了。

写在前面

  • iview里面的树组件思路跟vue官网上的树形视图示例类似,应用了组件的递归使用(好像是废话

  • 整个树组件里有两个vue文件,一个是递归使用的node.vue,一个是父组件tree.vue

  • 组件内的事件大致分为三种:expand(展开)、select(点击节点title触发)和check(复选框的选中与取消触发)

node.vue

这个组件内包括树组件的一个选项,以及它的children(如果有)

template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<collapse-transition>
<ul :class="classes" v-show="visible">
<li>
<span :class="arrowClasses" @click="handleExpand">
<Icon type="arrow-right-b"></Icon>
</span>
<Checkbox
v-if="showCheckbox"
:value="data.checked"
:indeterminate="indeterminate"
:disabled="data.disabled || data.disableCheckbox"
@click.native.prevent="handleCheck"></Checkbox>
<span :class="titleClasses" v-html="data.title" @click="handleSelect"></span>
<Tree-node
v-for="item in data.children"
:key="item.nodeKey"
:data="item"
:visible="data.expand"
:multiple="multiple"
:show-checkbox="showCheckbox">
</Tree-node>
</li>
</ul>
</collapse-transition>
</template>

collapse-transition应该是父节点展开收起的动画效果,ul则是包括一个父节点,里面的tree-node是这个父节点的所有子节点,通过v-for循环渲染出来,而其子节点的每一项跟父节点是一样的结构,所以tree-node实际上就是node.vue组件本身。

然后是script部分

主要props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
props: {
data: {
type: Object,
default () {
return {};
}
},
multiple: {
type: Boolean,
default: false
},
showCheckbox: {
type: Boolean,
default: false
},
visible: {
type: Boolean,
default: false
}
},
  • data: 树的数据,根据template中的v-for="item in data.children":data="item"可以知道每个节点的data都是当前节点的信息包括所有的子节点,简化的数据结构如下
1
2
3
4
5
6
7
8
9
10
11
{
title: '父节点',
children: [{
title: '子节点1',
}, {
title: '子节点2',
children: [{
title: '子节点2的子节点',
}]
}],
}

methods中,又对data追加声明了4个额外的属性:selected、disabled、expand、checked,具体是干嘛的看名字应该很清楚了

  • multiple: 控制多选的布尔值,默认false,这是控制能否select多个节点的值,而不是控制能不能check多个复选框的值

  • 其他props,showCheckbox、visible字面意思都能看出来,不多说

data

1
2
3
4
5
6
data () {
return {
prefixCls: prefixCls,
indeterminate: false
};
},
  • prefixCls:控制class前缀的,主要用于样式

  • indeterminate:对应checkbox的indeterminate状态

computed

computed中返回的都是跟class相关的值,应用样式的,就不说了。

created & mounted

1
2
3
4
5
6
7
8
9
10
created () {
// created node.vue first, mounted tree.vue second
if (!this.data.checked) this.$set(this.data, 'checked', false);
},
mounted () {
this.$on('indeterminate', () => {
this.broadcast('TreeNode', 'indeterminate');
this.setIndeterminate();
});
}

在created阶段定义了data props的checked属性

在mounted阶段监听了indeterminate事件,并广播该事件到所有的子节点中

methods

methods中主要处理一些事件,在template中可以看到用v-on绑定了三个click事件

  • handleExpand: 处理树展开收起
  • handleCheck: 处理checkbox勾选
  • handleSelect: 处理选项选中/取消选中

handleExpand

1
2
3
4
5
handleExpand () {
if (this.data.disabled) return;
this.$set(this.data, 'expand', !this.data.expand);
this.dispatch('Tree', 'toggle-expand', this.data);
},

处理展开收起只做了三个微小的工作:

  1. 判断节点是否被禁用
  2. 设置this.data.expand值,取反
  3. 在父组件Tree上触发toggle-expand事件

handleCheck

1
2
3
4
5
6
7
8
9
10
11
12
handleCheck () {
if (this.disabled) return;
const checked = !this.data.checked;
if (!checked || this.indeterminate) {
findComponentsDownward(this, 'TreeNode').forEach(node => node.data.checked = false);
} else {
findComponentsDownward(this, 'TreeNode').forEach(node => node.data.checked = true);
}
this.data.checked = checked;
this.dispatch('Tree', 'checked');
this.dispatch('Tree', 'on-checked');
},

处理check事件:

  1. 判断是否禁用(这边好像写错了?并没有找到this.disabled,怀疑是this.data.disabled
  2. const checked取点击后this.data.checked的值
  3. 如果checkedfalse,或者checkbox的状态是indeterminate,则将子节点全部取消勾选;否则将子节点全部勾选
  4. 修改this.data.checked为最新的值(因为在点击进入这个事件处理函数时,checkbox的状态还是未改变的状态,所以这边手动修改了this.data.checked的值)
  5. 在父组件Tree上触发checkedon-checked两个事件

handleSelect

1
2
3
4
5
6
7
8
9
10
11
handleSelect () {
if (this.data.disabled) return;
if (this.data.selected) {
this.data.selected = false;
} else if (this.multiple) {
this.$set(this.data, 'selected', !this.data.selected);
} else {
this.dispatch('Tree', 'selected', this.data);
}
this.dispatch('Tree', 'on-selected');
},

处理select事件:

  1. 判断是否禁用
  2. 当节点是选中时,改为非选中;否则判断是否多选,如果是,则将节点自身改为选中;否则在父组件Tree上触发selected事件(父组件中会把所有节点的selected改为false,并把当前节点的selected改为true
  3. 最后在父组件Tree上触发on-selected事件

node.vue相关的废话就说到这边


强势的分割线


tree.vue

Tree组件主要的作用是给Node组件加个外壳,不想写太多废话了,主要讲讲mounted跟watch吧。

template

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div :class="prefixCls">
<Tree-node
v-for="item in data"
:key="item.nodeKey"
:data="item"
visible
:multiple="multiple"
:show-checkbox="showCheckbox">
</Tree-node>
<div :class="[prefixCls + '-empty']" v-if="!data.length">{{ localeEmptyText }}</div>
</div>
</template>

tree.vue的结构比较简单,主要的工作就是把树的最外层渲染出来,然后每个Tree-node组件内会递归渲染各自的子节点

mounted

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mounted () {
this.updateData();
this.$on('selected', ori => {
const nodes = findComponentsDownward(this, 'TreeNode');
nodes.forEach(node => {
this.$set(node.data, 'selected', false);
});
this.$set(ori, 'selected', true);
});
this.$on('on-selected', () => {
this.$emit('on-select-change', this.getSelectedNodes());
});
this.$on('checked', () => {
this.updateData(false);
});
this.$on('on-checked', () => {
this.$emit('on-check-change', this.getCheckedNodes());
});
this.$on('toggle-expand', (payload) => {
this.$emit('on-toggle-expand', payload);
});
},

mounted中主要是监听Node组件中在Tree触发的各种事件(参见node.vue methods

那些往外部触发事件的就不说了,来看看selected事件的处理函数

1
2
3
4
5
6
7
this.$on('selected', ori => {
const nodes = findComponentsDownward(this, 'TreeNode');
nodes.forEach(node => {
this.$set(node.data, 'selected', false);
});
this.$set(ori, 'selected', true);
});
  1. 首先找到所有的TreeNode子组件(包括子组件的子组件,也就是无论是迭代几层都包括在里面)
  2. 遍历子组件,将子组件的data.selected都改为false
  3. 将触发事件的组件的data.selected改为true

结合node.vue中的handleSelect,就不难理解select功能的工作原理了。

watch

1
2
3
4
5
6
7
8
watch: {
data () {
this.$nextTick(() => {
this.updateData();
this.broadcast('TreeNode', 'indeterminate');
});
}
}

watch中监听了data的变化。当data变化时,在DOM更新完成后,执行updateData方法更新数据,并broadcast一个indeterminate事件。
结合node.vue mounted,在监听到indeterminate事件后,也向下broadcast了该事件,保证所有子节点都能接收到该事件的触发,并执行相应方法。

tree.vue就讲到这里吧。

用到的utils & mixins

先留个坑,会解释dispatch和broadcast,还有 findComponentsDownward 是怎么回事。