Element原始碼分析系列5-Input(輸入框)

超級索尼發表於2019-03-03

簡介

本來不打算寫輸入框的分析,心想一個輸入框能有多複雜,還能怎麼封裝,後來瀏覽了下原始碼,發現還是有很多自己不知道的知識點,於是打算還是寫,下圖就是一個Element的最基本的輸入框

Element原始碼分析系列5-Input(輸入框)
結果一看原始碼,我的鬼鬼,原始碼竟然300多行!咋會這麼複雜,看過官網的文件後,發現確實應該這麼複雜,因為這個輸入框不僅僅是隻有一個input這麼簡單,還附帶了很多的其他內容,上圖僅是一個最基本的形式而已,下面我們依次分析,官網原始碼點此
本來打算貼出全部原始碼,但是發現這樣篇幅太長,因此我們只分析重點,分析部分原始碼

輸入框原始碼html結構

首先還是先要搞懂Element封裝後的input的html結構才行,下面是簡化後的html結構

<template>
  <div ...>
    <template v-if="type !== 'textarea'">
      <!-- 前置元素 -->
      <div class="el-input-group__prepend" v-if="$slots.prepend">
        <slot name="prepend"></slot>
      </div>
      
      <!--主體input-->
      <input ...>
      
      <!-- 前置內容 -->
      <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
       ...
      </span>
      <!-- 後置內容 -->
      <span
        ...
      </span>
      <!-- 後置元素 -->
      <div class="el-input-group__append" v-if="$slots.append">
       ...
      </div>
    </template>
    <textarea v-else>
    </textarea>
  </div>
</template>
複製程式碼

是不是看著很頭大?其實很簡單,最外層一個div作為wrapper包裹裡面的元素,然後裡面是template標籤(template實際不會渲染出來)的v-if,最下面是textarea的v-else,說明type這個選項控制輸入框元件是顯示input還是textarea,對於v-else就一個textarea,沒啥可說的,關鍵在於前面的v-if,仔細看這個結構,是由前置元素,主體input,前置內容,後置內容,後置元素這幾部分構成,那麼它們分別代表啥呢?下圖就是答案

Element原始碼分析系列5-Input(輸入框)
圖中中間的是input輸入框,前後2個都是輔助性的內容,這2個就是前後置元素,而輸入框內的搜尋和日期Icon就是前後置內容,因此要封裝這麼個完整的input,程式碼量確實比較多

這裡值得注意的是前後置元素和input主體的佈局,修改前後置元素內容可以發現,中間input的寬度是自適應的,如下圖

Element原始碼分析系列5-Input(輸入框)
中間input自動變窄,那麼這哥佈局是咋回事呢,這哥佈局類似於左列寬度不定,右列自適應,左列不定的意思是寬度由內容撐開來,檢視css程式碼得知,這是table-cell佈局,我們知道table內表格寬度都是自適應的,某一列很寬的話,另外的列就會變窄,因此這個思想可以用到這裡來,下面就是示例佈局(左列寬度不定,右列自適應),注意外層容器設定display:table

<div style="display:table" class='wrapper'>
    <div style="display:table-cell" class='left'>
    </div>
    <div style="display:table-cell" class='right'>
    </div>
</div>
複製程式碼

這個佈局用flex也可以實現,具體就是left元素不設定寬度,right元素設定flex:1即可,下面看下輸入框的css

Element原始碼分析系列5-Input(輸入框)
輸入框其實是有左右padding的,為了更美觀,這裡不是用text-indent來控制游標位置

Element原始碼分析系列5-Input(輸入框)
可以看出-webkit-appearance:none,outline:none這些用法在和各個元件內都很普遍,目的就是去掉瀏覽器自己渲染出的樣式,統一規定樣式。這裡的transition居然使用了貝塞爾曲線進行過渡,話說過渡時間才0.2秒,使用貝塞爾曲線能看出來麼?直接ease應該也可以啊!

禁用狀態的實現

禁用很簡單,通過使用者傳入的disabled屬性來控制,如下程式碼

<el-input
  placeholder="請輸入內容"
  v-model="input1"
  :disabled="true">
</el-input>
複製程式碼

原始碼裡通過<input :disabled="inputDisabled" ...>來控制input的功能禁用,這個inputDisabled是個計算屬性

inputDisabled() {
        return this.disabled || (this.elForm || {}).disabled;
      },
複製程式碼

這裡因為要判斷如果input被包含在表單內,如果表單禁用,那麼自然自己也就被禁用了。輸入框樣式上的禁用是由最外層的div的class控制的

<div :class=[{'is-disabled': inputDisabled}...]>...</div>
複製程式碼

這裡沒有放在裡面的input上進行控制,原因是放在最外層可以統一控制裡面的textarea和input,減少程式碼冗餘,通過子選擇器選擇到input和textarea進行控制,這裡placeholder的顏色也是可以控制的,但要注意相容性

&::placeholder {
        color: $--input-disabled-placeholder-color;
      }
複製程式碼

input元素的屬性

通過檢視元件裡原生input的屬性,瞭解了很多知識點

<input
        :tabindex="tabindex"
        v-if="type !== 'textarea'"
        class="el-input__inner"
        v-bind="$attrs"
        :type="type"
        :disabled="inputDisabled"
        :readonly="readonly"
        :autocomplete="autoComplete"
        :value="currentValue"
        ref="input"
        @compositionstart="handleComposition"
        @compositionupdate="handleComposition"
        @compositionend="handleComposition"
        @input="handleInput"
        @focus="handleFocus"
        @blur="handleBlur"
        @change="handleChange"
        :aria-label="label"
      >
複製程式碼

哇,居然這麼多屬性和方法~~這就是一個成熟元件需要實現的東西,先看tabindex,就是控制tab鍵按下後的訪問順序,由使用者傳入tabindex如果設定為負數則無法通過tab鍵訪問,設定為0則是在最後訪問。然後v-if="type !== 'textarea'"控制了這個input的渲染與否,使用者傳入type屬性進行控制,然後是input的類el-input__inner,前面介紹過,然後是v-bind="$attrs"這句話,這句話是幹嘛的?翻開官網得知

Element原始碼分析系列5-Input(輸入框)
讀起來很拗口,下面用個例子說明

<el-input maxlength="5" minlength="2">
</el-input>
複製程式碼

這裡我們給<el-input>元件新增了2個原生屬性,注意這2個原生屬性並沒有在prop裡面,這2個屬性是控制input的最大輸入和最小輸入長度的,那麼這2個屬性現在僅僅放在了父元素<el-input>上,如何將其傳遞給素<el-input>內的原生input子元素呢?不傳遞則這2個屬性不起作用,因為子input上沒有這2個屬性。答案就是通過v-bind="$attrs"來實現,它將父元素所有非prop的特性都繫結在了子元素input上,否則你還得在props裡宣告maxlength,minlength,程式碼量增大。這就是$attrs的優勢所在
往下看:readonly="readonly" :autocomplete="autoComplete",這2個屬性都是原生的屬性,由使用者傳入,控制輸入框只讀和是否自動補全,然後是輸入框的value:value="currentValue"這裡的currentValue是在data裡面

currentValue: this.value === undefined || this.value === null
          ? ''
          : this.value,
複製程式碼

如果使用者沒有在<el-input>上寫v-model(v-model原理參考官網),那麼就沒有傳入value,所以currentValue就是空字串,否則就是傳入的值,接著ref="input"一句,ref用來給元素或子元件註冊引用資訊。引用資訊將會註冊在父元件的 $refs 物件上,這是為了方便後續程式碼直接拿到原生input的dom

然後是這3句話

@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
複製程式碼

這可不能小瞧,這3個方法是原生的方法,這裡簡單介紹下,官方定義如下compositionstart 事件觸發於一段文字的輸入之前(類似於 keydown 事件,但是該事件僅在若干可見字元的輸入之前,而這些可見字元的輸入可能需要一連串的鍵盤操作、語音識別或者點選輸入法的備選詞) 簡單來說就是切換中文輸入法時在打拼音時(此時input內還沒有填入真正的內容),會首先觸發compositionstart,然後每打一個拼音字母,觸發compositionupdate,最後將輸入好的中文填入input中時觸發compositionend。觸發compositionstart時,文字框會填入 “虛擬文字”(待確認文字),同時觸發input事件;在觸發compositionend時,就是填入實際內容後(已確認文字),所以這裡如果不想觸發input事件的話就得設定一個bool變數來控制

Element原始碼分析系列5-Input(輸入框)
上圖中點選空格後才會填入實際的文字,輸入英文或數字則沒有這3個事件的觸發

那麼問題來了,為啥Element要設定這3個事件的處理函式呢?原因很簡單,我們肯定不希望在輸入拼音的過程中就直接觸發input事件改變<el-input v-model="inputValue"></el-input>中inputValue的值,而是希望輸入完成後再改變,所以需要特殊處理,我們來看handleComposition的原始碼,注意這裡只寫了一個方法而不是3個,通過event.type來判斷事件型別從而簡化程式碼,可以借鑑

handleComposition(event) {
        if (event.type === 'compositionend') {
          this.isOnComposition = false;
          this.currentValue = this.valueBeforeComposition;
          this.valueBeforeComposition = null;
          this.handleInput(event);
        } else {
          const text = event.target.value;
          const lastCharacter = text[text.length - 1] || '';
          this.isOnComposition = !isKorean(lastCharacter);
          if (this.isOnComposition && event.type === 'compositionstart') {
            this.valueBeforeComposition = text;
          }
    }
},
複製程式碼

這裡首先在data中定義了一個bool變數isOnComposition,這個變數就是用來判斷是否在打拼音的過程中,初始為false,當開始打拼音後,觸發compositionstart事件,更新isOnComposition,通過this.isOnComposition = !isKorean(lastCharacter)來更新,這裡的邏輯是判斷輸入的字元的最後一個是不是韓文,韓文通過正規表示式來判斷,至於為啥要判斷韓文的最後一個字元,不清楚~ 如果是中文,則isOnComposition為true,這裡比較難理解的是後面這個if,當正在打拼音的過程中且是compositionstart事件時,則用一個valueBeforeComposition變數儲存當前的文字,也就是儲存此次打字前input中的文字內容,這個valueBeforeComposition的作用後面介紹,接下來看if (event.type === 'compositionend')中的內容,當打完拼音後,觸發compositionend,此時設定isOnComposition為false表明打字完成,然後注意這裡會手動觸發一個this.handleInput(event)(handleInput就是input上繫結的v-on:input),這是因為最後輸入完成時,compositionend會在input事件後觸發,此時isOnComposition還是true,無法觸發下面handleInput中的emit將新的input的value傳遞給父元件,所以這裡需要手動呼叫一次handleInput,這裡請仔細理解!

handleInput(event) {
        const value = event.target.value;
        this.setCurrentValue(value);
        if (this.isOnComposition) return;
        this.$emit('input', value);
      },
複製程式碼

handleInput中當isOnComposition為true時表明正在打拼音輸入,則不觸發emit事件,這是合理且正常的

可清空的實現

<el-input>中如果新增了clearable屬性則輸入文字後會出現一個叉的圖示,點選後input內容清空,如下圖

Element原始碼分析系列5-Input(輸入框)
先看html結構,下面是後置內容的html程式碼

<!-- 後置內容 -->
      <span
        class="el-input__suffix"
        v-if="$slots.suffix || suffixIcon || showClear || validateState && needStatusIcon">
        <span class="el-input__suffix-inner">
          <template v-if="!showClear">
            <slot name="suffix"></slot>
            <i class="el-input__icon"
              v-if="suffixIcon"
              :class="suffixIcon">
            </i>
          </template>
          
          
         <i v-else
            class="el-input__icon el-icon-circle-close el-input__clear"
            @click="clear"
          ></i>
          
          
        </span>
        <i class="el-input__icon"
          v-if="validateState"
          :class="['el-input__validateIcon', validateIcon]">
        </i>
      </span>
複製程式碼

中間這段<i>就是清空按鈕,它是一個i標籤,有一個click事件,前面通過showClear來判斷是否需要顯示清空按鈕,邏輯如下

showClear() {
        return this.clearable &&
          !this.disabled &&
          !this.readonly &&
          this.currentValue !== '' &&
          (this.focused || this.hovering);
      }
複製程式碼

這個計算屬性第一步得看使用者是否新增了顯示清空按鈕的屬性,如果沒有則不顯示,如果有則繼續判斷,在非禁用且非只讀狀態下才且當前input的value不是空且該input獲得焦點或者滑鼠移動上去才顯示,條件略多啊
然後看clear清空這個方法

clear() {
        this.$emit('input', '');
        this.$emit('change', '');
        this.$emit('clear');
        this.setCurrentValue('');
        this.focus();
      }
複製程式碼

居然有5句話,但都不能少,第一個emit是通知父元件自己的value值變成了空,從而更新<el-input v-model="v">中的v這個data為空,第二句emit觸發了父元件的change事件,這樣在<el-input v-model="v" @change="inputChange">中的inputChange中就能監聽到該事件了,第3個emit觸發父元件的@clear方法,讓父元件知道自己已經清空了,第四句話更新自己的currentValue為空,第五局讓input獲得焦點便於輸入內容

textarea高度自適應的實現

這個就比較難了,這裡只簡單分析其原理,原生的textarea隨著內容增多則會出現滾動條

Element原始碼分析系列5-Input(輸入框)
而Element的處理卻能夠讓其自適應高度,也就是不出現滾動條

Element原始碼分析系列5-Input(輸入框)
核心原理是在textarea的input事件中進行邏輯判斷,每觸發一次input就判斷一次,具體在下面函式中進行處理

function calcTextareaHeight(){
  ...  
  let height = hiddenTextarea.scrollHeight;
  const result = {};
  ...
  result.height = `${ height }px`;
  return result
}
複製程式碼

這裡讓height等於scrollHeight,也就是滾動條捲去的高度,這裡就將height變大了,然後返回該height並繫結到input的style中從而動態改變textarea的height,具體程式碼很複雜,還要處理最大最小高度等,參考github

相關文章