源码分析
- 组件组织及数据流
- 涉及的共用方法
- 具体实现
组件组织及数据流
再看下伪代码
<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;
}
}
}
});