好久没有记一些东西了,本菜鸡又来水文章了。
写在前面
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,具体是干嘛的看名字应该很清楚了
data
1 2 3 4 5 6
| data () { return { prefixCls: prefixCls, indeterminate: false }; },
|
computed
computed中返回的都是跟class相关的值,应用样式的,就不说了。
created & mounted
1 2 3 4 5 6 7 8 9 10
| created () { 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); },
|
处理展开收起只做了三个微小的工作:
- 判断节点是否被禁用
- 设置
this.data.expand
值,取反
- 在父组件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事件:
- 判断是否禁用(这边好像写错了?并没有找到this.disabled,怀疑是this.data.disabled
const checked
取点击后this.data.checked
的值
- 如果
checked
为false
,或者checkbox的状态是indeterminate,则将子节点全部取消勾选;否则将子节点全部勾选
- 修改
this.data.checked
为最新的值(因为在点击进入这个事件处理函数时,checkbox的状态还是未改变的状态,所以这边手动修改了this.data.checked
的值)
- 在父组件Tree上触发
checked
和on-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事件:
- 判断是否禁用
- 当节点是选中时,改为非选中;否则判断是否多选,如果是,则将节点自身改为选中;否则在父组件Tree上触发
selected
事件(父组件中会把所有节点的selected
改为false
,并把当前节点的selected
改为true
)
- 最后在父组件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); });
|
- 首先找到所有的TreeNode子组件(包括子组件的子组件,也就是无论是迭代几层都包括在里面)
- 遍历子组件,将子组件的
data.selected
都改为false
- 将触发事件的组件的
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 是怎么回事。