【源碼淺析】 ElementUI 的 Select 組件

前言

本文從select組件的代碼結構入手,瞭解select組件如何組織父子組件通訊,並重點分析了ElSelect組件 和 ElOption組件實現的巧妙之處。並介紹select組件的四個功能是如何實現的。html

代碼結構

Element 使用 div 模擬了 select,select 組件含有navigation-mixin.jsoption-group.vueoption.vueselect-dropdown.vueselect.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 和 $broadcastelement-ui

inject 和 provide

Vue2.2.0 新增 API,這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。一言而蔽之:祖先組件中經過 provider 來提供變量,而後在子孫組件中經過 inject 來注入變量。api

後面我會分析 ElementUI 如何使用的 inject 和 provide,以及爲何不能使用 this.$parentthis.$children來互相通訊緩存

dispatch 和 broadcast

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

ElSelect 和 ElOption

若是我寫這個組件,可能會這麼使用 <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-optioncreated的生命週期:

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-optionbeforeDestroy 生命週期

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爲子組件或爲孫組件的兩種狀況,使用injectprovide就很合適。

本地搜索

實際上是本地篩選功能,所以啓用本地搜索功能須要傳入 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);
  }
}
複製代碼

select 遠程搜索組件回顯

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 部分,非我強項,就不分析了。

參考文章

相關文章
相關標籤/搜索