0%

async-validator源码解析(五):校验方法validate

上篇 —— async-validator 源码解析(四):Schema 类 —— 将 async-validator 校验库的 Schema类 代码进行了分析。但是由于核心方法 validate 代码量过长,上次剔除了出来。本篇继续分析 async-validator 校验库的Schema类中的核心方法 validate

由于 validate 的代码整体较长,而且大量的用到了闭包和回调陷阱,各种反复横跳、consoledebugger 后,又参考了一个大佬对老版本代码的分析,终于看懂了主要的执行的逻辑。本篇按数据流向详解 validate 方法的全部代码,然后给出用到的 util 中的工具函数的分析。可以从仓库 https://github.com/MageeLin/async-validator-source-code-analysisanalysis 分支看到本篇中的代码分析。

已完成:

  1. async-validator 源码解析(一):文档翻译
  2. async-validator 源码解析(二):rule
  3. async-validator 源码解析(三):validator
  4. async-validator 源码解析(四):Schema 类
  5. async-validator 源码解析(五):校验方法 validate

validate

validate 方法的前半部分主要是在构造一个完整的series对象,后半部分是一个 asyncMap方法,本身 asyncMap 就是个挺复杂的方法,但是它又接收两个回调函数作为参数,回调函数也及其复杂,具有多层回调并且平级之间互相调用。其实作者把这块写的这么复杂的原因是为了闭包和异步。就是为了能间接操作闭包中的 errors 数组,通过每一次迭代校验将其完善成一个最终的 error 结果,再调用闭包中的 callback 将结果返回。

下面用一个数据流模型图看一下 validate 方法的工作原理:

数据流模型图

将整个过程分成三部分

  1. 生成 series 对象。黄色 💛
  2. 迭代 series 对象,连续的执行单次校验。绿色 💚
  3. 处理最终的错误对象,通过 callback 回调调用或者 promise 返回。蓝色 💙

生成 series 对象

处理参数

此处是为了把第二个参数 options 变为可选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 最重要的方法,实例上的校验方法
// - `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 把他们都返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 内部定义了个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 或合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如果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,统一了数据格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 校验规则转换。将参数 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)是在是太长了,所以把他单独拿出来分析。它的参数 dataseries 下属数组中的每一个元素,doIt 参数是用于执行下一个 rule 的校验或者最终回调。实现了 singleValidatordoIt 的来回互相调用,也就实现了迭代的效果。

内部定义的 cb 函数是 singleValidator 和 doIt 之间互相调用的间接桥梁,用来处理不同条件的特殊情况和深层校验的启动,也是实现异步校验的关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// 下面这个就是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 函数,也就是最终的处理过程。

1
2
3
(results) => {
complete(results);
};

asyncMap

异步迭代用的 asyncMap 函数并没有多长,它主要实现两个功能,第一是决定是串行还是并行的执行单步校验,第二个功能是实现异步,把整个迭代校验过程封装到一个 promise 中,实现了整体上的异步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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

异步并行校验时,遇到校验失败的情况时并不会中断执行,继续向下校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* 内部方法,异步并行校验 */
// 这里的关键就是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

异步串行校验时,遇到校验失败的情况时会立即中断执行,将当前校验结果直接返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 内部方法,异步有序校验,串行化 */
// 同样,这里的关键也是 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 返回结果的原因就是在这里,到了最终返回结果的阶段,用两种方式来返回。

1
2
3
4
callback(results);
return results.length
? reject(new AsyncValidationError(results, convertFieldsError(results)))
: resolve();

关键点

validate 校验的实现中,有几个关键的地方需要特别说明

深度校验

通过 rule 来判断是否需要深度校验。需要深度校验时将深层的 rulenew 一个新的 Schema 对象来进行校验。

1
2
3
4
5
6
7
8
9
10
11
12
// 通过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的返回。

1
2
3
4
5
6
7
8
9
const results = [];
// 这里定义的函数next和上面的类似,只不过多了total的判断
const pending = new Promise((resolve, reject) => {
const next = (errors) => {
results.push.apply(results, errors);
// ...
};
// ...
});
👆 全文结束,棒槌时间到 👇