本文從select組件的代碼結構入手,瞭解select組件如何組織父子組件通訊,並重點分析了ElSelect組件 和 ElOption組件實現的巧妙之處。並介紹select組件的四個功能是如何實現的。html
Element 使用 div 模擬了 select,select 組件含有navigation-mixin.js
,option-group.vue
,option.vue
,select-dropdown.vue
,select.vue
等文件前端
其select.vue
主文件的 HTML 結構和主流模擬 select 的思路類似,只不過 Element 更復雜一點,select 組件 HTML 結構以下(爲告終構清晰,部分代碼省略):vue
<div class="el-select">
<!-- 多選的狀況 -->
<div v-if="multiple" class="el-select__tags">
<span>
<!-- 放置多選時的選中的tag,以tag展示,或者合併成一段文字 -->
</span>
<!-- 搜索功能 -->
<input v-model="query" v-if="filterable" />
</div>
<!-- 單選的時候選中的值回顯 -->
<el-input v-model="selectedLabel" :class="{ 'is-focus': visible }">
<!-- xxx -->
</el-input>
<!--下拉框-->
<el-select-menu>
<el-scrollbar v-show="options.length > 0 && !loading">
<!-- 選項內容 -->
<el-option :value="query" created v-if="showNewOption"> </el-option>
<slot></slot>
</el-scrollbar>
<!-- options爲空顯示的默認文字或者select處於loading的狀況 -->
<template v-if="emptyText && (!allowCreate || loading || (allowCreate && options.length === 0 ))" >
<!-- xxx -->
</template>
</el-select-menu>
</div>
複製代碼
上一節分析代碼結構時候,讀者可能就以爲 select 組件很複雜了,其是如何處理父子組件的通訊的呢?以前我分析過其 Table 組件的組織方法,使用了簡單的 store 模式,select 組件則採用了 broadcast/dispatch 和 inject/provide。(PS:broadcast 是 ElementUI 的本身寫的,vue 2.0 已經移除了$dispatch 和 $broadcast
)element-ui
Vue2.2.0 新增 API,這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。一言而蔽之:祖先組件中經過 provider 來提供變量,而後在子孫組件中經過 inject 來注入變量。api
後面我會分析 ElementUI 如何使用的 inject 和 provide,以及爲何不能使用 this.$parent
和this.$children
來互相通訊緩存
broadcast 的代碼放置在'element-ui/src/mixins/emitter',經過 mixins 混入,使用方式 eg:this.broadcast('ElOption', 'queryChange', '');
和 this.dispatch("ElSelect", "setSelected");
ide
做爲一個才入行一年多的前端,真沒用過 broadcast ,具體詳見Vue$dispatch 和 $broadcast
詳解源碼分析
雖然 broadcast 有它的缺點,好比基於組件樹結構的事件流方式實在是讓人難以理解,沒有解決兄弟組件間的通訊問題。可是在父子層嵌套組件中,經過 $dispatch 和 $broadcast
定向的向某個父或者子組件遠程調用事件,避免了經過傳 props 或者使用 refs 調用組件實例方法的操做,仍是很簡潔的。post
若是我寫這個組件,可能會這麼使用 <my-select v-model="input" options="options" />
,即 Select 組件的選項經過 options 傳入組件。也許最後功能能夠實現,可是這樣作感受有點封裝過分了。從這個組件的結構來說,和原生的select不太像。咱們常常這樣使用 Select 組件,是否是很直觀:ui
<el-select v-model="input" filterable clearable placeholder="請選擇">
<el-option label="foo" value="foo"></el-option>
<el-option label="foo" value="foo"></el-option>
</el-select>
複製代碼
從代碼結構也能夠看到 select 組件裏有個默認插槽,顯示是沒什麼問題,可是考慮到 select 組件須要和 option 組件互相通訊,好比 option 組件須要瞭解 select 組件是否多選,是否能夠搜索等等,select 組件須要瞭解 option 組件的個數等等,如何作到呢?答案就是 inject 和 provide
select 組件裏直接把本身的實例this
注入到了 option 組件,option組件經過 this.select
直接修改父組件屬性:
provide() {
return {
'select': this
};
}
複製代碼
有多少選項就建立多少 el-option
實例,看一下el-option
的created
的生命週期:
created() {
//把 el-option實例push進父組件select的options
this.select.options.push(this);
this.select.cachedOptions.push(this);
this.select.optionsCount++;
this.select.filteredOptionsCount++;
// 監聽自定義的事件
this.$on('queryChange', this.queryChange);
this.$on('handleGroupDisabled', this.handleGroupDisabled);
}
複製代碼
有同窗可能就問了,this.select.options.push(this);
怎麼一直是 push 操做,若是我 選項不定,select.options
豈不是有不少值。不用擔心,看一下el-option
的 beforeDestroy
生命週期
beforeDestroy() {
this.select.onOptionDestroy(this.select.options.indexOf(this));
}
複製代碼
父組件 select 的 onOptionDestroy
方法,el-option
銷燬了,父組件的options裏面也會將其移除:
onOptionDestroy(index) {
if (index > -1) {
this.optionsCount--;
this.filteredOptionsCount--;
// cachedOptions 沒有去除
this.options.splice(index, 1);
}
}
複製代碼
option-group.vue
裏其 HTML 的結構很簡單:
<ul class="el-select-group__wrap" v-show="visible">
<li class="el-select-group__title">{{ label }}</li>
<li>
<ul class="el-select-group">
<slot></slot>
</ul>
</li>
</ul>
複製代碼
分組功能至關於把 el-option 包了一層,那麼這個時候el-option
組件就至關於el-select
組件的孫組件了,那麼 el-option 和 el-select 組件就不能經過this.$parent
通訊了,爲了兼容el-option
爲子組件或爲孫組件的兩種狀況,使用inject
和provide
就很合適。
實際上是本地篩選功能,所以啓用本地搜索功能須要傳入 filterable
字段而不是searchable
,篩選功能真的很妙,前面也說過有多少選項就建立多少 el-option
實例,所以當咱們啓用篩選得時候,只須要把和正則不匹配的 el-option 實例隱藏掉就能夠了。代碼以下(是否是比想象中的簡單):
queryChange(query) {
// 由於select還能夠手動建立條目,因此手動建立的條目必定顯示
this.visible =
new RegExp(escapeRegexpString(query), 'i').test(this.currentLabel) ||
this.created;
if (!this.visible) {
// 篩選的選項數目減一
this.select.filteredOptionsCount--;
}
}
複製代碼
代碼部分就是如此簡單,由於咱們知道遠程搜索,選項都是服務端返回,從新新建el-option
組件便可。
if (this.remote && typeof this.remoteMethod === 'function') {
this.hoverIndex = -1;
this.remoteMethod(val);
}
}
複製代碼
element-ui 當你的選項是固定的時候,它會基於你選中的 value,回顯對應的 label,可是遠程搜索組件因爲options
不固定,回顯就是一個問題。
解決的方法就是傳入已選中的值的options
傳入,好比我有一個組件ArticleSelect
,我選中的 id 值爲 [ 1,2 ] ,若是不作處理的話,這個組件就不會回顯。僅乾巴巴的顯示 1,2 兩個 tag。可是我能夠經過把選中的值的 options
(好比值爲[{value:1,label:'第一篇'},{value:2,label:'第二篇'}]
) 傳入這個組件,實現回顯顯示標題。
但,可能有人就問了,select 組件遠程搜索 options 不是會隨着搜索的關鍵詞而動態變化麼,爲何這樣能夠?咱們看一下 ElementUI select 組件設置選中值的代碼:
setSelected() {
// 省略不是多選的狀況的代碼
// 多選
let result = [];
if (Array.isArray(this.value)) {
this.value.forEach(value => {
// 注意到這裏是push操做,且getOption是從cachedOptions裏面取的,(cachedOptions是被緩存的,不會由於el-option銷燬而銷燬)
result.push(this.getOption(value));
});
}
this.selected = result;
// 設置完成以後從新計算選項框的高度
this.$nextTick(() => {
this.resetInputHeight();
});
}
複製代碼
由代碼可知, Element 設置 選中的值是一個 push 操做,因此 options 後續改變也不會影響我選中的值,完美解決了個人需求
這裏就更巧妙了,我再把下拉框的代碼結構展現一下:
<el-scrollbar v-show="options.length > 0 && !loading">
<!-- 選項內容 -->
<!-- 看到這個受 showNewOption 控制的option了嘛,他就是用來建立option -->
<el-option :value="query" created v-if="showNewOption"> </el-option>
<slot></slot>
</el-scrollbar>
複製代碼
計算屬性showNewOption
的代碼:
showNewOption() {
let hasExistingOption = this.options.filter(option => !option.created)
.some(option => option.currentLabel === this.query);
// this.query爲空的時候,由於v-if是真正的條件渲染,因此這個el-option組件又被銷燬了,(沒有手動去銷燬哦,是否是很巧妙)
return this.filterable && this.allowCreate && this.query !== '' && !hasExistingOption;
}
複製代碼
Element 的 select 組件有點複雜,可是功能實現,尤爲是代碼結構的構思上真的很是精巧。至於 CSS 部分,非我強項,就不分析了。