上篇 —— async-validator 源码解析(四):Schema 类 —— 将 async-validator 校验库的 Schema类 代码进行了分析。但是由于核心方法 validate 代码量过长,上次剔除了出来。本篇继续分析 async-validator 校验库的Schema类中的核心方法 validate。
由于 validate 的代码整体较长,而且大量的用到了闭包和回调陷阱,各种反复横跳、console 加 debugger 后,又参考了一个大佬对老版本代码的分析,终于看懂了主要的执行的逻辑。本篇按数据流向详解 validate 方法的全部代码,然后给出用到的 util 中的工具函数的分析。可以从仓库 github.com/MageeLin/as… 的 analysis 分支看到本篇中的代码分析。
本系列已完成:
- async-validator 源码解析(一):文档翻译
- async-validator 源码解析(二):rule
- async-validator 源码解析(三):validator
- async-validator 源码解析(四):Schema 类
- async-validator 源码解析(五):校验方法 validate
validate
validate 方法的前半部分主要是在构造一个完整的series对象,后半部分是一个 asyncMap方法,本身 asyncMap 就是个挺复杂的方法,但是它又接收两个回调函数作为参数,回调函数也及其复杂,具有多层回调并且平级之间互相调用。其实作者把这块写的这么复杂的原因是为了闭包和异步。就是为了能间接操作闭包中的 errors 数组,通过每一次迭代校验将其完善成一个最终的 error 结果,再调用闭包中的 callback 将结果返回。
下面用一个数据流模型图看一下 validate 方法的工作原理:
将整个过程分成三部分
- 生成
series对象。黄色 💛 - 迭代
series对象,连续的执行单次校验。绿色 💚 - 处理最终的错误对象,通过
callback回调调用或者promise返回。蓝色 💙
生成 series 对象
处理参数
此处是为了把第二个参数 options 变为可选。
// 最重要的方法,实例上的校验方法
// - `source_`: 需要校验的对象(必选)。
// - `o`: 描述校验的处理选项的对象(可选)。
// - `oc`: 当校验完成时调用的回调函数(必选)。
validate(source_, o = {}, oc = () => {}) {
let source = source_;
let options = o;
let callback = oc;
// 参数变换,因为options是可选的,所以第二个参数为函数时
// 说明第二个参数是callback,options自然就是空对象
if (typeof options === 'function') {
callback = options;
options = {};
}
// ......
}
定义 complete 函数
定义这个函数是为了完善 errors数组 为 fields对象,然后用 callback 把他们都返回。
// 内部定义了个complete 函数,目的是如何callback最后生成的 errors 和 fields
// 入参results = [{ message, field }]
// 返回的结果是errors = [{ field, message }]和参数fields = { fullFieldName: [{field, message}] }
// 然后把 errors 和 fields 传给 callback 调用
function complete(results) {
// 初始化
let i;
let errors = [];
let fields = {};
// 内部的内部定义了一个add函数
function add(e) {
// 给闭包中的errors添加新的error
if (Array.isArray(e)) {
errors = errors.concat(...e);
} else {
errors.push(e);
}
}
// 迭代参数results,把results中的每个error加到errors数组中
for (i = 0; i < results.length; i++) {
add(results[i]);
}
// 最后的结果里,如果什么error都没有,就返回null
if (!errors.length) {
errors = null;
fields = null;
} else {
// 要不然就把数组形式的errors转换格式
// 把errors中相同field的error合并,转化为对象的形式
fields = convertFieldsError(errors);
}
// 最后callback调用数组形式和对象形式的errors
callback(errors, fields);
}
options.messages
处理 messages,根据情况使用默认 messages 或合并。
// 如果options中给了messages属性
// 就需要合并messages
if (options.messages) {
// 调用实例上的messages方法创建一个message
// 其实就是默认的message
let messages = this.messages();
if (messages === defaultMessages) {
messages = newMessages();
}
// 将options的messages与默认的messages合并后赋值给options.messages
deepMerge(messages, options.messages);
options.messages = messages;
} else {
// options没有messages属性就给个默认值
options.messages = this.messages();
}
生成 series 对象
在这一步生成了本层深度的最终 series,统一了数据格式。
// 校验规则转换。将参数 rules 复合为 series 对象
// series = { key: [{ rule, value, source, field }] }
let arr;
let value;
const series = {};
// keys是rules的所有键
// 要注意此处的rules其实是单层的rule,所以每一个深度都要执行一次
const keys = options.keys || Object.keys(this.rules);
keys.forEach((z) => {
// arr是rule[z]的,是一个数组
// 存放的是该字段对应的所有rule
arr = this.rules[z];
// value是source[z],是一个值或者对象
value = source[z];
// 迭代z这个字段的所有rule
arr.forEach((r) => {
let rule = r;
// 当有transform属性而且是个函数时,要提前把值转换
if (typeof rule.transform === 'function') {
// 浅拷贝下,打破引用
if (source === source_) {
source = { ...source };
}
// 转换value
value = source[z] = rule.transform(value);
}
// 当rule本身就是个function时,赋值给validator统一处理
if (typeof rule === 'function') {
rule = {
validator: rule,
};
// 不是function时,浅拷贝打破引用
} else {
rule = { ...rule };
}
// 规范validator属性,统一处理方式
rule.validator = this.getValidationMethod(rule);
// 给rule加上field、fullField和type
rule.field = z;
rule.fullField = rule.fullField || z;
rule.type = this.getType(rule);
// 异常处理
if (!rule.validator) {
return;
}
// 最后生成了完整了series = { key: [{ rule, value, source, field }] }
series[z] = series[z] || [];
series[z].push({
rule,
value,
source,
field: z,
});
});
});
迭代 series 对象
迭代 series 时用的是 asyncMap(objArr, option, func, callback) 。为了便于理解,按照实际效果,把参数是命名为asyncMap(series, option, singleValidator, completeCallback);
singleValidator
singleValidator(data, doIt)是在是太长了,所以把他单独拿出来分析。它的参数 data 是 series 下属数组中的每一个元素,doIt 参数是用于执行下一个 rule 的校验或者最终回调。实现了 singleValidator 和 doIt 的来回互相调用,也就实现了迭代的效果。
内部定义的 cb 函数是 singleValidator 和 doIt 之间互相调用的间接桥梁,用来处理不同条件的特殊情况和深层校验的启动,也是实现异步校验的关键。
// 下面这个就是func函数,不管是并行还是串行,都要用这个func来校验和添加error
// 第一个参数data = { rule, value, source, field },也就是series中的每一个元素
// 第二个参数 doIt 是 next 函数,doIt 函数用于执行下一个校验器或者最终回调,如下:
// if(options.first) {
// 执行 asyncSerialArray 函数处理参数错误对象数组,将直接调用completeCallback回调,中断后续校验器的执行
// } else {
// 执行 asyncParallelArray 函数将所有校验器的错误对象数组构建成单一数组,供completeCallback回调处理
// }
(data, doIt) => {
const rule = data.rule;
// 通过rule.type、rule.fields、rule.defaultField判断是否深度校验。若是,内部变量deep置为真。
let deep =
(rule.type === 'object' || rule.type === 'array') &&
(typeof rule.fields === 'object' || typeof rule.defaultField === 'object');
deep = deep && (rule.required || (!rule.required && data.value));
rule.field = data.field;
// 定义addFullfield函数,用于获取深度校验时嵌套对象属性的fullField。
function addFullfield(key, schema) {
return {
...schema,
fullField: `${rule.fullField}.${key}`,
};
}
// 定义单次校验后执行的回调函数cb。cb的实现机制中,
// 包含将错误对象加工为[{ field, message }]数据格式;
function cb(e = []) {
// 确保封装成数组
let errors = e;
if (!Array.isArray(errors)) {
errors = [errors];
}
// 如果没有取消内部警告,并且errors数组长度不为0,就弹出警告
if (!options.suppressWarning && errors.length) {
Schema.warning('async-validator:', errors);
}
// 如果errors数组长度不为0,并且有message,就替换成message
if (errors.length && rule.message) {
// 这个写法是保证数组格式
errors = [].concat(rule.message);
}
// 比如,errors本来是 ["姓名为必填项"]
errors = errors.map(complementError(rule));
// 补充完是[{message: "姓名为必填项", field: "name"}]
// 当options设置了first属性后,并且有error时,就该doIt返回了
if (options.first && errors.length) {
errorFields[rule.field] = 1;
return doIt(errors);
}
// 当rule深度只有一层,也该直接doIt返回
if (!deep) {
doIt(errors);
} else {
// 如果rule是required的,但是在rule级别上目标对象不存在,那么就不继续向下
if (rule.required && !data.value) {
// rule的message存在就完善下
if (rule.message) {
errors = [].concat(rule.message).map(complementError(rule));
// 未公开的属性?自己决定怎么处理errors
} else if (options.error) {
errors = [
options.error(rule, format(options.messages.required, rule.field)),
];
}
// 相当于是在deep没有到头却遇到了该停的地方,也该直接doIt返回
return doIt(errors);
}
// 新建一个fieldsSchema对象
let fieldsSchema = {};
if (rule.defaultField) {
for (const k in data.value) {
if (data.value.hasOwnProperty(k)) {
fieldsSchema[k] = rule.defaultField;
}
}
}
// 合并
fieldsSchema = {
...fieldsSchema,
...data.rule.fields,
};
// 合并完之后格式如下:
// { name: rule{} }
// 数组化并添加fullField
for (const f in fieldsSchema) {
if (fieldsSchema.hasOwnProperty(f)) {
const fieldSchema = Array.isArray(fieldsSchema[f])
? fieldsSchema[f]
: [fieldsSchema[f]];
fieldsSchema[f] = fieldSchema.map(addFullfield.bind(null, f));
}
}
// 完成之后格式如下
// [ rule{} ]
// 在这里又new了一个新的Schema对象,用于验证更深一级的value
const schema = new Schema(fieldsSchema);
schema.messages(options.messages);
// 如果自身的rule有option,就给它配上上一层的options
if (data.rule.options) {
data.rule.options.messages = options.messages;
data.rule.options.error = options.error;
}
// 子Schema对象依然要去执行实例的validate方法,类似递归
schema.validate(data.value, data.rule.options || options, (errs) => {
const finalErrors = [];
if (errors && errors.length) {
finalErrors.push(...errors);
}
if (errs && errs.length) {
finalErrors.push(...errs);
}
// 把子规则的验证结果也要返回
doIt(finalErrors.length ? finalErrors : null);
});
}
}
let res;
// 如果指定了asyncValidator属性,就优先调用async,否则就去执行validator
if (rule.asyncValidator) {
res = rule.asyncValidator(rule, data.value, cb, data.source, options);
} else if (rule.validator) {
res = rule.validator(rule, data.value, cb, data.source, options);
//
if (res === true) {
cb();
} else if (res === false) {
cb(rule.message || `${rule.field} fails`);
} else if (res instanceof Array) {
cb(res);
} else if (res instanceof Error) {
cb(res.message);
}
}
// 若返回Promise实例,cb将在该Promise实例的then方法中执行。
if (res && res.then) {
// 利用这个promise的then结构实现了异步的校验
res.then(
() => cb(),
(e) => cb(e)
);
}
};
completeCallback
这个是方法是去调用前文解析过的闭包中 complete 函数,也就是最终的处理过程。
(results) => {
complete(results);
};
asyncMap
异步迭代用的 asyncMap 函数并没有多长,它主要实现两个功能,第一是决定是串行还是并行的执行单步校验,第二个功能是实现异步,把整个迭代校验过程封装到一个 promise 中,实现了整体上的异步。
export function asyncMap(objArr, option, func, callback) {
// 如果option.first选项为真,说明第一个error产生时就要报错
if (option.first) {
// pending是一个promise
const pending = new Promise((resolve, reject) => {
// 定义一个函数next,这个函数先调用callback,参数是errors
// 再根据errors的长度决定resolve还是reject
const next = (errors) => {
callback(errors);
return errors.length
? // reject的时候,返回一个AsyncValidationError的实例
// 实例化时第一个参数是errors数组,第二个参数是对象类型的errors
reject(new AsyncValidationError(errors, convertFieldsError(errors)))
: resolve();
};
// 把对象扁平化为数组flattenArr
const flattenArr = flattenObjArr(objArr);
// 串行
asyncSerialArray(flattenArr, func, next);
});
// 捕获error
pending.catch((e) => e);
// 返回promise实例
return pending;
}
// 如果option.first选项为假,说明所有的error都产生时才报错
// 当指定字段的第一个校验规则产生error时调用callback,不再继续处理相同字段的校验规则。
let firstFields = option.firstFields || [];
// true意味着所有字段生效。
if (firstFields === true) {
firstFields = Object.keys(objArr);
}
const objArrKeys = Object.keys(objArr);
const objArrLength = objArrKeys.length;
let total = 0;
const results = [];
// 这里定义的函数next和上面的类似,只不过多了total的判断
const pending = new Promise((resolve, reject) => {
const next = (errors) => {
results.push.apply(results, errors);
// 只有全部的校验完才能执行最后的callback和reject
total++;
if (total === objArrLength) {
// 这个callback和reject/resolve是这个库既能回调函数又能promise的核心
callback(results);
return results.length
? reject(
new AsyncValidationError(results, convertFieldsError(results))
)
: resolve();
}
};
if (!objArrKeys.length) {
callback(results);
resolve();
}
// 当firstFields中指定了该key时,说明该字段的第一个校验失败产生时就停止并调用callback
// 所以是串行的asyncSerialArray
// 没有指定该key,说明该字段的校验error需要都产生,就并行asyncParallelArray
objArrKeys.forEach((key) => {
const arr = objArr[key];
if (firstFields.indexOf(key) !== -1) {
asyncSerialArray(arr, func, next);
} else {
asyncParallelArray(arr, func, next);
}
});
});
// 捕获error,添加错误处理
pending.catch((e) => e);
// 返回promise实例
return pending;
}
asyncParallelArray
异步并行校验时,遇到校验失败的情况时并不会中断执行,继续向下校验。
/* 内部方法,异步并行校验 */
// 这里的关键就是asyncParallelArray -> doIt -> cb -> asyncParallelArray,循环调用实现的每一次校验
// arr的格式:[{ rule, value, source, field }]
// func的格式:
// 第一个参数data = { rule, value, source, field },也就是series中的每一个元素
// 第二个参数 doIt 是 next 函数,doIt 函数用于执行下一个校验器或者最终回调,如下:
// if(options.first) {
// 执行 asyncSerialArray 函数处理参数错误对象数组,将直接调用completeCallback回调,中断后续校验器的执行
// } else {
// 执行 asyncParallelArray 函数将所有校验器的错误对象数组构建成单一数组,供completeCallback回调处理
// }
// (data, doIt) => {
// const rule = data.rule;
// let deep =
// (rule.type === 'object' || rule.type === 'array') &&
// (typeof rule.fields === 'object' ||
// typeof rule.defaultField === 'object');
// deep = deep && (rule.required || (!rule.required && data.value));
// rule.field = data.field;
// function addFullfield(key, schema) {}
// function cb(e = []) {}
// let res;
// if (rule.asyncValidator) {} else if (rule.validator) {}
// if (res && res.then) {}
// },
// callback的格式:
// function next(errors) {
// callback(errors);
// return errors.length ? reject(new AsyncValidationError(errors, convertFieldsError(errors))) : resolve();
// };
function asyncParallelArray(arr, func, callback) {
const results = [];
let total = 0;
const arrLength = arr.length;
// 不断的给结果数组添加error
function count(errors) {
results.push.apply(results, errors);
// errors条数和数组大小一致时结束
total++;
if (total === arrLength) {
callback(results);
}
}
// 给arr中每一条都调用func方法,形成了并行处理
arr.forEach((a) => {
func(a, count); // 执行func(element, count),
});
}
asyncSerialArray
异步串行校验时,遇到校验失败的情况时会立即中断执行,将当前校验结果直接返回。
/* 内部方法,异步有序校验,串行化 */
// 同样,这里的关键也是 asyncSerialArray -> doIt -> asyncSerialArray ,
// 循环调用实现的每一次校验
function asyncSerialArray(arr, func, callback) {
let index = 0;
const arrLength = arr.length;
// 定义一个next内部方法
function next(errors) {
// 当errors有内容时
if (errors && errors.length) {
callback(errors); // 用callback调用errors
return;
}
// 当errors没有内容时
const original = index;
index = index + 1; // 闭包index + 1
// 当前的index比length小时
if (original < arrLength) {
func(arr[original], next); // 执行func(element, next),形成了递归
} else {
callback([]); // 否则调用callback([]);
}
}
// 这里面的几个方法都是用callback来进行的最后返回
next([]);
}
处理错误对象并返回
既能用回调函数也能用 promise 返回结果的原因就是在这里,到了最终返回结果的阶段,用两种方式来返回。
callback(results);
return results.length
? reject(new AsyncValidationError(results, convertFieldsError(results)))
: resolve();
关键点
在 validate 校验的实现中,有几个关键的地方需要特别说明
深度校验
通过 rule 来判断是否需要深度校验。需要深度校验时将深层的 rule 来 new 一个新的 Schema 对象来进行校验。
// 通过rule.type、rule.fields、rule.defaultField判断是否深度校验。若是,内部变量deep置为真。
let deep =
(rule.type === 'object' || rule.type === 'array') &&
(typeof rule.fields === 'object' || typeof rule.defaultField === 'object');
deep = deep && (rule.required || (!rule.required && data.value));
// ...
if(deep) {
const schema = new Schema(fieldsSchema);
schema.validate(...)
}
// ...
异步校验
异步校验的关键就是 asyncMap(开始) -> asyncParallelArray(asyncSerialArray) -> doIt -> cb(在这里实现异步) -> asyncParallelArray(asyncSerialArray)-> ... -> completeCallback(返回最终结果),循环调用实现的每一次校验。
统一错误处理
外层创建了一个 result 数组,内层定义了一个 next 函数,不管在哪里调用 next,都会向闭包中的 result 数组中统一添加 error,便于最终改为errors和field的返回。
const results = [];
// 这里定义的函数next和上面的类似,只不过多了total的判断
const pending = new Promise((resolve, reject) => {
const next = (errors) => {
results.push.apply(results, errors);
// ...
};
// ...
});