Element-UI阅读理解(2) - 表单组件el-form、el-form-item

5,875 阅读7分钟

前言

不推荐通篇阅读,建议将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 相加等于最长的那个lablewidth

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 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时,否则组件就不会加载(元素 和slot 还是会加载的)。

组件的作用:根据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实现解构赋值功能