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

场景描述

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

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

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

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

从源码入手

form.vue 源码位置

我们先从 template 开始

vue 复制代码
<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 部分:

javascript 复制代码
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

javascript 复制代码
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

javascript 复制代码
      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)

javascript 复制代码
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)

javascript 复制代码
      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

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

registerLabelWidth(val, oldVal)

kotlin 复制代码
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)

kotlin 复制代码
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 组件的源码分析,将逐个解答这些疑问!敬请期待~~~

最后的最后

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

相关推荐
ROCKY_817几秒前
web前端-HTML常用标签-综合案例
前端·html
Q186000000006 分钟前
在HTML中添加图片
前端·html
傻虎贼头贼脑21 分钟前
day21JS-npm中的部分插件使用方法
前端·npm·node.js
小胖伦的夕阳粉31 分钟前
js 获取树节点上某节点的最底层叶子节点数据
开发语言·javascript·ecmascript
low神32 分钟前
前端在网络安全攻击问题上能做什么?
前端·安全·web安全
@听风吟1 小时前
力扣之182.查找重复的电子邮箱
大数据·javascript·数据库·sql·leetcode
码力码力我爱你1 小时前
QT + WebAssembly + Vue环境搭建
vue.js·vue·wasm·webassembly·emscripten
qbbmnnnnnn1 小时前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹1 小时前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪1 小时前
uni-app环境搭建
前端·uni-app