让我们浅浅的深入了解一下天天都在用的 el-form 组件(一)

场景描述

vue2.x + elments 是目前大家非常常用的配方,中台开发的不错选择之一吧~

既然使用频率这么高,我们就有必要了解ElForm 这个组件的工作过程,实现原理;

废话不多说 我们先从一个简单的 demo 的开始。

让我们浅浅的深入了解一下天天都在用的  el-form 组件(一)

这个表单验证demo 是我们日常开发中使用的比较完整的表单示例了;
接下来我们就从源码实现来看一看 el-form 这个组件的具体实现(并不复杂)。

从源码入手

form.vue 源码位置

我们先从 template 开始

<template>
  <form class="el-form" :class="[
    labelPosition ? 'el-form--label-' + labelPosition : '',
    { 'el-form--inline': inline }
  ]">
    <slot></slot>
  </form>
</template>

呐,我们看到了,一个原生的 form 元素,该 form 有 el-form 的class , 以及 动态class 列表;

这动态class 列表主要是设置了form 元素的展现方式。 对应的参数就是 label-position , 他的取值范围是 right/left/top , 默认 right , 表单的样式这里就不作为了解了

form 的内容就是 一个内容插槽,slot , 用于展示表单内容项;

我们再看js 部分:


import objectAssign from 'element-ui/src/utils/merge';
export default {
name: 'ElForm',
componentName: 'ElForm',
provide() {
return {
elForm: this
};
},
props: {
model: Object,
rules: Object,
labelPosition: String,
labelWidth: String,
labelSuffix: {
type: String,
default: ''
},
inline: Boolean,
inlineMessage: Boolean,
statusIcon: Boolean,
showMessage: {
type: Boolean,
default: true
},
size: String,
disabled: Boolean,
validateOnRuleChange: {
type: Boolean,
default: true
},
hideRequiredAsterisk: {
type: Boolean,
default: false
}
},
watch: {
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(() => {});
}
}
},
computed: {
autoLabelWidth() {
if (!this.potentialLabelWidthArr.length) return 0;
const max = Math.max(...this.potentialLabelWidthArr);
return max ? `${max}px` : '';
}
},
data() {
return {
fields: [],
potentialLabelWidthArr: [] // use this array to calculate auto width
};
},
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);
}
});
},
methods: {
resetFields() {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for resetFields to work.');
return;
}
this.fields.forEach(field => {
field.resetField();
});
},
clearValidate(props = []) {
const fields = props.length
? (typeof props === 'string'
? this.fields.filter(field => props === field.prop)
: this.fields.filter(field => props.indexOf(field.prop) > -1)
) : this.fields;
fields.forEach(field => {
field.clearValidate();
});
},
validate(callback) {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
let promise;
// if no callback, return promise
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid, invalidFields) {
valid ? resolve(valid) : reject(invalidFields);
};
});
}
let valid = true;
let count = 0;
// 如果需要验证的fields为空,调用验证时立刻返回callback
if (this.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {};
this.fields.forEach(field => {
field.validate('', (message, field) => {
if (message) {
valid = false;
}
invalidFields = objectAssign({}, invalidFields, field);
if (typeof callback === 'function' && ++count === this.fields.length) {
callback(valid, invalidFields);
}
});
});
if (promise) {
return promise;
}
},
validateField(props, cb) {
props = [].concat(props);
const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
if (!fields.length) {
console.warn('[Element Warn]please pass correct props!');
return;
}
fields.forEach(field => {
field.validate('', cb);
});
},
getLabelWidthIndex(width) {
const index = this.potentialLabelWidthArr.indexOf(width);
// it's impossible
if (index === -1) {
throw new Error('[ElementForm]unpected width ', width);
}
return index;
},
registerLabelWidth(val, oldVal) {
if (val && oldVal) {
const index = this.getLabelWidthIndex(oldVal);
this.potentialLabelWidthArr.splice(index, 1, val);
} else if (val) {
this.potentialLabelWidthArr.push(val);
}
},
deregisterLabelWidth(val) {
const index = this.getLabelWidthIndex(val);
this.potentialLabelWidthArr.splice(index, 1);
}
}
};

从 这些源码中 我们可以发现以下信息:

  1. provide 了自己,供子组件在需要的时候使用

  2. 定义了众多props 提供场景定制,对应的就是我们文档中的 Form Attributes

  3. watch 监听了 rules ,也就是我们提供的校验规则对象;

  4. computed 计算了 labelWidth 用于计算labelWidth 最大的值值多少;在这里我们并没有看见他使用这个 autoLabelWidth 属性,猜测应该是供 slot 内容实用的;

  5. 在created 的hooks 中,使用了 $on 监听了 两个事件 el-form.addField,el-form-removeField ,从命名可以看出,分为别 添加 和 移除 表单项字段

  6. data 函数中 声明了 fields(表单的所有字段集) 和 potentialLabelWidthArr (计算 autoWidht 的一个集合)

  7. methods 中定义了 几个表单的相关API , 主要分为:重置model 的 resetFields, 和校验相关的:clearValidate,validate,validateField, 以及labelWidth 相关的函数:getLabelWidthIndex , registerLabelWidth,deregisterLabelWidth ;

定义的函数

resetFields

resetFields() {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for resetFields to work.');
return;
}
this.fields.forEach(field => {
field.resetField();
});
},

函数实现非常简单:判断是 model 是否有值,没有就给出警告信息并return 中断后续,否则循环fields 集合,调用每个field 的 restField 函数;

clearValidate

      clearValidate(props = []) {
const fields = props.length
? (typeof props === 'string'
? this.fields.filter(field => props === field.prop)
: this.fields.filter(field => props.indexOf(field.prop) > -1)
) : this.fields;
fields.forEach(field => {
field.clearValidate();
});
},

clearValidate 函数实现也比较简单,先计算了 需要 清除验证的字段集合,再遍历集合调用每个field 的 clearValidate 函数;判断需要clear 的字段集的逻辑为:

判断传入的 props 集合是否有值,没值就返回了 this.fields(就是全部字段集),
有值就判断props的类型是否为string,是string 就在this.fields 中过滤找到这个field,不是string 就找到 props 对应的fields; 最终的目的就是找到props 对应的fields 集合,没有 props 就 返回全部字段集 this.fields;

validate(callback)

validate(callback) {
// 首先判断了 model 是否有值,没有就给出警告,并中断后续执行了。
if (!this.model) {
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
// 其次 申明了 promise ,他的作用就是根据用户是否传入了 callback 回调函数,如果没有回调函数,就返回 promise, 所以该方法我们在使用的过程中,如果没有传入callback 我们可以使用 await validate() 去获取表单的校验结果 valid ;
let promise;
// if no callback, return promise
if (typeof callback !== 'function' && window.Promise) {
// 没设置 callback 就 定义一个设置promise ,在promise 中定义  callback , 并 resolve 校验通过,  或reject 没通过校验的字段;
promise = new window.Promise((resolve, reject) => {
callback = function(valid, invalidFields) {
valid ? resolve(valid) : reject(invalidFields);
};
});
}
// 此处申明了valid 默认状态是true, 和 count 主要用于计数判断校验的字段是否等于 字段集的长度。判断字段集是否都校验完。
let valid = true;
let count = 0;
// 如果需要验证的fields为空,调用验证时立刻返回callback
if (this.fields.length === 0 && callback) {
callback(true);
}
// 申明了没用通过校验的 Fields ,没有通过校验的都会存储在这里
let invalidFields = {};
// 开始遍历字段集,并调用 每个字段的 validate
this.fields.forEach(field => {
// 这里 调用 每个字段的 validate  
field.validate('', (message, field) => {
// 这里 如果 message 有值,就吧 valid 状态设置为flase,标记校验没有通过,  
if (message) {
valid = false;
}
// 吧当前 field 合并到invalidFields 上。
invalidFields = objectAssign({}, invalidFields, field);
// 这里 判断了 callback 是否是函数,如果是,计数器加一,并判断字段集合中的字段都校验完了,然后 调用回调函数;
if (typeof callback === 'function' && ++count === this.fields.length) {
callback(valid, invalidFields);
}
});
});
// 没有回调函数,就吧 promise return 回去;
if (promise) {
return promise;
}
},

validate(callback) 这个方法就是 我们在日常开发中直接调用表单的 校验的方法,我们一起来看看他的具体实现:代码较长 请查看代码中的注释信息!

validateField(props, cb)

      validateField(props, cb) {
props = [].concat(props);
// 根据传入的 props 过滤出需要校验目标字段集
const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
// 如果需要校验的目标字段集为空,就抛出警告 中断后续执行。
if (!fields.length) {
console.warn('[Element Warn]please pass correct props!');
return;
}
// 遍历需要校验的目标字段集,挨个调用字段的validate,并传入cb 函数,这里也就是说 我们传入的cb,会执行多次。 
fields.forEach(field => {
field.validate('', cb);
});
},

validateField(props, cb) 校验部分字段集,直接看代码中的注释信息!

getLabelWidthIndex

根据传入的width 值,找到 potentialLabelWidthArr 中的下表,过于简单不作赘述,

registerLabelWidth(val, oldVal)

registerLabelWidth(val, oldVal) {
if (val && oldVal) {
const index = this.getLabelWidthIndex(oldVal);
this.potentialLabelWidthArr.splice(index, 1, val);
} else if (val) {
this.potentialLabelWidthArr.push(val);
}
},

该方法根据 val,oldval 去更新 potentialLabelWidthArr , 如果val,old 都存在,就先根据 oldval 找到下标,用val,替换oldval ; 如果只有val,就直接push 进potentialLabelWidthArr 。该函数就是为了维护potentialLabelWidthArr;

deregisterLabelWidth(val)

deregisterLabelWidth(val) {
const index = this.getLabelWidthIndex(val);
this.potentialLabelWidthArr.splice(index, 1);
}

deregisterLabelWidth(val) 该方法就是根据传入的val,并在 找到val 在potentialLabelWidthArr 中对应的下标,然后根据下标,删除该val;

总结一下

我们通过对form.vue 源码的分析发现,看框架/库的源码并不困难,另外 form 的实现也不复杂,代码较多的是 validate 这个函数;

当你认真读到这里的时候,你一定有一下疑问:

  1. created 的 监听的 field 的 add/remove 事件,再哪儿触发的,在什么时机触发的 ?
  2. data 中定义的 fields 集合里面到底存放的是什么 ?
  3. field 中的 validate 函数到底是如何实现的 ?
  4. 关于 lableWidth 的 这些函数方法在哪儿使用的 ?

有了这些疑问, 我们下一篇 form-item 组件的源码分析,将逐个解答这些疑问!敬请期待~~~

最后的最后

下面请上我们今天的主角:有请小趴菜

让我们浅浅的深入了解一下天天都在用的  el-form 组件(一)

原文链接:https://juejin.cn/post/7320426329165561895 作者:半个小趴菜

(0)
上一篇 2024年1月6日 上午10:43
下一篇 2024年1月6日 上午10:53

相关推荐

发表回复

登录后才能评论