Element-UI閱讀理解(2) - 表單組件el-form、el-form-item

前言

不推薦通篇閱讀,建議將element-ui源碼下載並運行到本地,按照本身的方式閱讀源碼,趕上不明白點能夠來這裏Ctrl+F(網頁內搜索)搜索一下,看這裏有沒有記錄,若是你也是像我同樣是初次閱讀源碼的人,也能夠來了解一下個人思路(共同窗習,其實我很是想了借鑑下大佬閱讀源碼的方式方法,畢竟有方法思路能夠更快些)html

element-ui版本:2.11.1vue

文章分兩部分:react

  • 組件實現的基本原理介紹
  • 詳細說明組件中的大部分的函數、生命週期函數、工具函數/類,做爲記錄;(不推薦通篇閱讀,枯燥,建議閱讀源碼時遇到不懂的點,能夠來查閱一下)

組件實現的基本原理介紹

組件label-wrap.vue

組件<label-wrap>的做用:根據inject提供的elForm elFormItem,和props計算一個值變量marginLeft,做爲組件的margin-left;而且給組件el-form-item提供一個值,做爲el-form-item__content的margin-left,兩個組件的margin-left 相加等於最長的那個lablewidthelement-ui

el-form-item__content slot內的表單類組件

el-input爲例,簡單介紹el-input組件主要功能:處理的原生vue事件,並向外派發若干事件,處理input的前置後置元素,動態計算textarea的高度,在組件數據變化是向el-form-item派發事件"change", 在組件失去焦點是向el-form-item派發事件"blue",派發事件是實現表單驗證功能的一部分;整體來講,是將html元素修改爲相似react中的 '受控組件' 。api

表單驗證

el-form組件的屬性rules上保存着全部的驗證規則,且model屬性上保存着整個表單的數據,而且el-form中也暴露了一些有關表單驗證的函數;表單驗證須要被驗證的字段名,對應的數據,對應的驗證規則,以及派發和監聽事件;驗證邏輯的主體在el-form-item,根據prop字段名從el-form獲取對應字段的value和驗證邏輯,維護驗證狀態和驗證信息;數組

FML 瀏覽器

看源碼時常常是這個表情,寫完沒人看也是這樣,太難了


具體分析流程(枯燥)

請不要在乎下面,格式和結構,不適合閱讀,看源碼是思惟有點發散,基本邏輯是看到哪個函數就分析那個函數,順着函數的上下文順序和執行順序走了一遍。bash

form-item.vue 的基本結構

三部分組成:左側的lable,右上的輸入框的插槽,右下的提示信息插槽;app

//  form-item.vue 的結構
div.el-form-item
    label-wrap
        label
            slot[name="label"]
    .el-form-item__content  
        slot
        slot.[name="error"]
            .el-form-item__error
複製代碼

lable-wrap組件

從左側的lable開始,組件form-item的子組件<lable-wrap>dom

// label-wrap.vue 由父組件el-form提供
props: { 
    isAutoWidth: Boolean,
    updateAll: Boolean
},
複製代碼
組件dom結構
<label-wrap  :is-auto-width="labelStyle && labelStyle.width === 'auto'"  :update-all="form.labelWidth === 'auto'">
...
</label-wrap>
複製代碼

兩個屬性對應着el-form的屬性的值,後面會詳細說明;

provide / inject機制

inject: ['elForm', 'elFormItem'],
複製代碼

這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。若是你熟悉 React,這與 React 的上下文特性很類似。provide/inject機制文檔

methods、watch中的函數和生命週期函數(都用於計算一個值)

如下全部分析的前提:當this.isAutoWidth 爲true時

組件<label-wrap>的做用根據inject 提供的elForm elFormItem,和props計算一個值變量marginLeft,做爲組件的margin-left

// 當this.isAutoWidth 爲true時,lable-wrap纔有輸出
style.marginLeft = marginLeft + 'px';
// 輸出
return (<div class="el-form-item__label-wrap" style={style}> 複製代碼

el-form和el-form-item都有label-width屬性,當el-form-item沒有設置label-widht時,能夠繼承el-form的

從marginLeft向上推
// 函數render 中:
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
const autoLabelWidth = this.elForm.autoLabelWidth;
// 函數getLableWidth 中:
const computedWidth = 
window.getComputedStyle(this.$el.firstElementChild).width;
複製代碼
分析this.elForm.autoLabelWidth;
// form.vue
computed: { 
    autoLabelWidth() {  
        if (!this.potentialLabelWidthArr.length) return 0const max = Math.max(...this.potentialLabelWidthArr); 
        return max ? `${max}px` : ''; 
    }
},

potentialLabelWidthArr: [] // use this array to calculate auto width, 定義

registerLabelWidth(val, oldVal) {
    if (val && oldVal) { 
        const index = this.getLabelWidthIndex(oldVal);  
        this.potentialLabelWidthArr.splice(index, 1, val); 
    } else if (val) {   
        this.potentialLabelWidthArr.push(val);
    }
}
// 操做數組potentialLabelWidthArr(push和splice)

// 函數registerLabelWidth 在label-wrap.vue中被調用
watch: { 
    computedWidth(val, oldVal) { 
        if (this.updateAll) {   
            this.elForm.registerLabelWidth(val, oldVal);   
            this.elFormItem.updateComputedLabelWidth(val);  
        } 
    }
},
// 回到marginLeft 的第二個因數 computWidth
// 好繞啊,好煩啊,折執行流程也太煩人了吧,弗了;

// 函數this.computedWidth的值,在函數updateLabelWidth獲取
this.computedWidth = this.getLabelWidth();
// getLabelWidth 的返回值是元素 <div class="el-form-item__label-wrap" style={style}> 的寬;

// 函數updateLabelWidth 組件的mounted、updated、beforeDestory生命週期內都執行一次;
// action==='update'時 執行this.getLabelWidth
// action==='remove' 執行this.elForm.deregisterLabelWidth(this.computedWidth)
// form.vue
// 函數 deregisterLabelWidth
deregisterLabelWidth(val) { 
    const index = this.getLabelWidthIndex(val);  
    this.potentialLabelWidthArr.splice(index, 1);
}
// 函數 getLabelWidthIndex
getLabelWidthIndex(width) { 
    const index = this.potentialLabelWidthArr.indexOf(width);   // it's impossible 
    if (index === -1) { 
        throw new Error('[ElementForm]unpected width ', width);
    }  
    return index;
}
// potentialLabelWidthArr 是個數組

// 函數this.elForm.registerLabelWidth(val, oldVal);的做用
複製代碼

label-wrap.vuethis.isAutoWidth 爲true的條件

// form.vue
<label-wrap  :is-auto-width="labelStyle && labelStyle.width === 'auto'"
:update-all="form.labelWidth === 'auto'"</label-wrap> // 計算屬性 labelStyle labelStyle() {  const ret = {};  if (this.form.labelPosition === 'top') return ret;  const labelWidth = this.labelWidth || this.form.labelWidth;  if (labelWidth) {    ret.width = labelWidth;  }  return ret; }, // 複製代碼

當el-form和el-form-item的labelWidth屬性都是auto時, isAutoWidth 爲true

isAutoWidth 爲true時的組件lable-wrap.vue作的事:

// 在<lable>外增長一層div並設置margin-left
style.marginLeft = marginLeft + 'px';
return (<div class="el-form-item__label-wrap" style={style}> 複製代碼
// form-item.vue, 計算屬性contentStyle
contentStyle() { 
    const ret = {}; 
    const label = this.label; 
    if (this.form.labelPosition === 'top' || this.form.inline) return ret;
    if (!label && !this.labelWidth && this.isNested) return ret; 
    const labelWidth = this.labelWidth || this.form.labelWidth; 
    if (labelWidth === 'auto') {   
        if (this.labelWidth === 'auto') { 
            ret.marginLeft = this.computedLabelWidth;
        } else if (this.form.labelWidth === 'auto') { 
            ret.marginLeft = this.elForm.autoLabelWidth; 
         }  
    } else {  
        ret.marginLeft = labelWidth; 
    } 
    return ret;
},
複製代碼
// form.vue this.computedLabelWidth的值
updateComputedLabelWidth(width) { 
    this.computedLabelWidth = width ? `${width}px` : '';
},
// label-wrap.vue&emsp;調用函數updateComputedLabelWidth
watch: {  
    computedWidth(val, oldVal) { 
        if (this.updateAll) {          
            this.elForm.registerLabelWidth(val, oldVal);
            this.elFormItem.updateComputedLabelWidth(val); 
        } 
    }
},
// computedWidth 是lable的width

複製代碼

經過一些prop和data屬性的數據判斷contentStyle是否要置空;ret.marginLeft=this.elForm.autoLableWidth

// 常見的判斷組件間是否有其餘層級的元素,
// this.isNested 是標誌位
 while (parentName !== 'ElForm') {
  if (parentName === 'ElFormItem') {
    this.isNested = true;
  }
  parent = parent.$parent;
  parentName = parent.$options.componentName;
}
return parent;
複製代碼
小結

以上全部分析的前提:當this.isAutoWidth 爲true時,不然組件就不會加載(元素

組件的做用:根據inject 提供的elForm elFormItem,和props計算一個值變量marginLeft,做爲組件的margin-left,而且給組件el-form提供一個值,做爲el-form-item__contentmargin-left

label-wrap.vue中計算元素<label>width保存在data的變量computedWidth中,而且在不一樣的生命週期內(mounted,updated,beforeDestory)從新計算computed的值;

wach選項監聽computedWidth的變化,且執行兩個函數:

  • 1執行組件el-form中的registerLabelWidth(val, oldVal)函數將新的值替換舊的值(根據oldVal),這個值保存在數組potentialLabelWidthArr,全部el-form-item的computedWidth值都會按順序存儲在這,因此是替換;

  • 2執行el-form-item中的updateComputedLableWidth(val),將值保存在data的computedLabelWidth

labe-wrap el-form-item分別利用各自data中的變量,給相應的元素添加margin-left樣式;


回到form.vue

el-form對全部el-form-item 管理(漸漸地發覺組件data中的變量是最要的,此次可已從data入手看代碼,追根溯源);

fields: []

// form.vue
fields: [] 
// 保存全部el-form-item 相關的內容,目前具體是什麼還不知道;
複製代碼

在form.vue 一頓Ctrl+F 'fields'

// form.vue
// 監聽事件,對this.fields 增刪
created() {  
    this.$on('el.form.addField', (field) => {  
        if (field) { 
            this.fields.push(field);  
        }
    }); 
    /* istanbul ignore next */  
    this.$on('el.form.removeField', (field) => { 
        if (field.prop) { 
            this.fields.splice(this.fields.indexOf(field), 1);   
        }
    });
}

複製代碼
// form-item.vue/
this.dispatch('ElForm', 'el.form.addField', [this]);
this.dispatch('ElForm', 'el.form.removeField', [this]);
複製代碼

el.form.addField el.form.removeField事件, 分別在生命週期 mounted beforeDestory 中派發;

this.dispatch()

dispatch函數是以mixins方式引入的,由emitter提供的;

// element\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));
    }
}
// 向上找到組件名爲componentName的組件,將事件名和參數傳給它
複製代碼

emitter 中還有一個boradcast()dispatch()做用相似,只是傳遞的方向是全部子組件;

組件el-form-item經過dispatch派發的事件在el-form被觸發

this.dispatch('ElForm', 'el.form.addField', [this]);
...
 parent.$emit.apply(parent, [eventName].concat(params));
複製代碼

this是組件實例,[this]的寫法是利用了apply第二個參數是數組的特性,實現了ES6的解構賦值特性;

疑問父子組件間關於生命週期函數

表單驗證

已經獲取的全部子組件的實例,能夠執行表單驗證了

屬性rules
rules() { 
    // remove then add event listeners on form-item after form rules change  
    this.fields.forEach(field => { 
        field.removeValidateEvents();
        field.addValidateEvents();
    }); 
    if (this.validateOnRuleChange) {
        this.validate(() => {});  
    }
}
// 執行el-form-item實例上的函數
複製代碼
addValidateEvents() { 
    const rules = this.getRules();
    if (rules.length || this.required !== undefined) {   
        this.$on('el.form.blur', this.onFieldBlur); 
        this.$on('el.form.change', this.onFieldChange); 
    }
},
removeValidateEvents() {  
    this.$off();
}
複製代碼

事件el.form.blur el.form.change 由form-item組件內的一系列表單組件派發的。 以el-input爲例

// input.vue
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
this.dispatch('ElFormItem', 'el.form.change', [val]);
複製代碼

簡單介紹,el-input組件主要功能是:處理原生輸入元素的vue事件,並向外派發若干事件,處理input的前置後置元素,動態計算textarea的高度;整體來講,是將html元素修改爲react中的 '受控組件'

表單類的組件派發事件的 el.form.blur el.form.change 在el-form-item中監聽到了,並處理

// form-item.vue
// 關於表單驗證的 驗證函數開始執行 this.validate()
onFieldBlur() {  
    this.validate('blur');
},
onFieldChange() { 
    if (this.validateDisabled) { 
        this.validateDisabled = falsereturn; 
    }
    this.validate('change');
}
// validate函數 form.vue form-item.vue 都有定義
複製代碼

form-item.vue的validate函數

validate(trigger, callback = noop) {}
// noop 是工具函數一個空函數
import { noop, getPropByPath } from 'element-ui/src/utils/util';

export function noop() {};

this.validateDisabled = false;      // 應該是個標誌位,代表能夠驗證
const rules = this.getFilteredRule(trigger);
複製代碼
// getFilteredRule 函數
getFilteredRule(trigger) { 
    const rules = this.getRules();
    return rules.filter(rule => {   
        if (!rule.trigger || trigger === '') return trueif (Array.isArray(rule.trigger)) {  
            return rule.trigger.indexOf(trigger) > -1;   
        } else {     
            return rule.trigger === trigger; 
        } 
    }).map(rule => objectAssign({}, rule));
}
複製代碼
// getRules 函數
getRules() {
    let formRules = this.form.rules; 
    const selfRules = this.rules;
    const requiredRule = this.required !== undefined ? { required: 
!!this.required } : []; 
    const prop = getPropByPath(formRules, this.prop || '');  
    formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];  
    return [].concat(selfRules || formRules || []).concat(requiredRule);
},
複製代碼

let formRules = this.form.rules; 是父組件的rules;const selfRules = this.rules;是el-form-item的rules,兩個組件都有屬性rules,因此函數getRules兼容了二者的rules屬性,優先使用el-form-item自己的rules。this.prop文檔: prop表單域 model 字段,在使用 validate、resetFields 方法的狀況下,該屬性是必填的string傳入 Form 組件的 model 中的字段

工具方法`getPropByPath

const prop = getPropByPath(formRules, this.prop || '');
// 返回值 prop 是一個對象 o: tempObj,  // el-form 的rules屬性
    k: keyArr[i],   // 當前el-form-item 的prop屬性
    v: tempObj ? tempObj[keyArr[i]] : null  // 當前el-form-item的驗證信息,是個數組
};
// 舉例,el-form的rules屬性
rules: { 
    pass: [   
        { validator: validatePass, trigger: 'blur' }  
    ],  
    checkPass: [   
        { validator: validatePass2, trigger: 'blur' }
    ],  
    age: [    
        { validator: checkAge, trigger: 'change' } 
    ]
}
複製代碼

函數getRules 是返回el-form-item的驗證信息是一個數組 例如:

// age字段
[    
   { validator: checkAge, trigger: 'change' } 
]
複製代碼

getFilteredRule函數,返回當前觸發驗證事件的驗證規則 在函數validate中生成對象descriptor

const descriptor = {};
if (rules && rules.length > 0) { 
    rules.forEach(rule => {   
        delete rule.trigger; 
    });
}
descriptor[this.prop] = rules;

const validator = new AsyncValidator(descriptor);
// descriptor做爲參數傳給 第三方庫函數AsyncValidator

model[this.prop] = this.fieldValue;
// this.fieldValue 是計算屬性中的函數,
fieldValue() { 
    const model = this.form.model;  
    if (!model || !this.prop) { return; } 
    let path = this.prop; 
    if (path.indexOf(':') !== -1) {   
        path = path.replace(/:/, '.'); 
    }
    return getPropByPath(model, path, true).v;
},
// 獲取當前el-form-item的值

validator.validate(model, { firstFields: true }, (errors, 
invalidFields) => { 
    this.validateState = !errors ? 'success' : 'error'this.validateMessage = errors ? errors[0].message : '';  
    callback(this.validateMessage, invalidFields);  
    this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
    });
}
// 生成 validateState validateMessage
// 執行callback函數
// 派發validate'事件給el-form
// 表單驗證結束
複製代碼

其餘el-form-item,el-form的事件能夠在組件上監聽並處理,比較簡單; 其餘el-form-item,el-form的方法,只是對現有數據的修改,不不復雜; 例如上面的事件'validate'

this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
複製代碼

文檔: validate任一表單項被校驗後觸發被校驗的表單項 prop 值,校驗是否經過,錯誤消息(若是存在) 例如el-form的方法resetFields

//  el-form.vue 
resetFields() { 
    if (!this.model) { 
        console.warn('[Element Warn][Form]model is required for resetFields 
to work.'); 
        return; 
    } 
    this.fields.forEach(field => {
        field.resetField(); 
    });
}
// el-form-item.vue的函數resetField將關於驗證相關的data數據置空,並將表單的各個字段設置爲初始值;
rest
複製代碼

el-form-item.vue的

文檔:resetFields對整個表單進行重置,將全部字段值重置爲初始值並移除校驗結果

el-form和el-form-item的功能: 監聽el-form-item組件的建立和銷燬,維護el-form組件和el-form-item組件的信息和實例,都在data中;監聽el-form-item組件的驗證事件,驗證並返回錯誤信息;


this.$slots控制 div的類名,也控制相關slot內容的顯示;

具名插槽 插槽

自 2.6.0 起有所更新。已廢棄的使用 slot 特性的語法在這裏Element-UI 使用的vue版本是^2.5.17

注意

v-slot 指令自 Vue 2.6.0 起被引入,提供更好的支持 slot 和 slot-scope 特性的 API 替代方案。v-slot 完整的由來參見這份 RFC。在接下來全部的 2.x 版本中 slot 和 slot-scope特性仍會被支持,但已經被官方廢棄且不會出如今 Vue 3 中。

input 事件 compositionstart並非input 的原生事件;

compositionStart, compositionend 事件是爲了兼容 漢字日文韓語 的輸入;

在給輸入框綁定input或keydown事件時 預期效果是有輸入法時,輸入中文後觸發事件,不但願輸一個字母就觸發一次事件能夠用到compositionstart,compositionend。 主流瀏覽器都兼容

input 引入了 mixins: [emitter, Migrating],

// element-ui/src/mixins/emitter
複製代碼

組件的rootparent 分別是什麼

:$root :當前組件樹的根 Vue 實例。若是當前實例沒有父實例,此實例將會是其本身

// 代碼中的 || 僅僅是在該組件是更組件是生效,是一種邊界狀況處理
var parent = this.$parent || this.$root;
複製代碼
// element-ui/src/mixins/emitter 這段代碼搞什麼啊,
//尋找父級,若是父級不是符合的組件名,則循環向上查找
while (parent && (!name || name !== componentName)) {  
    parent = parent.$parent;
    if (parent) {   
     name = parent.$options.componentName; 
    }
}
複製代碼

:這段什麼意思

parent.$emit.apply(parent, [eventName].concat(params));
複製代碼

: vm.$emit( eventName, […args] ),派發帶參數的事件,參數的是個數組, :爲何要多加apply(parent),、 : 爲了提供結構賦值功能

inheritAttrs: false, , 與Props下的非Prop的特性相關 還沒理解

inject: {  elForm: {    default: ''  },  elFormItem: {    default: ''  }},
複製代碼
// 是把 原生input&emsp;變爲受控組件,對吧
<input @blur="handleBlur" >

methods: {
    focus() {  this.getInput().focus();},
    focus() {  this.getInput().focus();},
    handleBlur(event) {  
        this.focused = falsethis.$emit('blur', event); 
        if (this.validateEvent) {   
            this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
        }
    },
    getInput() {  return this.$refs.input || this.$refs.textarea;},
},
複製代碼

擴展閱讀:

provide/inject機制文檔

input 事件compositionstart並非input 的原生事件

非Prop的特性相關

apply實現解構賦值功能

相關文章
相關標籤/搜索