超詳細 ElementUI 源碼分析 —— Input

最近在學習 Vue 框架,想深刻了解一下組件化開發以及封裝組件庫的思想,而 ElementUI 做爲這方面作的最好的也是最經常使用的組件庫,它的源碼必定有不少值得咱們去學習的地方,因此去 ElementUI 官網Github(來都來了,給個 star) 看了一下,決定邊學習組件庫邊看源碼,並把學習中的收穫和疑問記錄下來,預計會須要很長很長時間才能看完,可是爲了提升自(gong)己(zi),仍是靜下心來慢慢看吧。html

因爲本渣的 webpack 只會用一點點,尚未深刻學習過,因此就先不看源碼的打包、生成和引入了(是真不會),後面有時間會學習的。本系列是針對 UI 組件作的分析,源碼全部的註釋都會上傳到 Github歡迎 star」(愛您❤️)。vue

Input 輸入框

分析一個組件,首先須要知道它的功能是什麼?在哪裏使用的?node

  • input 的功能就是收集用戶輸入的數據傳給後臺程序,常見的有文本、密碼、文本域(只考慮輸入文字的)
  • 通常會放在表單中,配合其餘表單項一塊兒使用

那麼一開始,咱們就須要瞭解一下它的基本結構:react

基本結構

<template>
  <div>
    <!-- 正常 input 輸入框 -->
    <template>
      <!-- 前置元素,通常是放置標籤或者下拉框 -->
      <div></div>
      <!-- 主體 input -->
      <input />
      <!-- 前置內容,通常是圖標 -->
      <span></span>
      <!-- 後置內容,通常是圖標 -->
      <span></span>
      <!-- 後置元素,如 .com 或者搜索按鈕 -->
      <div></div>
    </template>
    
    <!-- 文本域 -->
    <textarea></textarea>
  </div>
</template>
複製代碼

在 input 框裏分別對應下面圖中的各個元素,能夠看出在基本結構中,「放元素」的用div標籤包裹,「放置圖標」的用span包裹。webpack

)

代碼分析

接下來看具體代碼:git

<!-- 非多行文本框 -->
<template v-if="type !== 'textarea'">
  <!-- 前置元素 -->
  <!-- 若是傳遞了 prepend 插槽就顯示,並把傳進來的模板或者字符串渲染到 slot 中 -->
  <div class="el-input-group__prepend" v-if="$slots.prepend">
    <slot name="prepend"></slot>
  </div>
  <!-- input 屬性稍後分析 -->
  <input />
  <!-- 前置內容 -->
  <!-- 支持經過 slot 和 prefi-icon 傳值 -->
  <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
    <!-- 當沒有傳遞插槽時,這個是不會渲染的 -->
    <slot name="prefix"></slot>
    <i class="el-input__icon" v-if="prefixIcon" :class="prefixIcon"> </i>
  </span>
  <!-- 後置內容 -->
  <span class="el-input__suffix" v-if="getSuffixVisible()">
    <span class="el-input__suffix-inner">
      <!-- 該模板渲染的是後置圖標 -->
      <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
        <slot name="suffix"></slot>
        <i class="el-input__icon" v-if="suffixIcon" :class="suffixIcon">
        </i>
      </template>
      <!-- 清空按鈕 -->
      <i v-if="showClear" class="el-input__icon el-icon-circle-close el-input__clear" @mousedown.prevent @click="clear" ></i>
      <!-- 顯示密碼按鈕 -->
      <i v-if="showPwdVisible" class="el-input__icon el-icon-view el-input__clear" @click="handlePasswordVisible" ></i>
      <!-- 輸入長度限制 -->
      <span v-if="isWordLimitVisible" class="el-input__count">
        <span class="el-input__count-inner">
          {{ textLength }}/{{ upperLimit }}
        </span>
      </span>
    </span>
    <i class="el-input__icon" v-if="validateState" :class="['el-input__validateIcon', validateIcon]" >
    </i>
  </span>
  <!-- 後置元素 -->
  <div class="el-input-group__append" v-if="$slots.append">
    <slot name="append"></slot>
  </div>
</template>
複製代碼

首先是 getSuffixVisible 方法,用來判斷後置內容是否顯示,包括圖標、清空按鈕、顯示密碼按鈕、輸入長度限制字符。es6

getSuffixVisible () {
  return (
    this.$slots.suffix ||
    this.suffixIcon ||
    this.showClear ||
    this.showPassword ||
    this.isWordLimitVisible ||
    (this.validateState && this.needStatusIcon) // 這個主要和表單校驗有關
  )
}
複製代碼

textLength 和 upperLimit,這兩個都是計算屬性,前者表示輸入框輸入的字符長度,後者是限制長度。github

textLength () {
  // 若是是數字,先轉換成字符串再求長度
  if (typeof this.value === 'number') {
    return String(this.value).length
  }
  return (this.value || '').length
},
upperLimit () {
  // 獲取傳遞的原生屬性 maxlength
  return this.$attrs.maxlength
}
複製代碼

關於vm.$attrs在 Vue 官網有介紹,這裏解釋一下,就是將父組件的屬性(除去在 props 中傳入的屬性)傳遞給子組件。像 input 的原生屬性特別多,若是全部的都經過子組件 props 來傳遞,代碼會顯得很冗餘。web

showClear 等這些計算屬性考慮了不少種狀況:面試

  • 是否傳遞了 clearable
  • 是否被禁用了
  • 是否只讀
  • 是否聚焦或者 hover 狀態

input 屬性

再回過頭來分析 input

:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:type="showPassword ? (passwordVisible ? 'text' : 'password') : type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
ref="input"
複製代碼
  • tabindex表示使用 tab 鍵切換聚焦的順序,有三個值:
    • -1,表示用 tab 鍵不能聚焦,可是可使用 JS 獲取
    • 0,表示能夠經過 tab 鍵獲取焦點
    • 正值,表示能夠經過 tab 鍵獲取焦點,切換的順序是tabindex數值由小到大的順序,若是多個元素相同,則是經過 DOM 中的順序來決定的
  • v-bind="$attrs"將父組件的非 props 屬性傳遞給子組件 input
  • type輸入框類型,有 text 和 password
  • disabled是否禁用
  • readonly是否只讀
  • autocomplete是否打開輸入框提示
  • ref註冊元素或子組件引用信息
    • 普通 DOM 元素上使用,引用指向的是 DOM 元素
    • 子組件上使用,引用指向這個子組件實例
    • 經過this.$refs.input能夠訪問到子組件

input 事件

@compositionstart="handleCompositionStart"
@compositionupdate="handleCompositionUpdate"
@compositionend="handleCompositionEnd"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
複製代碼
  • compositionstart文字輸入以前觸發
  • compositionupdate輸入過程當中每次敲擊鍵盤觸發
  • compositionend選擇字詞完成時觸發

註冊這三個事件的緣由在於實現中文輸入法下,僅在選詞後觸發 input 事件。因爲在輸入拼音的時輸入框不是當即得到輸入的值,而是要確實後才能獲取到。

觸發compositionstart時,文本框會填入待確認文本,同時觸發 input 事件;在觸發compositionend時,就是填入已確認的文本,因此這裏若是不想觸發 input 事件的話就得設置一個變量來控制。

MDN 關於 Composition 事件的介紹

handleCompositionStart () {
  // 正在輸入
  this.isComposing = true
},
handleCompositionUpdate (event) {
  // 獲取敲擊鍵盤的值
  const text = event.target.value
  // 獲取最後一個輸入的字符
  const lastCharacter = text[text.length - 1] || ''
  // 這個和韓文有關,暫時沒搞清楚是啥意思
  this.isComposing = !isKorean(lastCharacter)
},
handleCompositionEnd (event) {
  // 若是輸入結束並選擇了字詞,就觸發 input 事件
  if (this.isComposing) {
    this.isComposing = false
    this.handleInput(event)
  }
},
handleInput (event) {
  // 若是正在輸入就不觸發 input 事件
  if (this.isComposing) return
  // 沒懂這個 nativeInputValue 是啥意思
  if (event.target.value === this.nativeInputValue) return
  // 通知父組件觸發 input 方法
  this.$emit('input', event.target.value)
  
  this.$nextTick(this.setNativeInputValue)
}
複製代碼

關於 input 身上的東西總算沒有了,textarea 和 input 區別不大,這裏就不贅敘了,關於文本域的自適應高度後面會講。

代碼邏輯

如今將重點關注在script標籤上,從源碼能夠看出有 300 多行代碼都是關於 input 邏輯的。

頭部import

import emitter from 'element-ui/src/mixins/emitter'
import Migrating from 'element-ui/src/mixins/migrating'
import calcTextareaHeight from './calcTextareaHeight'
import merge from 'element-ui/src/utils/merge'
import { isKorean } from 'element-ui/src/utils/shared'
複製代碼

這裏面導入的都是一些混入和工具類函數,首先在srcmixins裏面找到emitter,咱們看一下它的源碼

/** * 廣播,就是父組件向後代組件廣播事件 * 經過不斷遞歸子組件,觸發所需組件的對應事件 * @param {*} componentName 目標組件名稱 * @param {*} eventName 要觸發的事件名 * @param {*} params 參數 */
function broadcast (componentName, eventName, params) {
  // 遍歷當前組件實例的全部子組件
  this.$children.forEach(child => {
    // 拿到子組件名稱
    var name = child.$options.componentName
    // 若是當前子組件就是目標組件
    if (name === componentName) {
      // 通知子組件觸發對應事件
      child.$emit.apply(child, [eventName].concat(params))
    } else {
      // 遞歸遍歷深層子組件
      broadcast.apply(child, [componentName, eventName].concat([params]))
    }
  })
}
export default {
  methods: {
    // 派發,就是子組件向父組件派發事件
    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))
      }
    },
    broadcast (componentName, eventName, params) {
      // 把 this 指向調用它的組件實例身上
      broadcast.call(this, componentName, eventName, params)
    }
  }
}
複製代碼

至於爲何要定義這樣一個混入文件,咱們能夠經過它的調用來了解。

handleBlur (event) {
  this.focused = false
  this.$emit('blur', event)
  if (this.validateEvent) {
    this.dispatch('ElFormItem', 'el.form.blur', [this.value])
  }
}
複製代碼

當 input 失去焦點時,經過官方的校驗工具對輸入框進行校驗,這裏就涉及到了深層次的父子組件通訊,經過dispatch可以指定ElFormItem觸發el.form.blur事件,而沒必要一層一層的向上傳遞。

因此定義dispatchbroadcast方法是爲了解決有嵌套關係的父子通訊問題,定向的向某個父或者子組件「遠程調用」事件,這樣就避免了經過傳 props 或者使用 refs 調用組件實例方法的操做。

這裏有必要提早說明一下混入,官網關於混入的介紹可自行查看,一句話歸納就是組件公共的方法被提取出來,須要用到的時候就經過混入方式將方法或者生命週期函數添加到須要用到的組件中。

對於$broadcast$dispatch詳細參考了掘金的這篇文章,感謝做者,你們有興趣能夠去點個贊!

接下來看第二個混入migrating,直接上源碼

export default {
  // 首先是混入生命週期函數 mounted,該函數會在組件自身 mounted 調用以前調用
  mounted () {
    // 若是是生產環境直接返回,由於在實際上線後是不建議使用 console 的
    if (process.env.NODE_ENV === 'production') return
    // 若是該組件不是一個虛擬 DOM 節點直接返回,由於你都渲染成真實 DOM 了還警告啥
    if (!this.$vnode) return
    // 解構賦值瞭解一下
    const { props = {}, events = {} } = this.getMigratingConfig()
    const { data, componentOptions } = this.$vnode
    // data 中的屬性,不要問爲何是 attrs,本身去看抽象語法樹
    const definedProps = data.attrs || {}
    // listeners 包含了組件的事件監聽器
    const definedEvents = componentOptions.listeners || {}

    // for/in 循環遍歷定義的屬性
    for (let propName in definedProps) {
      // 把駝峯命名的屬性改成以 - 鏈接的形式
      propName = kebabCase(propName)
      // 若是在 data 中定義了 props 中的屬性,控制檯會警告
      if (props[propName]) {
        console.warn(
          `[Element Migrating][${this.$options.name}][Attribute]: ${props[propName]}`
        )
      }
    }
    // 這個不解釋了,頭大
    for (let eventName in definedEvents) {
      eventName = kebabCase(eventName) // compatible with camel case
      if (events[eventName]) {
        console.warn(
          `[Element Migrating][${this.$options.name}][Event]: ${events[eventName]}`
        )
      }
    }
  },
  methods: {
    getMigratingConfig () {
      return {
        props: {},
        events: {}
      }
    }
  }
}
複製代碼

在源碼的註釋上明確指出了該如何使用該混入,這裏直接看在 input 組如何使用的。

getMigratingConfig () {
  return {
    props: {
      icon: 'icon is removed, use suffix-icon / prefix-icon instead.',
      'on-icon-click': 'on-icon-click is removed.'
    },
    events: {
      click: 'click is removed.'
    }
  }
}
複製代碼

這個方法的做用就是:若是咱們在el-input中添加一個icon屬性,就會出現警告,而且icon屬性沒有生效,一樣的,click事件也同樣。

<el-input icon="el-icon-date"></el-input>
複製代碼

這裏順便提一下kebabCase方法,解釋一下這個正則表達式,()表示的是正則裏面的分組,整個正則表達式匹配的是第一組爲不包含-^放在[]裏表示取反)和第二組包含大寫字母的字符串。經過字符串替換方法,將目標字符串用$1-$2的格式替換,$1$2表明的是正則表達式的分組,$1表示的是第一個小括號匹配到的內容,$2表示的是第二個小括號匹配到的字符串。最後將字符串所有轉換成小寫,這裏有一點不明白的就是爲何使用兩次replace方法,我試了不少個字符串,調用一次都可以獲得正確的結果。

好比將suffixIcon當成參數傳進去,獲得的就是suffix-icon

/** * 將小駝峯命名的字符串轉換成以 - 鏈接的字符串 * @param {*} str 須要轉換的字符串 */
export const kebabCase = function(str) {
  const hyphenateRE = /([^-])([A-Z])/g;
  return str
    .replace(hyphenateRE, '$1-$2')
    .replace(hyphenateRE, '$1-$2')
    .toLowerCase();
};
複製代碼

有關 VNode 結構能夠去 Vue 的源代碼中查看,若是你想了解的更多可能要去看一下虛擬 DOM 的原理了,因爲本渣尚未看,就很少講了。

接下來就是calcTextareaHeight方法了,首先咱們要知道爲何要定義這個方法?

在 ElementUI 官方文檔上有指出經過設置autosize屬性可使得文本域的高度可以根據文本內容自動進行調整,而且autosize還能夠設定爲一個對象,指定最小行數和最大行數。

因此這個方法固然是用來動態計算文本域的高度的,再來看在哪裏調用的:

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

  // 當傳入的是布爾值時,maxRows 就是 null,即沒有最大高度限制
  // 傳入對象時,就會出現最小高度和最大高度
  this.textareaCalcStyle = calcTextareaHeight(
    this.$refs.textarea,
    minRows,
    maxRows
  )
}
複製代碼

resizeTextarea這個方法是用來改變文本域的大小的,初次渲染時會調用該方法,當咱們改變輸入框的值時會屢次觸發該方法。

// 先提早說明,該變量是爲了計算 textarea 的高度而存在的
// 至於爲何要定義一個這樣的變量,看完後面就明白了
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) {
  // 拿到目標元素真實的 style 數據(計算後的)
  const style = window.getComputedStyle(targetElement)

  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(';')

  return { contextStyle, paddingSize, borderSize, boxSizing }
}

/** * 動態計算 textarea 的高度 * @param {*} targetElement 須要計算高度的目標元素 * @param {*} minRows 最小行,默認 1 * @param {*} maxRows 最大行,默認 null */
export default function calcTextareaHeight ( targetElement, minRows = 1, maxRows = null ) {
  // 若是 textarea 不存在就建立一個
  if (!hiddenTextarea) {
    hiddenTextarea = document.createElement('textarea')
    document.body.appendChild(hiddenTextarea)
  }

  // 與數組的解構賦值不一樣,對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值
  let {
    paddingSize,
    borderSize,
    boxSizing,
    contextStyle
  } = calculateNodeStyling(targetElement)

  // 經過直接設置 style 屬性,使之成爲內聯樣式
  hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`)
  hiddenTextarea.value = targetElement.value || targetElement.placeholder || ''

  // scrollHeight 是一個元素內容高度,包括因爲溢出致使的視圖中不可見內容(包含 padding)
  let height = hiddenTextarea.scrollHeight
  const result = {}

  // 這裏判斷一下當前是 IE 盒模型仍是標準盒模型
  // IE 盒模型高度包括了內容(包含 padding)和邊框
  // 標準盒模型高度只是內容的高度,而且不包含 padding
  if (boxSizing === 'border-box') {
    height = height + borderSize
  } else if (boxSizing === 'content-box') {
    height = height - paddingSize
  }

  // 經過將 textarea 的值設爲空字符串來計算單行文本內容所佔的高度
  hiddenTextarea.value = ''
  let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize

  if (minRows !== null) {
    let minHeight = singleRowHeight * minRows
    if (boxSizing === 'border-box') {
      minHeight = minHeight + paddingSize + borderSize
    }
    // 最小高度應該取二者最大的,你品,你仔細品!
    height = Math.max(minHeight, height)
    result.minHeight = `${minHeight}px`
  }
  if (maxRows !== null) {
    let maxHeight = singleRowHeight * maxRows
    if (boxSizing === 'border-box') {
      maxHeight = maxHeight + paddingSize + borderSize
    }
    // 最大高度應該取二者最小的
    height = Math.min(maxHeight, height)
  }
  result.height = `${height}px`
  // 在移除 hiddenTextarea 前須要先判斷是否有父節點,若是不判斷,沒有父節點時會報錯
  // && 前面的若是爲假就不會繼續執行後面的表達式
  // 通常我想到的是使用 if 來判斷,使用 && 這種寫法可使代碼很優雅,學習了!
  hiddenTextarea.parentNode &&
    hiddenTextarea.parentNode.removeChild(hiddenTextarea)
  // 必定要釋放變量,不然一直在內存中存在,消耗內存
  hiddenTextarea = null
  return result
}
複製代碼

最後看一下導入的merge.js這個文件,這個文件主要導出了一個函數,主要做用是合併兩個對象爲一個對象,至關於 ES6 的Object.assign()方法,只不過這裏爲了兼容瀏覽器,而使用 ES5 的方式實現了。

趕忙記下來吧,說不定面試就會讓你手寫一個Object.assign()方法,有關該方法的能夠看阮老師的 ES6

/** * 合併對象的屬性,至關於 ES6 的 Object.assign() * 是淺拷貝,注意引用類型 * @param {*} target 目標對象 */
export default function (target) {
  // 從第二個實參開始遍歷
  for (let i = 1, j = arguments.length; i < j; i++) {
    // 把拿到的實參看成源對象
    let source = arguments[i] || {}
    // 遍歷源對象身上的屬性
    for (let prop in source) {
      // 必須保證是源對象自身的屬性
      if (source.hasOwnProperty(prop)) {
        // 若是這個屬性值不是 undefined 就把它添加到目標對象中
        // 注意,同名屬性會覆蓋
        let value = source[prop]
        if (value !== undefined) {
          target[prop] = value
        }
      }
    }
  }
  return target
}
複製代碼

MDN 上關於 scrollHeight 的解釋,不懂的小夥伴趕忙補習一下,面試的時候還會問你和clientHeightoffsetHeight有什麼區別。

各位看官,暈嗎?若是暈,我建議你喝口水打局農藥再繼續往下看,由於我也是這麼作的!

到如今爲止,input 組件主體內容已經分析的差很少了,接下來就是一些零零散散的內容了,一個個往下看。

inheritAttrs: false
複製代碼

這是 Vue2.4.0 新增的,那麼爲何要使用它呢?由於組件內未被註冊的屬性將做爲普通 HTML 元素屬性被渲染,若是想讓屬性可以向下傳遞,即便 prop 組件沒有被使用,你也須要在組件上註冊,這樣作會使組件預期功能變得模糊不清。

若是在組件中添加了inheritAttrs: false,那麼組件將不會把未被註冊的 props 呈現爲普通的 HTML 屬性。可是在組件裏咱們能夠經過其$attrs能夠獲取到沒有使用的註冊屬性。

依賴注入

當咱們的組件嵌套很深時,咱們就不推薦使用this.$parent來訪問咱們的父組件,由於嵌套很深時很難判斷this.$parent到底指向的是那個父組件,因此須要使用到「依賴注入

看一看源碼中的依賴注入

inject: {
  elForm: {
    default: ''
  },
  elFormItem: {
    default: ''
  }
}
複製代碼

也就是說這裏將el-form的相關數據注入了進來,由於 input 組件通常是伴隨着 Form 表單出現,當 input 事件被觸發時,須要通知其父組件的觸發相關事件。

有關依賴注入的詳細解釋能夠看 Vue 官方文檔

監聽屬性

再來看一下 watch 裏面都作了什麼(堅持一下,就快完了!!!)

要了解 watch 的做用,咱們就得先知道爲何要使用 watch,不是說能用計算屬性儘可能使用計算屬性嗎,怎麼又使用了 watch (小問號,你是否有不少朋友?)

watch 是 Vue 裏面提供的監聽器,用來監聽響應式數據的變化,當 data 中的數據發生了變化時,就會觸發 watch 中的同名函數,在 watch 中能夠執行任何邏輯,而且產生的數據不會緩存,這也就是說爲何 Vue 建議咱們在 computed 中使用「複雜的計算邏輯」,而在 watch 中儘可能執行一些「異步或者開銷大的操做」。

既然知道了爲何要使用 watch,那咱們就來看一下 input 中的 watch。給爺上代碼:

watch: {
  // 監聽 value 的變化
  value (val) {
    // 當 value 變化了須要從新改變文本域的大小
    // 這個屬於 DOM 操做,因此放在 $nextTick() 中
    this.$nextTick(this.resizeTextarea)
    // 若是須要校驗,那就要通知父組件觸發 change 方法
    if (this.validateEvent) {
      this.dispatch('ElFormItem', 'el.form.change', [val])
    }
  },
  // 監聽 type 的變化,type 爲 input 或者 textarea
  type () {
    // 一樣當 type 變化時,DOM 也會發生改變
    this.$nextTick(() => {
      this.setNativeInputValue()
      this.resizeTextarea()
      this.updateIconOffset()
    })
  }
}
複製代碼

須要說一下的就是this.$nextTick(callback),它在 DOM 更新後執行回調函數以獲取最新的 DOM,也就是說咱們能夠在「回調函數裏執行 DOM 操做

那何時使用$nextTick呢,目前我所知道有兩種:

  • 生命週期created()函數中進行的 DOM 操做必定要放在$nextTick()中,由於created()函數執行時 頁面中 DOM 節點尚未渲染,拿不到 DOM 節點
  • 當你的數據更新以後,須要手動操做 DOM 元素時,能夠講邏輯寫在回調函數裏

那既然知道爲何使用了,也就能看懂上面的解釋了。

移步官網查看異步更新隊列

因此綜上來看,input 中的 watch 主要是用於 input 值或者類型發生變化時須要更新 DOM。

還有一個小小的函數是calcIconOffset(),這個函數主要是和小圖標的樣式相關(關於樣式的部分後續再分析吧,真的搞不動了),用於計算橫向偏移量。看下源碼:

// 計算圖標的橫向偏移量
calcIconOffset (place) {
  // 找到 class 爲 .el-input__suffix 和 .el-input__prefix 的元素
  // 並把他們轉換爲數組
  let elList = [].slice.call(
    this.$el.querySelectorAll(`.el-input__${place}`) || []
  )
  if (!elList.length) return
  let el = null
  // 經過循環判斷 .el-input__suffix/prefix 是不是 input 的直接子元素
  for (let i = 0; i < elList.length; i++) {
    // $el 表示當前組件的根元素
    // $root 表示組件樹的根元素
    // 若是本次循環的 DOM 元素的父元素就是當前實例的根元素,把它賦給 el
    if (elList[i].parentNode === this.$el) {
      el = elList[i]
      break
    }
  }
  if (!el) return
  // 此時 el 應該是 .el-input__suffix/prefix 元素
  // 定義前綴後綴
  const pendantMap = {
    suffix: 'append',
    prefix: 'prepend'
  }

  const pendant = pendantMap[place]
  // pendant: append/prepend
  // 若是在組件中添加了前置或後置元素
  if (this.$slots[pendant]) {
    // 設置 .el-input__suffix/prefix 元素的行內樣式
    // 若是是後置元素,那平移的距離就是負的(向左)
    // 平移的距離就是前置或後置元素的寬度
    el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${ this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth }px)`
  } else {
    el.removeAttribute('style')
  }
},
複製代碼

不得不說他們對於樣式的計算很是精準,考慮了不少複雜的方面,放一張結構圖讓你們好理解一點

照着這個結構再去看calcIconOffset()方法就會容易不少,該有的都有了我也不想解釋了(心累啊!!)。

到目前爲止,input 組件的全部內容基本上全寫完了,關於官方文檔上的autocomplete這個組件我準備後期再看,留到後面去分析。

總結與梳理

如今讓咱們把腦子放空,閉上眼睛,仔細去想想 input 組件到底作了哪些事?從咱們移動鼠標到輸入框上開始,到最後輸入完成移開鼠標,這中間的數據是怎麼流動的?過程當中觸發了哪些事件?這樣一想你會從總體上理解input 組件。

到目前爲止這是我看的第二個組件,第一個是 button,至於爲何不寫 button,實在是由於 button 裏面的東西太少了,主要仍是和樣式有關,可是對於你初看源碼的話仍是建議先看一下 button,先了解一下組件思想以及他們是如何封裝的,考慮了哪些狀況,而後本身再試着手寫一個 button 組件,這樣你會對組件有更深的理解,對後面閱讀源碼也會有很大的幫助。

白白,撒由那拉❤️

相關文章
相關標籤/搜索