前言
最近自己封装了一些组件,闲着没事,就像把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
结尾
这里主要就是讲了一下校验功能是怎么实现的,不是很难,学习记录用,里面可能有些不对的地方,后面可能会完善