Vue Form 源码结构分析

问题来源

其实这个问题来源于写 Vue 表单时没有太注意的细节。<el-form> 是通过 :model 绑定,而 <el-input> 是通过 v-model 绑定。当时粗略看了下代码,但是已经很久了,并且没有形成记录,所以现在再看一次,并记录一下。

Vue Form 相关代码分析

这需要来看一下 Vue 中是怎么完成一个表单的。

Input

具体的 Item 控件以 Input 为例来看看代码。

input.vue
<template>
				<!-- ... -->
				<i class="el-input__icon"
          v-if="validateState"
          :class="['el-input__validateIcon', validateIcon]">
        </i>
				<!-- ... -->
</template>
<script>
	export default {
		inject: {
      elForm: {
        default: ''
      },
      elFormItem: {
        default: ''
      }
    },
		props: {
			value: [String, Number],
		},
		computed: {
      validateState() {
        return this.elFormItem ? this.elFormItem.validateState : '';
      },
      needStatusIcon() {
        return this.elForm ? this.elForm.statusIcon : false;
      },
    }
		methods: {
      // 响应输入
			handleInput(event) {
        // should not emit input during composition
        // see: https://github.com/ElemeFE/element/issues/10516
        if (this.isComposing) return;
        // hack for https://github.com/ElemeFE/element/issues/8548
        // should remove the following line when we don't support IE
        if (event.target.value === this.nativeInputValue) return;
        this.$emit('input', event.target.value);
        // ensure native input value is controlled
        // see: https://github.com/ElemeFE/element/issues/12850
        this.$nextTick(this.setNativeInputValue);
      },
      // 输入框尾部
      getSuffixVisible() {
        return ... || (this.validateState && this.needStatusIcon);
      }
		}
	}
</script>

可以看出,Input 组件主要做的就是处理真正的输入,并响应输入的改变。他不做数据的校验,只是根据父组件 FormItem 的校验状态 validateState 和父父组件 Form 的显示状态 statusIcon 来展示最终的校验状态而已(根据方法 getSuffixVisible)。而这一步,是通过依赖注入实现的。

除此之外,Input 组件会分别在改变和 blur 时向组件 FormItem 传播事件 el.form.changeel.form.blur,在后面的章节中,我们来看看这个事件具体是干嘛。

FormItem

让我们再来看看 FormItem 组件。

FormItem.vue
<script>
	export default {
		provide() {
      return {
        elFormItem: this
      };
    },
    data() {
      return {
        validateState: '',
        validateMessage: '',
        validateDisabled: false,
        validator: {},
        ...
      };
    },
		props: {
			prop: String,
		},
		computed: {
      // 获取父元素
      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;
      },
      // 从父元素绑定的 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;
      },
    }
		methods: {
      // 校验本属性,trigger 为 blur、change 等
			validate(trigger, callback = noop) {
        this.validateDisabled = false;
        const rules = this.getFilteredRule(trigger);
        if ((!rules || rules.length === 0) && this.required === undefined) {
          callback();
          return true;
        }
        this.validateState = 'validating';
        const descriptor = {};
        // 匹配 AsyncValidator 插件所需要的格式,需要做规则数据做一些操作
        if (rules && rules.length > 0) {
          rules.forEach(rule => {
            delete rule.trigger;
          });
        }
        descriptor[this.prop] = rules;
        const validator = new AsyncValidator(descriptor);
        const model = {};
        model[this.prop] = this.fieldValue;
        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);
        });
      },
      // 获取所有的校验规则
      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);
      },
      // 添加监听
      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);
        }
      },
		},
    mounted() {
      if (this.prop) {
        this.dispatch('ElForm', 'el.form.addField', [this]);
        ...
        this.addValidateEvents();
      }
    },
	}
</script>

FormItem 与 Form 之间同样有交互,它有两种方式。

一是依赖注入,获取到最近的 Form 表单,传播了一个 validate 的事件。(不过我没有看到在哪里响应了这个事件。。。有看到同学欢迎告诉我~)

二是通过在计算属性中直接获取父元素计算 form 得到的最外层的 Form 组件(因为可能有嵌套),在具体使用到父元素 form 的地方主要有以下两点。

  1. 获取对应属性的值:通过观察 fieldValue 方法,我们可以知道,FormItem 是通过父元素 Form 绑定的 model 和自身 props 中的 prop 间接获取对应值的(通过 util 方法 getPropByPath 计算对象 model 中的一个路径为 prop 的属性的值,可参考代码)。

  2. 获取所有的校验规则:通过观察 getRules 方法,在 FormItem 组件中会获取整个 Form 中与自己相关的规则,但是通过自身传入的规则优先级更高,会覆盖整个 Form 中关于自身的规则。(根据代码 selfRules || formRules || []

此外,FormItem 做的最重要的一件事就是对当前 Item 的验证。在 validate 方法中,会根据 trigger 的不同获取相应 trigger 的规则,然后通过 AsyncValidator 进行校验,set 当前的validateState (正如之前所说,具体的 Item 控件会获取此值)并进行回调。

再让我们来看看 FormItem 是怎么响应 Input 组件传播过来的事件的。在 FormItem 挂载时,调用了 addValidateEvents 方法,他监听了 el.form.changeel.form.blur 这两个事件,在具体的事件处理函数中,调用了 validate 方法,即对当前值立刻进行了验证。

还需要注意的一点是,在 FormItem 挂载的钩子中,它传播了一个事件 el.form.addField 给父组件 Form,参数为当前组件。我们待会儿再来看它具体做了什么。

Form

最后,我们再来看看 Form 组件。

Form.vue
<script>
	export default {
		provide() {
      return {
        elForm: this
      };
    },
    created() {
      // 初始化时
      this.$on('el.form.addField', (field) => {
        if (field) {
          this.fields.push(field);
        }
      });
      ...
    },
		methods: {
			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) {
              valid ? resolve(valid) : reject(valid);
            };
          });
        }
        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);
        });
      },
		}
	}
</script>

我们首先来看看 Form 的 created 生命周期钩子,它监听了事件 el.form.addField,并将参数 push 进了当前的 fields 数组以完成 fields 的初始化,所以 fields 中实际存的就是当前表单的所有 FormItem。

最后,我们来回想一下用户进行表单验证时是怎么调用的。用户验证表单时不是单个单个对 FormItem 而是对整个 Form 的,所以在验证时,无论是调用 validate 还是 validateField,会遍历所有相关 field 即 FormItem,并通过子组件 FormItem 依次进行验证。

总结

相信到现在,Form、FormItem、具体的表单控件如 Input 这三者之间的关系已经很清楚了。

  • 具体的表单控件:处理数据变化。
    • 双向绑定了操作的对象;
    • 传递 blurchange 事件以期望进行立刻的校验;
    • 它获取父组件等是通过依赖注入。
  • FormItem:通过规则校验数据。
    • 在挂载时通过事件 addField 告知父组件;
    • 响应 blurchange 事件以进行真正的校验;
    • 它不主动与子组件交互,但会直接获取父元素,并通过计算拿到父元素单向绑定的对象中特定属性的值,即需要校验的值,从而让校验可以进行;
    • 用自身的规则(如果存在)覆盖父组件 Form 中的规则。
  • Form:提供表单整体验证的功能。
    • 响应 addField 事件以记录当前表单的 FormItem;
    • validate 等被调用时实际是通过 FormItem.validate 实现的。

参考