前言
不推荐通篇阅读,建议将element-ui源码下载并运行到本地,按照自己的方式阅读源码,遇上不明白点可以来这里Ctrl+F(网页内搜索)搜索一下,看这里有没有记录,如果你也是像我一样是初次阅读源码的人,也可以来了解一下我的思路(共同学习,其实我非常想了借鉴下大佬阅读源码的方式方法,毕竟有方法思路可以更快些)
element-ui版本:2.11.1
文章分两部分:
- 组件实现的基本原理介绍
- 详细说明组件中的大部分的函数、生命周期函数、工具函数/类,作为记录;(不推荐通篇阅读,枯燥,建议阅读源码时遇到不懂的点,可以来查阅一下)
组件实现的基本原理介绍
组件label-wrap.vue
组件<label-wrap>
的作用:根据inject
提供的elForm elFormItem
,和props
计算一个值变量marginLeft,作为组件的margin-left;并且给组件el-form-item
提供一个值,作为el-form-item__content
的margin-left,两个组件的margin-left
相加等于最长的那个lable
的width
el-form-item__content slot内的表单类组件
以el-input
为例,简单介绍el-input组件主要功能:处理的原生vue事件,并向外派发若干事件,处理input的前置后置元素,动态计算textarea
的高度,在组件数据变化是向el-form-item派发事件"change"
, 在组件失去焦点是向el-form-item派发事件"blue",
派发事件是实现表单验证功能的一部分;总体来说,是将html元素修改成类似react
中的 '受控组件' 。
表单验证
el-form
组件的属性rules
上保存着所有的验证规则,且model
属性上保存着整个表单的数据,并且el-form
中也暴露了一些有关表单验证的函数;表单验证需要被验证的字段名,对应的数据,对应的验证规则,以及派发和监听事件;验证逻辑的主体在el-form-item
,根据prop
字段名从el-form
获取对应字段的value和验证逻辑,维护验证状态和验证信息;
FML
具体分析流程(枯燥)
请不要在意下面,格式和结构,不适合阅读,看源码是思维有点发散,基本逻辑是看到哪一个函数就分析那个函数,顺着函数的上下文顺序和执行顺序走了一遍。
form-item.vue 的基本结构
三部分组成:左侧的lable,右上的输入框的插槽,右下的提示信息插槽;
// 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>
// 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 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时,否则组件就不会加载(元素 和slot 还是会加载的)。
组件的作用:根据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
样式;
回到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 = 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 的原生事件