Element-ui源码解析之el-form与el-form-item(一)

1,881 阅读2分钟

前言

最近自己封装了一些组件,闲着没事,就像把element-ui源码看看,顺便记录一下,目前只关注逻辑层面,样式的处理后面有时间在看

主要内容

看源码主要是要先对着element-ui的文档一步一步来看,先看element-ui的校验是怎么实现的,校验这块主要是在el-form的created声明周期里面执行一些addField和removeField的操作,通过监听当前实例的$emit方法emit出来的事件

// el-form
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);
    }
  });
},
// el-form-item进行emit
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();
  }
},

el-form-item这里的dispatch方法主要是利用的一个封装的dispatch方法在指定的组件上面进行自定义事件的派发,相关源码emitter.js里面,this.dispatch('ElForm', 'el.form.addField', [this]);意思就是在El-from组件上面派发一个el.form.addField事件,值是当前组件实例,所以el-form组件中的fields的值都是el-form-item这个组件实例,然后平常我们对表单进行校验,恰是都是使用下方这种方式

submitForm(formName) {
    this.$refs[formName].validate((valid) => {
        if (valid) {
            alert('submit!');
        } else {
            console.log('error submit!!');
            return false;
        }
    });
},

这时候可以看下validate这个方法的实现

// el-form
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;
  }
},
// el-form-item
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 = {};
  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) => {
    console.log(model)
    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);
  });
},

validate可以传入一个callback回调函数,如果没有传的话,则可以返回一个promise,可以看到该处声明了一个valid为true,如果子组件el-form-item的validate方法只要有一个抛出了校验不通过的message提示,就把valid置为false,整体校验就不通过,callback的参数是是否校验成功和未通过校验的字段,这里我们主要关注一下el-form-item子组件的validate方法,这个方法主要是对所有rules进行校验,忽视rules的trigger的值,只要有rules就校验,如果rules没有就直接通过,然后把每个el-form-item的prop放到descriptor这个对象里面,在进行new AsyncValidator(descriptor);操作,接下就关注一个model[this.prop] = this.fieldValue;这里就是可以理解为拿到form表单每个字段的值,然后用AsyncValidator这个库的validate方法去校验,我们可以看下this.fieldValue是怎么取到每个form表单的字段的值的

// 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;
},
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;
},
// util.js
export function getPropByPath(obj, path, strict) {
  let tempObj = obj;
  path = path.replace(/[(\w+)]/g, ".$1");
  path = path.replace(/^./, "");
  let keyArr = path.split(".");
  let i = 0;
  for (let len = keyArr.length; i < len - 1; ++i) {
    if (!tempObj && !strict) break;
    let key = keyArr[i];
    if (key in tempObj) {
      tempObj = tempObj[key];
    } else {
      if (strict) {
        throw new Error("please transfer a valid prop path to form item!");
      }
      break;
    }
  }
  return {
    o: tempObj,
    k: keyArr[i],
    v: tempObj ? tempObj[keyArr[i]] : null,
  };
}

可以看到先是拿到el-form组件的model对象的字段,然后如果碰到了这种格式prop A:B:C就把他转换为A,B,C的格式,然后调用return getPropByPath(model, path, true).v;getPropByPath这个方法主要就是就一个取model对象每个字段值的操作,这里分为两种情况,先说第一种情况,如果prop是普通的一个key,那么就直接取,最后格式大概是这样

return {
    o: {name: '', age: '', address: 'hz'},
    k: 'name', // 普通格式不走for循环
    v: null
}
return {
    o: {name: '', age: '', address: 'hz'},
    k: 'age', // 普通格式不走for循环
    v: null
}
return {
    o: {name: '', age: '', address: 'hz'},
    k: 'address', // 普通格式不走for循环
    v: 'hz'
}

如果是动态增减表单项,例如文档中的例子

<el-form-item
    v-for="(domain, index) in dynamicValidateForm.domains"
    :label="'域名' + index"
    :key="domain.key"
    :prop="'domains.' + index + '.value'"
    :rules="{
    required: true, message: '域名不能为空', trigger: 'blur'
  }"
>
  <el-input v-model="domain.value"></el-input><el-button @click.prevent="removeDomain(domain)">删除</el-button>
</el-form-item>

这种情况下prop是动态的,格式为:prop="'domains.' + index + '.value'",那么getPropByPath返回的格式大概是这样的,比如上面这个例子,最后domains会是一个domains:[{key: 1636695934176, value: ""}, {key: 1636695934176, value: ""}]的key-value数组,然后props带有点,所以会分割为一个这样的数组['domains', index, 'value'],之后进行遍历,因为domians作为key在tempObj中,所以tempObj会被重新赋值变为[{key: 1636695934176, value: ""}, {key: 1636695934176, value: ""}],之后再for循环取到相应索引的值{key: 1636695934176, value: ""}赋值给tempObj,之后因为条件是let len = keyArr.length; i < len - 1; ++i,所以跳出循环,最后返回大概这样子

return {
  o: tempObj, // {key: 1636695934176, value: ""}
  k: keyArr[i], // 'value'
  v: tempObj ? tempObj[keyArr[i]] : null, // ''
};

这样就能拿到model对象中相应的字段的值,然后进行校验,如果校验通过validateMessage就为空,否则就为rules里面定义的message的值,之后因为message有值,所以校验就不会通过了,valid就为false

结尾

这里主要就是讲了一下校验功能是怎么实现的,不是很难,学习记录用,里面可能有些不对的地方,后面可能会完善