async-validator 源码解析(二) - 核心实现

1,216 阅读7分钟

核心流程

asyncMap 方法设计的较为复杂,核心是外层遍历字段,内层遍历字段的规则,外层的 next 函数用于计算字段校验进度,顺序校验器和并行校验器内部的 countnext 函数用于迭代字段的规则执行行 singleValidator 校验.

数据流转

构造器 new Schema

构造函数代码很简单,就是调用了 this.define(rules) 生成了 this.rules,为了规范化 field 的规则为一个数组

define(rules: Rules) {
  // 必须传递一个rules对象
  if (!rules) {
    throw new Error('Cannot configure a schema with no rules');
  }
  // 必须是 object
  if (typeof rules !== 'object' || Array.isArray(rules)) {
    throw new Error('Rules must be an object');
  }
  this.rules = {};

  // 确保字段配置的规则为一个数组
  Object.keys(rules).forEach(name => {
    const item: Rule = rules[name];
    // 不是数组的时候,包装为数组
    this.rules[name] = Array.isArray(item) ? item : [item];
  });
}

验证 validator

validate(source, options, calback){
  // 1. (略)如果只有两个参数,则按 (source, callbck) 处理
  // 2. (略)如果有自定义的全局 message, 则与默认的合并
  // 3. 定义 complete 验证完成后的回调,用于处理并返回最终的异常
  function complete(results) {
    // 将 results 拍平加到 errors 数组中
    let errors = [];
    // 将 errors 数组转换成 以 field 为key的对象
    let fields = {}

    callback(errors, fields)
  }

  // 4. 遍历 rules 生成 series
  const series = {};
  Object.keys(rules).forEach((field) => {
   rules[field].forEach(rule => {
    series[field].push({
      rule,
      value,
      source,
      field
    })
   })
  })

  return asyncMap(
    series, 
    options,
    // 单个字段的验证
    sigleValidator,
    // 全部完成后的回调
    results => {
      complete(results);
    },
    source
  )

}

sigleValidator

用来验证每一个规则

  • dataseries 中的每个元素 { rule, value, source, field}
  • doIt 为内部同步或顺序迭代器传递的 countnext 函数,用于判断是否需要继续校验
(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;

  // 用于生成嵌套字段的全路径 如 a.b.c
  function addFullField(key: string, schema: RuleItem) {
    return {
      ...schema,
      fullField: `${rule.fullField}.${key}`,
      fullFields: rule.fullFields ? [...rule.fullFields, key] : [key],
    };
  }
  // cb: 生成异常信息对象,并调用内部的 next 或 count 函数来判断是否需要进行下一个校验
  function cb(e: SyncErrorType | SyncErrorType[] = []) {
    let errorList = Array.isArray(e) ? e : [e];
    // 打印错误到控制台
    if (!options.suppressWarning && errorList.length) {
      Schema.warning('async-validator:', errorList);
    }
    // 如果配置了自定义的 message,就加到 errorList 中
    if (errorList.length && rule.message !== undefined) {
      errorList = [].concat(rule.message);
    }

    // 格式化 message,并添加额外的字段信息,返回 [{message, fieldValue, field}]
    // Fill error info
    let filledErrors = errorList.map(complementError(rule, source));
    
    // 如果设置了规则:出错就停止 就回调doIt
    if (options.first && filledErrors.length) {
      // 这里设置了一下标志位 {age: 1}  未知其意
      errorFields[rule.field] = 1;
      return doIt(filledErrors);
    }

    // 如果没有嵌套字段校验,就回调doIt
    if (!deep) {
      doIt(filledErrors);
      // 嵌套校验
    } else {
      // 如果是必填,但没有值,生成必填错误,回调 doIt
      if (rule.required && !data.value) {
        if (rule.message !== undefined) {
          filledErrors = []
            .concat(rule.message)
            .map(complementError(rule, source));
        } else if (options.error) {
          filledErrors = [
            options.error(
              rule,
              format(options.messages.required, rule.field),
            ),
          ];
        }
        return doIt(filledErrors);
      }

      // 需要嵌套校验的情况 ----------------- S
      let fieldsSchema: Record<string, Rule> = {};
      // 如果有 defaultField,那么给每个字段都加上 defaultField 规则
      if (rule.defaultField) {
        Object.keys(data.value).map(key => {
          fieldsSchema[key] = rule.defaultField;
        });
      }
      // 生成 field 的规则对象
      fieldsSchema = {
        ...fieldsSchema,
        ...data.rule.fields,
      };

      const paredFieldsSchema: Record<string, RuleItem[]> = {};

      // 添加 fullField 生成字段的全路径 a.b.c
      Object.keys(fieldsSchema).forEach(field => {
        const fieldSchema = fieldsSchema[field];
        const fieldSchemaList = Array.isArray(fieldSchema)
          ? fieldSchema
          : [fieldSchema];
        paredFieldsSchema[field] = fieldSchemaList.map(
          addFullField.bind(null, field),
        );
      });
      const schema = new Schema(paredFieldsSchema);
      // 向下传递父级的 message 
      schema.messages(options.messages);

      // 向下传递父级的配置
      if (data.rule.options) {
        data.rule.options.messages = options.messages;
        data.rule.options.error = options.error;
      }
      // 继续验证子级,相当于递归
      schema.validate(data.value, data.rule.options || options, errs => {
        const finalErrors = [];
        if (filledErrors && filledErrors.length) {
          finalErrors.push(...filledErrors);
        }
        if (errs && errs.length) {
          finalErrors.push(...errs);
        }
        // 子级验证完成后,把错误集合传递给父级,向上传递
        doIt(finalErrors.length ? finalErrors : null);
      });
      // 需要嵌套校验的情况 ----------------- E
    }
  }

  // 执行 同步或异步的 validator 方法,获取结果
  // 结果类型:Boolean、Array、Error、Promise
  // 获取结果后,执行cb,进行下一步的规则校验
  let res: ValidateResult;
  
  // 异步validator
  if (rule.asyncValidator) {
    res = rule.asyncValidator(rule, data.value, cb, data.source, options);
  // 同步的validator
  } else if (rule.validator) {
    try {
      res = rule.validator(rule, data.value, cb, data.source, options);
    } catch (error) {
      console.error?.(error);
      // rethrow to report error
      if (!options.suppressValidatorError) {
        setTimeout(() => {
          throw error;
        }, 0);
      }
      // 如果验证器执行报错,则把错误收集起来
      cb(error.message);
    }

    // 对返回值做处理 boolean/array/Error
    if (res === true) {
      cb();
    } else if (res === false) {
      cb(
        typeof rule.message === 'function'
          ? rule.message(rule.fullField || rule.field)
          : rule.message || `${rule.fullField || rule.field} fails`,
      );
    } else if (res instanceof Array) {
      cb(res);
    } else if (res instanceof Error) {
      cb(res.message);
    }
  }

  // 如果返回的结果是 promise
  if (res && (res as Promise<void>).then) {
    (res as Promise<void>).then(
      () => cb(),
      e => cb(e),
    );
  }
},

asyncMap

asyncMap 函数会遍历 series,遍历每个字段和字段的规则进行校验,返回一个 promise,并调用 callback 返回最终结果

伪代码说明核心逻辑

series 主要有两层遍历,第一层是字段遍历,第二层是规则遍历,内层的规则遍历完后,会通过 next 函数将结果给外层并收集到 results 中,此时 total 加1,当所有字段遍历完后,调用 completeCallback,返回最终结果

next 函数的功能是收集所有错误,并判断字段是否全部校验完成,返回最终结果

function asyncMap(series, options, singleValidator, completeCallback, source){
  // 最终结果
  let results = [];
  let total = 0;

  return new Promise((resolve, reject) => {
    const next = errors => {
      // 将 errors 添加到 results,并保证是一维数组
      results.push.apply(results, errors);
      // 验证完一个字段就加1
      total++;
      if(total === series.length){
        // 所有字段验证完就 callback
        completeCallback(results);
        // 并返回promise
        results.length ? reject(results) : resolve(source)
      }
    }

    // 遍历 series
    Object.keys(series).forEach(key => {
      // 如果配置了 options.firstFields 
      if(options.firstFields){
        // {age: [rule1, rule2]}
        // 顺序校验规则 ,有错就终止当前字段的后面的规则校验
        asyncSerialArray(series[key], singleValidator, next);
      }else{
        // 并行校验规则,所有规则都校验,完成后一次性返回
        asyncParallelArray(series[key], singleValidator, next);
      }
    })
  })
}

下面是源代码详情注释

export function asyncMap(
  objArr: Record<string, RuleValuePackage[]>,
  option: ValidateOption,
  func: ValidateFunc,
  callback: (errors: ValidateError[]) => void,
  source: Values,
): Promise<Values> {
  // 遇到异常就中止后面字段的校验(用于嵌套校验)
  if (option.first) {
    const pending = new Promise<Values>((resolve, reject) => {
      const next = (errors: ValidateError[]) => {
        // 结束验证,callback执行,并返回promise对象,reject 或 resolve;
        callback(errors);
        return errors.length
          ? reject(new AsyncValidationError(errors, convertFieldsError(errors)))
          : resolve(source);
      };
      // 对象转数组
      const flattenArr = flattenObjArr(objArr);

      // 顺序校验,有错就返回错误,结束校验
      asyncSerialArray(flattenArr, func, next);
    });
    pending.catch(e => e);
    // 返回 promise 对象
    return pending;
  }

  // 当指定字段的第一个校验规则产生error时调用callback
  // 为true,只要遇到规则未通过,就继续校验后面规则,继续下一个字段的校验
  // 默认为false, 也就是 firstFields = [] => 并行校验
  const firstFields =
    option.firstFields === true
      ? Object.keys(objArr)
      : option.firstFields || [];

  const objArrKeys = Object.keys(objArr);
  const objArrLength = objArrKeys.length;
  let total = 0;
  const results: ValidateError[] = [];

  const pending = new Promise<Values>((resolve, reject) => {

    const next = (errors: ValidateError[]) => {
      // 收集 errors, apply 用于拍平为一维数组
      results.push.apply(results, errors);
      // 一个字段的所有规则校验完成后,加1,计数字段完成数量
      total++;
      // 所有字段都校验完成后,调用 callback -> 结束校验
      if (total === objArrLength) {
        // 调用callback 返回结果
        callback(results);
        // 同时,返回一个Promise,便于外面使用then或catch调用
        return results.length
          ? reject(
              new AsyncValidationError(results, convertFieldsError(results)),
            )
          : resolve(source);
      }
    };
    if (!objArrKeys.length) {
      callback(results);
      resolve(source);
    }
    
    // 遍历每个字段和字段的规则进行校验
    objArrKeys.forEach(key => {
      const arr = objArr[key];
      // 如果设置了 某些字段规则校验是 顺序校验
      if (firstFields.indexOf(key) !== -1) {
        // 顺序校验规则,有错就终止
        asyncSerialArray(arr, func, next);
      } else {
        // 并行校验规则,规则遍历完成后一次性返回
        asyncParallelArray(arr, func, next);
      }
    });
  });
  pending.catch(e => e);

  // 返回最终的 promise 对象
  return pending;
}

规则校验的两种模式

asyncParallelArray

并行校验规则,所有规则都校验完成后就回调 callback 进行下一个字段的校验

function asyncParallelArray(
  arr: RuleValuePackage[],
  func: ValidateFunc,
  callback: (errors: ValidateError[]) => void,
) {
  const results: ValidateError[] = [];
  let total = 0;
  const arrLength = arr.length;

  function count(errors: ValidateError[]) {
    results.push(...(errors || []));
    // 在校验完字段的 1个规则后,加1,计数规则校验数量
    total++;
    // 并行校验所有规则后,回调进行下一个字段的校验 callback -> 父next
    if (total === arrLength) {
      callback(results);
    }
  }
  // 所有规则并行执行 func (也就是 asyncMap 的检验函数 (data, doIt) => void)
  arr.forEach(a => {
    func(a, count);
  });
}

asyncSerialArray

顺序校验规则,有错就中断后面的规则校验,返回结果,回调 callback 进行下一个字段的校验

function asyncSerialArray(
  arr: RuleValuePackage[],
  func: ValidateFunc,
  callback: (errors: ValidateError[]) => void,
) {
  let index = 0;
  const arrLength = arr.length;

  function next(errors: ValidateError[]) {
    // 如果有错就回调,不再继续下一个规则,调用callback(为pending中的next)继续下一个字段
    if (errors && errors.length) {
      callback(errors);
      return;
    }
    // 没有错,就继续下一个规则
    const original = index;
    index = index + 1;

    // 顺序执行 func(也就是 asyncMap 的检验函数)
    if (original < arrLength) {
      func(arr[original], next);
    } else {
      callback([]);
    }
  }

  next([]);
}