不推薦通篇閱讀,建議將element-ui源碼下載並運行到本地,按照本身的方式閱讀源碼,趕上不明白點能夠來這裏Ctrl+F(網頁內搜索)搜索一下,看這裏有沒有記錄,若是你也是像我同樣是初次閱讀源碼的人,也能夠來了解一下個人思路(共同窗習,其實我很是想了借鑑下大佬閱讀源碼的方式方法,畢竟有方法思路能夠更快些)html
element-ui版本:2.11.1vue
文章分兩部分:react
組件<label-wrap>
的做用:根據inject
提供的elForm elFormItem
,和props
計算一個值變量marginLeft,做爲組件的margin-left;而且給組件el-form-item
提供一個值,做爲el-form-item__content
的margin-left,兩個組件的margin-left
相加等於最長的那個lable
的width
element-ui
以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
三部分組成:左側的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開始,組件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的屬性的值,後面會詳細說明;
inject: ['elForm', 'elFormItem'],
複製代碼
這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。若是你熟悉 React,這與 React 的上下文特性很類似。provide/inject機制文檔
如下全部分析的前提:當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的
// 函數render 中:
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
const autoLabelWidth = this.elForm.autoLabelWidth;
// 函數getLableWidth 中:
const computedWidth =
window.getComputedStyle(this.$el.firstElementChild).width;
複製代碼
// form.vue
computed: {
autoLabelWidth() {
if (!this.potentialLabelWidthArr.length) return 0;
const 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 調用函數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__content
的margin-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
樣式;
el-form對全部el-form-item 管理(漸漸地發覺組件data中的變量是最要的,此次可已從data入手看代碼,追根溯源);
// 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
中派發;
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() {
// 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 = false;
return;
}
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 true;
if (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
複製代碼
問
: 組件的parent 分別是什麼
答
:$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 變爲受控組件,對吧
<input @blur="handleBlur" >
methods: {
focus() { this.getInput().focus();},
focus() { this.getInput().focus();},
handleBlur(event) {
this.focused = false;
this.$emit('blur', event);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
}
},
getInput() { return this.$refs.input || this.$refs.textarea;},
},
複製代碼
input 事件compositionstart並非input 的原生事件