拜讀及分析Element源碼-input組件篇

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

首先看生命週期作了什麼

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

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

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

外層DIV綁定的一些class

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

<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接收的變量或者插槽控制樣式及位置偏移,這裏我就先「...」了vue

中文輸入法相關的事件

  • compositionstart
  • compositionupdate
  • compositionend

首先會看到input上綁定了這三個事件(在下孤陋寡聞沒有見過),因而嘗試一下觸發時機java

根據上圖能夠看到

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

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

下面是MDN上的解釋element-ui

相似於 keydown 事件,可是該事件僅在若干可見字符的輸入以前,而這些可見字符的輸入可能須要一連串的鍵盤操做、語音識別或者點擊輸入法的備選詞api

那麼問題來了 爲何要使用這幾個事件呢

由於input組件經常跟form表單一塊兒出現,須要作表單驗證數組

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

咱們但願中文輸入完成之後才驗證服務器

未曾用過的屬性

特指本渣눈.눈

  • $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;
            }
        }
    }
}
複製代碼
相關文章
相關標籤/搜索