拜讀及分析Element原始碼-input元件篇

hollyDysania發表於2018-09-07

element-ui原始碼詳細分析以及在其中可以學到的東西整理。(有問題歡迎指正與討論 也可以來小站逛逛)

拜讀及分析Element原始碼-input元件篇

首先看生命週期做了什麼

    created() {
      // 與select元件相關聯 (若select元件已釋出inputSelect事件則觸發選中)
      this.$on('inputSelect', this.select);
    },

    mounted() {
      // 動態文字域(高度)
      this.resizeTextarea();
      // 前置後置元素偏移(樣式)
      this.updateIconOffset();
    },

    updated() {
      // 檢視重繪完畢後 前置後置偏移(樣式)
      this.$nextTick(this.updateIconOffset);
    }
複製程式碼

外層DIV繫結的一些class

插槽及一些props傳入的引數控制外層樣式

  <div :class="[
    type === 'textarea' ? 'el-textarea' : 'el-input',
    inputSize ? 'el-input--' + inputSize : '',
    {
      'is-disabled': inputDisabled,
      'el-input-group': $slots.prepend || $slots.append,
      'el-input-group--append': $slots.append,
      'el-input-group--prepend': $slots.prepend,
      'el-input--prefix': $slots.prefix || prefixIcon,
      'el-input--suffix': $slots.suffix || suffixIcon || clearable
    }
    ]"
    @mouseenter="hovering = true"
    @mouseleave="hovering = false"
  >
  <!-- 內部被分為
	input結構 與 textarea結構 
  -->
</div>
<!-- 
  動態class
    具名插槽
      $slots.prepend: 前置插槽
      $slots.append: 後置插槽
      $slots.prefix: 前置icon插槽
      $slots.suffix: 後置icon插槽
    不使用插槽的icon
      prefixIcon: 前置icon
      suffixIcon: 後置icon
      clearable: 後置是否清空
 -->
複製程式碼

例項屬性$slots用來訪問被插槽分發的內容

  • vm.$slots.foo 訪問具名插槽foo
  • vm.$slots.default 沒有被包含在具名插槽中的節點

有多個條件 class 時:

  • 可以用陣列結合物件的寫法

內層input結構

  <!-- 輸入框結構 -->
    <template v-if="type !== 'textarea'">
      <!-- 前置元素 -->
      <div class="el-input-group__prepend" v-if="$slots.prepend">
        ...
      </div>
      <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"
      >
      <!-- 前置內容 -->
      <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
        ...
      </span>
      <!-- 後置內容 -->
      <span
        class="el-input__suffix"
        v-if="$slots.suffix || suffixIcon || showClear || validateState && needStatusIcon">
        ...
      </span>
      <!-- 後置元素 -->
      <div class="el-input-group__append" v-if="$slots.append">
        ...
      </div>
    </template>

複製程式碼

前置後置內容及插槽:基本上都是通過props接收的變數或者插槽控制樣式及位置偏移,這裡我就先“...”了

中文輸入法相關的事件

  • compositionstart
  • compositionupdate
  • compositionend

首先會看到input上繫結了這三個事件(在下孤陋寡聞沒有見過),於是嘗試一下觸發時機

拜讀及分析Element原始碼-input元件篇
根據上圖可以看到

  • 輸入到input框觸發input事件
  • 失去焦點後內容有改變觸發change事件
  • 識別到你開始使用中文輸入法觸發**compositionstart **事件
  • 未輸入結束但還在輸入中觸發**compositionupdate **事件
  • 輸入完成(也就是我們回車或者選擇了對應的文字插入到輸入框的時刻)觸發compositionend事件。

查閱資料後發現,這三個事件不僅包括中文輸入法還包括語音識別

下面是MDN上的解釋

類似於 keydown 事件,但是該事件僅在若干可見字元的輸入之前,而這些可見字元的輸入可能需要一連串的鍵盤操作、語音識別或者點選輸入法的備選詞

那麼問題來了 為什麼要使用這幾個事件呢

因為input元件常常跟form表單一起出現,需要做表單驗證

拜讀及分析Element原始碼-input元件篇

為了解決中文輸入法輸入內容時還沒將中文插入到輸入框就驗證的問題

我們希望中文輸入完成以後才驗證

不曾用過的屬性

特指本渣눈.눈

  • $attrs: 獲取到子元件props沒有註冊的,除了style和class以外所有父元件的屬性。(感覺好強!)
  • tabindex: 原生屬性,  元素的 tab 鍵控制次序(具體的自行查閱)
  • **readonly **:原生屬性,只讀。(true時input框不可修改)
  • autoComplete:原生屬性 當使用者在欄位開始鍵入時,瀏覽器基於之前鍵入過的值,是否顯示出在欄位中填寫的選項。
  • aria-label:原生屬性,tab到輸入框時,讀屏軟體就會讀出相應label裡的文字。

內層textarea 結構

  <!-- 文字域結構 -->
    <textarea
      v-else
      :tabindex="tabindex"
      class="el-textarea__inner"
      :value="currentValue"
      @compositionstart="handleComposition"
      @compositionupdate="handleComposition"
      @compositionend="handleComposition"
      @input="handleInput"
      ref="textarea"
      v-bind="$attrs"
      :disabled="inputDisabled"
      :readonly="readonly"
      :style="textareaStyle"
      @focus="handleFocus"
      @blur="handleBlur"
      @change="handleChange"
      :aria-label="label"
    >
    </textarea>

複製程式碼

繫結的事件及屬性與input差不多,區別是textarea動態控制高度的style

textarea 高度自適應

props

  • autosize 自適應高度的配置
  • resize 是否縮放
computed: {
	textareaStyle() {
        // merge 從src/utils/merge.js引入 合併物件的方法
		return merge({}, this.textareaCalcStyle, { resize: this.resize });
	},	
},
methods: {
     resizeTextarea() {
        // 是否執行於伺服器 (伺服器渲染)
        if (this.$isServer) return;
        const { autosize, type } = this;
        if (type !== 'textarea') return;
        if (!autosize) {
          this.textareaCalcStyle = {
            minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
          };
          return;
        }
        const minRows = autosize.minRows;
        const maxRows = autosize.maxRows;

        this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, 		minRows, maxRows);
      }
}
複製程式碼

calcTextareaHeight 是calcTextareaHeight.js裡的方法,計算文字域高度及設定樣式

我就直接貼程式碼和分析的註釋了

let hiddenTextarea;

// 預設的一些樣式
const HIDDEN_STYLE = `
  height:0 !important;
  visibility:hidden !important;
  overflow:hidden !important;
  position:absolute !important;
  z-index:-1000 !important;
  top:0 !important;
  right:0 !important
`;

// 預計要用的一些樣式屬性
const CONTEXT_STYLE = [
  'letter-spacing',
  'line-height',
  'padding-top',
  'padding-bottom',
  'font-family',
  'font-weight',
  'font-size',
  'text-rendering',
  'text-transform',
  'width',
  'text-indent',
  'padding-left',
  'padding-right',
  'border-width',
  'box-sizing'
];

// 獲取到一些需要用到的樣式
function calculateNodeStyling(targetElement) {
  // 獲取最終作用到元素的所有樣式(返回CSSStyleDeclaration物件)
  const style = window.getComputedStyle(targetElement);

  // getPropertyValue為CSSStyleDeclaration原型上的方法獲取到具體的樣式
  const boxSizing = style.getPropertyValue('box-sizing');

  // 上下內邊距
  const paddingSize = (
    parseFloat(style.getPropertyValue('padding-bottom')) +
    parseFloat(style.getPropertyValue('padding-top'))
  );

  // 上下邊框寬度
  const borderSize = (
    parseFloat(style.getPropertyValue('border-bottom-width')) +
    parseFloat(style.getPropertyValue('border-top-width'))
  );

  // 取出預計要用的屬性名和值,以分號拼接成字串
  const contextStyle = CONTEXT_STYLE
    .map(name => `${name}:${style.getPropertyValue(name)}`)
    .join(';');

  // 返回預設要用的樣式字串,上下內邊距和, 邊框和, boxSizing屬性值
  return { contextStyle, paddingSize, borderSize, boxSizing };
}

export default function calcTextareaHeight(
  targetElement,
  minRows = 1,
  maxRows = null
) {
  // hiddenTextarea不存在則建立textarea元素append到body中
  if (!hiddenTextarea) {
    hiddenTextarea = document.createElement('textarea');
    document.body.appendChild(hiddenTextarea);
  }
  // 取出以下屬性值
  let {
    paddingSize,
    borderSize,
    boxSizing,
    contextStyle
  } = calculateNodeStyling(targetElement);

  // 給建立的hiddenTextarea新增行內樣式並賦值value或palceholder,無則''
  hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
  hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';

  // 獲取元素自身高度
  let height = hiddenTextarea.scrollHeight;
  const result = {};

  // boxSizing不同 高度計算不同
  if (boxSizing === 'border-box') {
    // border-box:高度 = 元素自身高度 + 上下邊框寬度和
    height = height + borderSize;
  } else if (boxSizing === 'content-box') {
    // content-box: 高度 = 高度 - 上下內邊距和
    height = height - paddingSize;
  }

  hiddenTextarea.value = '';
  // 單行文字的高度
  let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;

  // minRows最小行存在
  if (minRows !== null) {
    // 最小高度 = 單行高度 * 行數
    let minHeight = singleRowHeight * minRows;
    if (boxSizing === 'border-box') {
      // border-box則加上內邊距及邊框
      minHeight = minHeight + paddingSize + borderSize;
    }
    // minHeight與height取最大值給height賦值
    height = Math.max(minHeight, height);
    result.minHeight = `${ minHeight }px`;
  }
  // 最大行存在
  if (maxRows !== null) {
    // 邏輯同上
    let maxHeight = singleRowHeight * maxRows;
    if (boxSizing === 'border-box') {
      maxHeight = maxHeight + paddingSize + borderSize;
    }
    // maxHeight與height取最小值給height賦值
    height = Math.min(maxHeight, height);
  }
  result.height = `${ height }px`;
  // 計算完成後移除hiddenTextarea元素
  hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
  hiddenTextarea = null;

  // 暴露包含minHeight及height的物件
  return result;
};
複製程式碼

需要注意的一些點

form元件中巢狀input元件時樣式也會受form一些注入屬性的控制。

 // 接收form元件注入的屬性
    inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    }
複製程式碼
  • size(input的大小)

  • this.elFormItem.validateState: 與表單驗證關聯 ,控制表單驗證時icon的樣式(紅x之類的)

computed: {
    // 表單驗證相關
    validateState() {
    	return this.elFormItem ? this.elFormItem.validateState : '';
    },
    needStatusIcon() {
    	return this.elForm ? this.elForm.statusIcon : false;
    },
    // 表單驗證樣式
    validateIcon() {
        return {
        validating: 'el-icon-loading',
        success: 'el-icon-circle-check',
        error: 'el-icon-circle-close'
        }[this.validateState];
    }
}
複製程式碼

props的validateEvent屬性:時間選擇器會傳入false其他預設true (意思大概true是需要做校驗),以下是用到validateEvent的methods


      handleBlur(event) {
        this.focused = false;
        // 暴露blur事件
        this.$emit('blur', event);
        if (this.validateEvent) {
          // 向上找到ElFormItem元件釋出el.form.blur事件並傳值
          this.dispatch('ElFormItem', 'el.form.blur', [this.currentValue]);
        }
      },
      setCurrentValue(value) {
        // 還在輸入並且內容與之前內容相同 return
        if (this.isOnComposition && value === this.valueBeforeComposition) return;
        // input內容賦值
        this.currentValue = value;
        // 還在輸入return
        if (this.isOnComposition) return;
        this.$nextTick(_ => {
          this.resizeTextarea();
        });
        // 除了時間選擇器其他元件中使用預設為true
        if (this.validateEvent) {
          // mixin中的方法 意思是向上找到ElFormItem元件釋出el.form.change事件並傳遞當前input內容
          this.dispatch('ElFormItem', 'el.form.change', [value]);
        }
      }
複製程式碼

dispatch這個方法開始我以為是觸發vuex的方法結果是mixin裡的

路徑: src/mixins/emitter.js

// 接收元件名,事件名,引數
dispatch(componentName, eventName, params) {
    var parent = this.$parent || this.$root;
    var name = parent.$options.componentName;

    // 尋找父級,如果父級不是符合的元件名,則迴圈向上查詢
    while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        if (parent) {
            name = parent.$options.componentName;
        }
    }
    // 找到符合元件名稱的父級後,釋出傳入事件。
    if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
    }
}
複製程式碼

匯入的Migrating

迭代api友好提示 方便由於用了移除的api報錯 找出問題在哪 參見methos中getMigratingConfig事件及**src/mixins/migrating.js **

疑問

// 判斷韓文的方法(不清楚為什麼)
import { isKorean } from 'element-ui/src/utils/shared';

methods: {
    // 中文或語音輸入開始 中 後 觸發詳見↑
    handleComposition(event) {
        // 完成輸入時
        if (event.type === 'compositionend') {
            // 輸入中標識為false
            this.isOnComposition = false;
            // 中文或語音輸入前的值賦值給當前
            this.currentValue = this.valueBeforeComposition;
            // 清空之前的值
            this.valueBeforeComposition = null;
            // 賦值並且向父元件暴露input方法
            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;
            }
        }
    }
}
複製程式碼

相關文章