【源码浅析】 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");markdown

做为一个才入行一年多的前端,真没用过 broadcast ,具体详见Vue$dispatch 和 $broadcast 详解ide

虽然 broadcast 有它的缺点,好比基于组件树结构的事件流方式实在是让人难以理解,没有解决兄弟组件间的通讯问题。可是在父子层嵌套组件中,经过 $dispatch 和 $broadcast 定向的向某个父或者子组件远程调用事件,避免了经过传 props 或者使用 refs 调用组件实例方法的操做,仍是很简洁的。oop

ElSelect 和 ElOption

若是我写这个组件,可能会这么使用 <my-select v-model="input" options="options" />,即 Select 组件的选项经过 options 传入组件。也许最后功能能够实现,可是这样作感受有点封装过分了。从这个组件的结构来说,和原生的select不太像。咱们常常这样使用 Select 组件,是否是很直观:源码分析

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

参考文章

相关文章
相关标签/搜索