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

2,089 阅读10分钟

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

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

本系列已完成:

  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 变为可选。

// 最重要的方法,实例上的校验方法
// - `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)是在是太长了,所以把他单独拿出来分析。它的参数 dataseries 下属数组中的每一个元素,doIt 参数是用于执行下一个 rule 的校验或者最终回调。实现了 singleValidatordoIt 的来回互相调用,也就实现了迭代的效果。

内部定义的 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 来判断是否需要深度校验。需要深度校验时将深层的 rulenew 一个新的 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);
    // ...
  };
  // ...
});