iView-Cascader组件分析

1,421 阅读2分钟

源码分析

  • 组件组织及数据流
  • 涉及的共用方法
  • 具体实现

组件组织及数据流

再看下伪代码

<div ref="reference">
    <Input/>
    <div v-show="filterable && query === ''"></div>
</div>
<Drop>
    <Caspanel>
        <Casitem class="1"/>
        <Casitem class="2"/>
        <Casitem class="3"/>
        ...
    </Caspanel>
</Drop>

首先,获取到数据后,利用Casitem组件自递归方式,所以就有了父子关系,即class1是class2的父级,class2是class3的父级,以此类推,就可以形成一条'链';当用户点击某一项,当前项通过寻找上一级this.$parent,进行$emit操作,触发on-result-change事件,最终在Cascader组件监听到;当默认赋予选中值, 通过寻找下一级this.$children,进行$emit操作,触发on-find-selected事件,对比value值,最终渲染出高亮的内容。

涉及的共用方法

1.指令相关的: clickOutside,函数劫持;点击的元素如果不在绑定的元素中,则运行绑定的函数,反之则不执行。

export default {
  bind (el, binding, vnode) {
      function documentHandler (e) {
          if (el.contains(e.target)) {
              return false;
          }
          if (binding.expression) {
              binding.value(e);
          }
      }
      el.__vueClickOutside__ = documentHandler;
      document.addEventListener('click', documentHandler);
  },
  unbind (el, binding) {
      document.removeEventListener('click', el.__vueClickOutside__);
      delete el.__vueClickOutside__;
  }
};

2.Mixins相关的: Emitter,包含两个方法broadcast和dispatch;broadcast是用来查找指定子组件并且$emit指定事件,dispatch是用来查找指定父组件并且$emit指定事件。

具体实现

思考以下问题,下文会围绕这4个问题进行分析
1.如何将选中的数据按照父到子的顺序进行展示?['广东省', '中山市','石岐区']
2.为什么这里需要多加div来展示选中的内容?
<Input/>
<div v-show="filterable && query === ''"></div>
3.如果指定选中的值(value),如何正确渲染出对应的内容?
<Cascader v-model="value"></Cascader>
  • 第一个问题:
先看Cascader 组件,通过监听事件on-result-change, 得到一个对象params, 同时将选中的结果值赋予this.selected, 方法updateResult等待子元素进行调用
//Cascader 组件
created () {
    this.$on('on-result-change', (params) => {
        // lastValue: is click the final val
        // fromInit: is this emit from update value
        const lastValue = params.lastValue; //是否是最后一个Casitem的选中值
        const changeOnSelect = params.changeOnSelect; //是否每个节点可选
        const fromInit = params.fromInit; //是否初始化状态

        ...
        this.selected = this.tmpSelected;
        ...
    });
},
methods: {
	updateResult (result) {
        this.tmpSelected = result;
    }
}
Caspanel组件通过递归,将data对应的内容渲染出来;hover和click事件调用handleTriggerItem方法,通过调用emitUpdate进行判断,如果父级元素是Caspanel组件,那么拼接item,最后的父级元素是Cascader组件,调用上面所说的updateResult方法,对this.tmpSelected进行赋值;同时通过on-result-change事件通知Cascader组件,判断是否为最后一个值或启用changeOnSelect,再触发on-change事件。
//Caspanel组件
<template>
    <span>
        <ul v-if="data && data.length">
            <Casitem
                v-for="item in data"
                :key="getKey()"
                :data="item"
                :tmp-item="tmpItem"
                @click.native.stop="handleClickItem(item)"
                @mouseenter.native.stop="handleHoverItem(item)"></Casitem>//使用的是mouseenter事件,而非mouseover事件
        </ul>
        <Caspanel 
        	v-if="sublist && sublist.length" 
            :data="sublist" 
            :disabled="disabled" 
            :trigger="trigger" 
            :change-on-select="changeOnSelect"></Caspanel>
    </span>
</template>
<script>
...
methods: {
	handleClickItem (item) {
        if (this.trigger !== 'click' && item.children && item.children.length) return;  
        this.handleTriggerItem(item, false, true);
    },
    handleHoverItem (item) {
        if (this.trigger !== 'hover' || !item.children || !item.children.length) return;  
        this.handleTriggerItem(item, false, true);
    },
    handleTriggerItem (item, fromInit = false, fromUser = false) {
    	const backItem = this.getBaseItem(item);
        ...
        this.tmpItem = backItem;
        this.emitUpdate([backItem]);

        if (item.children && item.children.length){
            this.sublist = item.children;
            this.dispatch('Cascader', 'on-result-change', {
                lastValue: false,
                changeOnSelect: this.changeOnSelect,
                fromInit: fromInit
            });
            ...
        } else {
            this.sublist = [];
            this.dispatch('Cascader', 'on-result-change', {
                lastValue: true,
                changeOnSelect: this.changeOnSelect,
                fromInit: fromInit
            });
        }
    },
    getBaseItem (item) {
        let backItem = Object.assign({}, item);
        if (backItem.children) {
            delete backItem.children;
        }

        return backItem;
    },
    updateResult (item) {
        this.result = [this.tmpItem].concat(item);
        this.emitUpdate(this.result);
    },
    emitUpdate (result) {
        if (this.$parent.$options.name === 'Caspanel') {
            this.$parent.updateResult(result);
        } else {
            this.$parent.$parent.updateResult(result);
        }
    },
}
...
</script>
  • 第二个问题
如果启用了filterable过滤功能,则显示Div进行文本展示,有更好的用户体验,不需要手动删除输入框内容
//Cascader 组件
<i-input
    ref="input"
    :value="displayInputRender"
    @on-change="handleInput"></i-input>
<div
    v-show="filterable && query === ''"
    @click="handleFocus">{{ displayRender }}</div>
...
<script>
	computed: {
    	displayRender () {
            let label = [];
            for (let i = 0; i < this.selected.length; i++) {
                label.push(this.selected[i].label);
            }

            return this.renderFormat(label, this.selected); //外部传入的自定义显示格式
        },
        displayInputRender () {
            return this.filterable ? '' : this.displayRender;
        },
    },
    methods: {
    	handleInput (event) {
            this.query = event.target.value;
        },
        handleFocus () {
            this.$refs.input.focus();
        }
    }
</script>
  • 第三个问题
观测到value值发生更改,调用updateSelected,此时对子组件广播事件on-find-selected,并且将选中的值(数组)传递,子组件对值进行遍历,匹配到值后,删除数组中的值,即是第一个,然后再对子组件广播事件on-find-selected,最后,匹配每一项的value跟原始数据是否一致,渲染出组件。
//Cascader 组件
methods:  {
	updateSelected (init = false, changeOnSelectDataChange = false) {
        if (!this.changeOnSelect || init || changeOnSelectDataChange) {
            this.broadcast('Caspanel', 'on-find-selected', {
                value: this.currentValue
            });
        }
    },
}
watch: {
	value (val) {
        this.currentValue = val;
        if (!val.length) this.selected = [];
    },
    currentValue () {
        ...
        this.updateSelected(true);
    },
}
//Caspanel组件
this.$on('on-find-selected', (params) => {
    const val = params.value;
    let value = [...val];
    for (let i = 0; i < value.length; i++) {
        for (let j = 0; j < this.data.length; j++) {
            if (value[i] === this.data[j].value) {
                this.handleTriggerItem(this.data[j], true);
                value.splice(0, 1);
                this.$nextTick(() => {
                    this.broadcast('Caspanel', 'on-find-selected', {
                        value: value
                    });
                });
                return false;
            }
        }
    }
});