• 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);
    },

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

  • 判断节点是否被禁用
  • 设置 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 是怎么回事。