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

场景描述

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

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

上一篇《让我们浅浅的深入了解一下天天都在用的 el-form 组件(一)》 中我们从头到尾了解了el-form 的实现过程,从整个过程中我们了解到他的实现并不复杂,el-form 更多的是提供一个容器,去存放 el-form-item,

  • 在 template 部分 非常简洁,就提供了一些动态class ,适配不同场景下的不同样式

  • 在 computed 部分 提供了一些计算属性主要是用于计算 label-width 的相关数据

  • 在 watcher 部分监听了 rules ,也就是我们提供的校验规则对象

  • 在 created 的hooks 中,使用了 $on 监听了 两个事件 el-form.addField,el-form-removeField

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

methods 中定义了 几个表单的相关API , 主要分为校验相关的函数,和计算label-width 相关的函数

今天我们通过上一篇遗留的如下的问题来分析解答每一个问题:

遗留疑问

我们先来看看遗留的问题列表:

  • created 的 监听的 field 的 add/remove 事件,在哪儿触发的,在什么时机触发的 ?

  • data 中定义的 fields 集合里面到底存放的是什么 ?

  • field 中的 validate 函数到底是如何实现的 ?

  • 关于 lableWidth 的 这些函数方法在哪儿使用的 ?

从源码入手

el-form-item 组件的源码目录为:element/blob/dev/packages/form/src/form-item.vue ,快捷入口 点我

还是老规矩 先看 template 部分

vue 复制代码
<template>
  <div class="el-form-item" :class="[{
      'el-form-item--feedback': elForm && elForm.statusIcon,
      'is-error': validateState === 'error',
      'is-validating': validateState === 'validating',
      'is-success': validateState === 'success',
      'is-required': isRequired || required,
      'is-no-asterisk': elForm && elForm.hideRequiredAsterisk
    },
    sizeClass ? 'el-form-item--' + sizeClass : ''
  ]">
    <label-wrap
      :is-auto-width="labelStyle && labelStyle.width === 'auto'"
      :update-all="form.labelWidth === 'auto'">
      <label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
        <slot name="label">{{label + form.labelSuffix}}</slot>
      </label>
    </label-wrap>
    <div class="el-form-item__content" :style="contentStyle">
      <slot></slot>
      <transition name="el-zoom-in-top">
        <slot
          v-if="validateState === 'error' && showMessage && form.showMessage"
          name="error"
          :error="validateMessage">
          <div
            class="el-form-item__error"
            :class="{
              'el-form-item__error--inline': typeof inlineMessage === 'boolean'
                ? inlineMessage
                : (elForm && elForm.inlineMessage || false)
            }"
          >
            {{validateMessage}}
          </div>
        </slot>
      </transition>
    </div>
  </div>
</template>

我们可以清晰地看到,使用了 class 为 el-form-item 的div 作为最外层容器,并且根据一系列的条件判断添加了很多动态的class 列表。

包含了 内容部分 包含了 label-wrap 和 div.el-form-item__content 两个部分,分别对应的就是 表单的 label, 和 内容部分。

label-wrap 我们稍后再去看他是怎么个实现

div.el-form-item__content 提供了 solt 用于放置具体的表单元素,如 input, checkbox ,select ,以及其他任意的内容;transition 部分就是错误信息的展示部分了,使用的el-zoom-in-top 的动画样式;

template 部分就这样简单的看完了,内容非常的简单 .

接下来我们继续 js 部分

import 部分

JavaScript 复制代码
import AsyncValidator from 'async-validator'; // 一个表单校验框架
import emitter from 'element-ui/src/mixins/emitter'; // 事件
import objectAssign from 'element-ui/src/utils/merge';
import { noop, getPropByPath } from 'element-ui/src/utils/util';
import LabelWrap from './label-wrap';

async-validator 为一个表单校验工具库 见文档 el 正是使用了该库来完成了表单的校验工作

emitter :这里提供了跨层级的通讯能力,具体可以看这篇《浅析Element ui 中事件 broadcast 与 dispatch》

objectAssign 函数为一个工具函数,旨在对象的属性合并,具体和 Object.assign() 类似,有兴趣的可以查看一下他们的实现;

noop, getPropByPath 都是工具函数,noop:定义了一个空函数,getPropByPath 通过path 获取 prop

LabelWrap 就是 lebel 的包装组件

props等选项

yaml 复制代码
    name: 'ElFormItem',
    componentName: 'ElFormItem',
    mixins: [emitter],
    provide() {
      return {
        elFormItem: this
      };
    },
    inject: ['elForm'],
    props: {
      label: String,
      labelWidth: String,
      prop: String,
      required: {
        type: Boolean,
        default: undefined
      },
      rules: [Object, Array],
      error: String,
      validateStatus: String,
      for: String,
      inlineMessage: {
        type: [String, Boolean],
        default: ''
      },
      showMessage: {
        type: Boolean,
        default: true
      },
      size: String
    },
    components: {
      // use this component to calculate auto width
      LabelWrap
    },

这里我们简单的过一下,不作具体的赘述,因为意义不大,都是一些常规的神明以及固有的选项值等。

data() 部分

javascript 复制代码
   data() {
      return {
        validateState: '',
        validateMessage: '',
        validateDisabled: false,
        validator: {},
        isNested: false,
        computedLabelWidth: ''
      };

    },

依然是声明了很多属性,更多的还是 validate相关的,以及 labelWidht 相关的

watch 部分

javascript 复制代码
  watch: {
      
      error: { 
        immediate: true,
        handler(value) {
          this.validateMessage = value;
          this.validateState = value ? 'error' : '';
        }
      },

      validateStatus(value) {
        this.validateState = value;
      },

      rules(value) {
        if ((!value || value.length === 0) && this.required === undefined) {
          this.clearValidate();
        }
      }
    },

监听了 error 的变化,给validateMessage以及validateState 赋值,以及校验rules的变化来调用clearValidate清除字段的校验;依旧是非常的简单。

computed 部分

JavaScript 复制代码
computed: { 
      labelFor() {
        return this.for || this.prop;
      },
      // 计算 lebel 的 样式信息,用于 LabelWrap 组件的is-auto-witdh 属性判断
      labelStyle() {
        const ret = {};
        if (this.form.labelPosition === 'top') return ret;
        const labelWidth = this.labelWidth || this.form.labelWidth;
        if (labelWidth) {
          ret.width = labelWidth;
        }
        return ret;
      },
      // 计算 表单 内容元素的样式 主要是根据 leble的样式 计算不同场景下的 content 的 marginLeft 值        
      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;
      },
      // 通过不停的找爹模式 找到 el-form, 因为 el-form 是可以包裹任意元素的,不限层级,所以这个地方一直向上找爹,直到找到;  
      form() 
        let parent = this.$parent;
        let parentName = parent.$options.componentName;
        while (parentName !== 'ElForm') {
          if (parentName === 'ElFormItem') {
            this.isNested = true;
          }
          parent = parent.$parent;
          parentName = parent.$options.componentName;
        }
        return parent;
      },
      // 获取 prop 的对应的model 的值;  
      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;
      },
      // 计算该表单元素是不需要必填,  getRules 就是获取了 该表单项的所有校验规则,再查询这些规则中是否有 required 为真的项目,最终返回
      isRequired() {
        let rules = this.getRules();
        let isRequired = false;
        if (rules && rules.length) {
          rules.every(rule => {
            if (rule.required) {
              isRequired = true;
              return false;
            }
            return true;
          });
        }
        return isRequired;
      },
      // 获取 表单的上设置的表单项的尺寸大小;对应的就是文档中的  mini,small 这个些个值 
      _formSize() {
        return this.elForm.size;
      },
      // 获取 表单项的size,规则为优先自身设置的值其次再试表单统一设置的值
      elFormItemSize() {
        return this.size || this._formSize;
      },
      // ..... 不同size 对应的class  
      sizeClass() {
        return this.elFormItemSize || (this.$ELEMENT || {}).size;
      }
    },

代码较长,我们直接在源码上添加注释,我们进行简单的备注,请看属性的备注信息;

methods 部分

JavaScript 复制代码
// 表单项的校验函数 ,
validate(trigger, callback = noop) {
        this.validateDisabled = false
        // 首先根据 trigger 参数 获取了 该字段的校验规则
        const rules = this.getFilteredRule(trigger)
        // 如果校验规则没有,或者 都不需要必填,就直接执行 callback ,并返回 true
        if ((!rules || rules.length === 0) && this.required === undefined) {
          callback();
          return true;
        }
        
        // 这里就开始了正在的校验逻辑了
        
        // 先初始化了 validateState 为 正在校验,这个状态也对应了 template 中的 校验状态 class 样式信息。
        this.validateState = 'validating';
     
        // 申明校验的descriptor
        const descriptor = {};
        // 这里如果校验规则存在,就吧每一项的 trigger 属性删掉
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        
        // 定义了 prop 的 rules 用于初始化 AsyncValidator 的实例 
        descriptor[this.prop] = rules;
       // 初始化 AsyncValidator 的实例,validator
       
        const validator = new AsyncValidator(descriptor);
        
        // 吧 fieldValue 赋值给 model 的prop ,因为 validator.validate 的参数类型原因,申明生来了对象模式;
        const model = {};
        model[this.prop] = this.fieldValue; 
            
        // 调用 validator.validate 的方式,完成校验工作,包括 设定 validate 的状态,错误信息,以及执行 callback 注入 validateMessage   ,invalidFields , 这也就是我们可以再 el-form 中收集到 invalidFields 的原因,因为这里传入了这些需要的信息
        validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
          this.validateState = !errors ? 'success' : 'error';
          this.validateMessage = errors ? errors[0].message : '';
          callback(this.validateMessage, invalidFields);
          // 去触发 form 的 validate 事件,并注入参数
          this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
        });
      },
      // 清楚Validate 重置一些信息 
      clearValidate() {
        this.validateState = '';
        this.validateMessage = '';
        this.validateDisabled = false;
      },
        
      // 重置 field 的值,这就是 el-form 中调用 resetFields 中 循环调用 field的  resetField 函数的实现。处理不同场景下的 值等;
      resetField() {
        this.validateState = '';
        this.validateMessage = '';
        let model = this.form.model;
        let value = this.fieldValue;
        let path = this.prop;
        if (path.indexOf(':') !== -1) {
          path = path.replace(/:/, '.');
        }
        let prop = getPropByPath(model, path, true);
        this.validateDisabled = true;
        if (Array.isArray(value)) {
          prop.o[prop.k] = [].concat(this.initialValue);
        } else {
          prop.o[prop.k] = this.initialValue;
        }

        // reset validateDisabled after onFieldChange triggered
        this.$nextTick(() => {
          this.validateDisabled = false;
        });
        this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
      },
        
        
      // 获取 校验规则,  先获取了 form 上定义的规则,再获取自身定义的规则,以及 requiredRule 等,最后合并到一起,
      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);
      },
     // 工具函数 获取 通过 trigger 过滤后的规则;
      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));
      },
      
      // 触发 Blur 事件,就调用 validate函数去触发校验。
      onFieldBlur() {
        this.validate('blur');
      },
      // 同上 触发 change 事件 , 就调用 validate函数去触发校验。
      onFieldChange() {
        if (this.validateDisabled) {
          this.validateDisabled = false;
          return;
        }
        this.validate('change');
      },
      // ....  顾名思义 ,哈哈哈
      updateComputedLabelWidth(width) {
        this.computedLabelWidth = width ? `${width}px` : '';
      },
      // 添加事件(blur , change )的监听,去执行对象校验函数
      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();
      }

这部分代码也比较长,我们还是通过源码加备注的方式来理解,请看备注信息吧!

mounted 部分

kotlin 复制代码
    mounted() {
      if (this.prop) {
        this.dispatch('ElForm', 'el.form.addField', [this]);
        let initialValue = this.fieldValue;
        if (Array.isArray(initialValue)) {
          initialValue = [].concat(initialValue);
        }
        Object.defineProperty(this, 'initialValue', {
          value: initialValue
        });
        this.addValidateEvents();
      }
    },

这部分内容也是非常简单,就是判断如果设置了 prop 属性,在dom 渲染完成后触发 el.form.addField 事件,这个事件在el-form 中有监听,将加入 el-form 的 fields 字段集合中;设置初始值,以及添加校验事件;

这就解释我了我们的疑问:el.form.addField 在哪儿触发的? 他是在 el-form-item mounted后 根据prop 判断存在后触发的。

beforeDestroy 部分

javascript 复制代码
beforeDestroy() {

      this.dispatch('ElForm', 'el.form.removeField', [this]);

    }

这个就是 在自身销毁的之前,触发 el.form.removeField 吧自从从 el-from 的 fields 中移除;

至此 我们的 el-form-item 就基本分析完毕了

相信通过这两篇文章 恭喜你你对 el-form 的设计 和 使用又有了一些新的认识 ~~~

最后的最后

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

相关推荐
I_Am_Me_24 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
℘团子এ34 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z39 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
星星会笑滴43 分钟前
vue+node+Express+xlsx+emements-plus实现导入excel,并且将数据保存到数据库
vue.js·excel·express
前端百草阁1 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple1 小时前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
临枫5411 小时前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript