Schema class — The Code of the Schema class of the Async-Validator verification library was analyzed. However, due to the long validate code of the core method, it was removed last time. This article continues to analyze validate, the core method in the Schema class of the Async-Validator validation library.

Due to the long validate code and the extensive use of closures and callback traps, I finally understood the main execution logic after repeatedly jumping, adding console to the debugger, and referring to a big guy’s analysis of the old version of the code. This article elaborates all the code of validate method according to the data flow, and then gives the analysis of the utility function used in util. Available from the warehouse github.com/MageeLin/as… See the code analysis section in this article.

This series has been completed:

  1. Async-validator source code parsing (a) : document translation
  2. Async-validator source code parsing (2) : async-validator
  3. Async-validator source code parsing (c) : Validator
  4. Async-validator source code parsing (4) : Schema class
  5. Async-validator source code parsing (5) : validate method validate

validate

The first half of the validate method is mainly to construct a complete series object, and the second half is an asyncMap method. AsyncMap itself is a very complex method, but it takes two callback functions as parameters, and the callback functions are also very complex. There are multiple levels of callbacks and horizontal levels call each other. The reason the author made this so complicated is because of closures and asynchrony. In order to operate indirectly on the errors array in the closure, refine it into a final error result with each iteration check, and call the closure callback to return the result.

To see how the validate method works, use a data flow model diagram:

Break the process down into three parts

  1. generateseriesObject. Yellow 💛
  2. The iterationseriesObject that continuously performs a single check. Green 💚
  3. Handle the final error object throughcallbackTo call back to orpromiseTo return. Blue 💙

Generating a series object

Processing parameters

To make the second parameter options optional.

// The most important method, the validation method on the instance
// - 'source_' : object to be verified (mandatory).
// - 'o' : object (optional) describing the processing options of the validation.
// - 'oc' : callback function to be called when verification is complete (mandatory).
validate(source_, o = {}, oc = () => {}) {
  let source = source_;
  let options = o;
  let callback = oc;
  // Parameter transformation, since options is optional, so the second argument is function
  // The second argument is callback, and options is empty
  if (typeof options === 'function') {
    callback = options;
    options = {};
  }
  / /...
}
Copy the code

Defining the complete function

This function is defined to refine the Errors array into fields objects and then return them all with callback.

// Call errors and fields (); // Call errors and fields ()
// results = [{message, field}]
Errors = [{field, message}] and fields = {fullFieldName: [{field, message}]}
// Then pass errors and fields to the callback call
function complete(results) {
  / / initialization
  let i;
  let errors = [];
  let fields = {};

  // The inner inner defines an add function
  function add(e) {
    // Add a new error to errors in the closure
    if (Array.isArray(e)) { errors = errors.concat(... e); }else{ errors.push(e); }}// Iterate over results, adding each error in results to the errors array
  for (i = 0; i < results.length; i++) {
    add(results[i]);
  }
  // Return null if there is no error in the result
  if(! errors.length) { errors =null;
    fields = null;
  } else {
    // Otherwise, format the array errors
    // Merge errors of the same field in errors into object form
    fields = convertFieldsError(errors);
  }
  // Finally callback calls errors in array form and object form
  callback(errors, fields);
}
Copy the code

options.messages

Process messages, using default messages or merge as appropriate.

// If options is given the messages attribute
// Merge messages
if (options.messages) {
  Call the Messages method on the instance to create a message
  // This is the default message
  let messages = this.messages();
  if (messages === defaultMessages) {
    messages = newMessages();
  }
  // Merge the messages of options with the default messages and assign the value to options.messages
  deepMerge(messages, options.messages);
  options.messages = messages;
} else {
  // options has no messages attribute and gives a default value
  options.messages = this.messages();
}
Copy the code

Generating a series object

In this step, the final series of this depth is generated, unifying the data format.

// Check rule conversion. Compound the rules argument into a series object
// series = { key: [{ rule, value, source, field }] }
let arr;
let value;
const series = {};
// keys are all keys of rules
// Note that rules here are single-layer rules, so each depth is executed once
const keys = options.keys || Object.keys(this.rules);
keys.forEach((z) = > {
  // arr is an array of rules [z]
  // Store all rules corresponding to this field
  arr = this.rules[z];
  // value is source[z], which is a value or object
  value = source[z];
  // Iterate over all rules in the z field
  arr.forEach((r) = > {
    let rule = r;
    // If there is a transform attribute and it is a function, the value should be converted in advance
    if (typeof rule.transform === 'function') {
      // Shallow copy to break the reference
      if(source === source_) { source = { ... source }; }/ / convert the value
      value = source[z] = rule.transform(value);
    }
    // When a rule is itself a function, the assignment is handled by the validator
    if (typeof rule === 'function') {
      rule = {
        validator: rule,
      };
      // Shallow copy breaks references when not function
    } else{ rule = { ... rule }; }// Specify the validator attribute
    rule.validator = this.getValidationMethod(rule);
    // Add field, fullField, and type to rule
    rule.field = z;
    rule.fullField = rule.fullField || z;
    rule.type = this.getType(rule);
    // Exception handling
    if(! rule.validator) {return;
    }
    Series = {key: [{rule, value, source, field}]}
    series[z] = series[z] || [];
    series[z].push({
      rule,
      value,
      source,
      field: z,
    });
  });
});
Copy the code

Iterating over a series object

AsyncMap (objArr, Option, func, callback) is used to iterate over series. To make it easier to understand, name the argument asyncMap(series, Option, singleValidator, completeCallback).

singleValidator

The singleValidator(data, doIt) is too long, so take it out for analysis. Its argument data is each element in the series array, and its argument doIt is used to perform the checksum or final callback for the next rule. This allows the singleValidator and doIt to call each other back and forth, thus achieving an iterative effect.

Internally defined CB functions serve as an indirect bridge between singleValidator and doIt. They are used to handle special cases of different conditions and the initiation of deep validations, and are also key to implementing asynchronous validations.

// The following is the func function, whether parallel or serial, to use the func checksum add error
// The first argument data = {rule, value, source, field}, that is, each element in the series
// The second argument doIt is the next function, which is used to execute the next validator or final callback as follows:
// if(options.first) {
// Execute the asyncSerialArray function to handle an array of parameter error objects, which calls the completeCallback callback directly, interrupting subsequent validator execution
// } else {
// Execute the asyncParallelArray function to organize the number of error objects for all verifiers into a single array for processing by the completeCallback callback
// }
(data, doIt) => {
  const rule = data.rule;
  Type, rule-fields, and rule-defaultfield are used to determine whether depth check is performed. If so, the internal variable deep is set to true.
  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;// Define addFullfield function to get fullField of nested object attributes during depth check.
  function addFullfield(key, schema) {
    return {
      ...schema,
      fullField: `${rule.fullField}.${key}`}; }// Define the callback function cb to execute after a single check. In the implementation mechanism of CB,
  [{field, message}];
  function cb(e = []) {
    // Make sure to wrap it into an array
    let errors = e;
    if (!Array.isArray(errors)) {
      errors = [errors];
    }
    // If the internal warning is not cancelled and the errors array length is not 0, the warning is displayed
    if(! options.suppressWarning && errors.length) { Schema.warning('async-validator:', errors);
    }
    // If the length of the errors array is not 0 and there is a message, it is replaced with message
    if (errors.length && rule.message) {
      // This is an array format
      errors = [].concat(rule.message);
    }

    // For example, errors would have been [" Name required "]
    errors = errors.map(complementError(rule));
    [{message: "name is mandatory ", field: "name"}]

    // doIt returns when the first property is set for options and there is an error
    if (options.first && errors.length) {
      errorFields[rule.field] = 1;
      return doIt(errors);
    }
    // When the rule depth is only one layer, it should also be doIt
    if(! deep) { doIt(errors); }else {
      // If the rule is required, but the target object does not exist at the rule level, then no further down is done
      if(rule.required && ! data.value) {// The rule message exists
        if (rule.message) {
          errors = [].concat(rule.message).map(complementError(rule));
          // Unpublished attributes? Decide what to do with errors
        } else if (options.error) {
          errors = [
            options.error(rule, format(options.messages.required, rule.field)),
          ];
        }
        // If you are deep and you don't know where to stop, you should doIt back
        return doIt(errors);
      }

      // Create a fieldsSchema object
      let fieldsSchema = {};
      if (rule.defaultField) {
        for (const k in data.value) {
          if(data.value.hasOwnProperty(k)) { fieldsSchema[k] = rule.defaultField; }}}/ / mergefieldsSchema = { ... fieldsSchema, ... data.rule.fields, };// After merging, the format is as follows:
      // { name: rule{} }

      // add 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)); }}// The format is as follows
      // [ rule{} ]

      // A new Schema object is created to validate deeper values
      const schema = new Schema(fieldsSchema);
      schema.messages(options.messages);
      // If your rule has options, give it the options of the previous layer
      if (data.rule.options) {
        data.rule.options.messages = options.messages;
        data.rule.options.error = options.error;
      }
      // Subschema objects still perform the instance's validate method, similar to recursion
      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); }// Return the validation result of the subrule as well
        doIt(finalErrors.length ? finalErrors : null); }); }}let res;
  // If asyncValidator is specified, async is called first, otherwise the validator is executed
  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); }}// If a Promise instance is returned, cb is executed in the then method of that Promise instance.
  if (res && res.then) {
    // Use the promise's then structure to implement asynchronous validation
    res.then(
      () = > cb(),
      (e) = >cb(e) ); }};Copy the code

completeCallback

The method is to call the complete function in the closure that was parsed earlier, which is the final processing.

(results) => {
  complete(results);
};
Copy the code

asyncMap

The asyncMap function used for asynchronous iteration is not long, and it mainly realizes two functions: the first is to decide whether to perform single-step verification in serial or parallel; the second function is to realize asynchracy, encapsulating the whole iterative verification process into a promise, realizing asynchracy on the whole.

export function asyncMap(objArr, option, func, callback) {
  // If the option.first option is true, the first error is reported
  if (option.first) {
    // Pending is a promise
    const pending = new Promise((resolve, reject) = > {
      // Define a function next, which first calls callback with errors
      // Determine resolve or reject according to the length of errors
      const next = (errors) = > {
        callback(errors);
        return errors.length
          ? // reject returns an instance of AsyncValidationError
            The first argument to instantiation is the errors array, and the second argument is the errors of the object type
            reject(new AsyncValidationError(errors, convertFieldsError(errors)))
          : resolve();
      };
      // Flattens the object into an array arr
      const flattenArr = flattenObjArr(objArr);
      / / serial
      asyncSerialArray(flattenArr, func, next);
    });
    / / capture the error
    pending.catch((e) = > e);
    // Return the Promise instance
    return pending;
  }

  // If option.first is false, all errors are generated
  Callback is called when the first validation rule of the specified field generates an error.
  let firstFields = option.firstFields || [];
  // true means all fields are in effect.
  if (firstFields === true) {
    firstFields = Object.keys(objArr);
  }
  const objArrKeys = Object.keys(objArr);
  const objArrLength = objArrKeys.length;
  let total = 0;
  const results = [];
  // The next function defined here is similar to the above, but with a total judgment
  const pending = new Promise((resolve, reject) = > {
    const next = (errors) = > {
      results.push.apply(results, errors);
      Callback and reject are executed only after all the validation is complete
      total++;
      if (total === objArrLength) {
        // callback and reject/resolve are at the heart of the library's ability to callback functions and promise
        callback(results);
        return results.length
          ? reject(
              newAsyncValidationError(results, convertFieldsError(results)) ) : resolve(); }};if(! objArrKeys.length) { callback(results); resolve(); }// When the key is specified in firstFields, the first validation failure for the field is stopped and the callback is called
    // Serial asyncSerialArray
    // If this key is not specified, the verification error of this field must be generated, so asyncParallelArray is parallel
    objArrKeys.forEach((key) = > {
      const arr = objArr[key];
      if(firstFields.indexOf(key) ! = = -1) {
        asyncSerialArray(arr, func, next);
      } else{ asyncParallelArray(arr, func, next); }}); });// Catch error and add error handling
  pending.catch((e) = > e);
  // Return the Promise instance
  return pending;
}
Copy the code

asyncParallelArray

In asynchronous parallel verification, if the verification fails, the execution is not interrupted and the verification continues.

/* Internal method, asynchronous parallel verification */
// The key here is asyncParallelArray -> doIt -> cb -> asyncParallelArray
[{rule, value, source, field}]

// The format of func:
// The first argument data = {rule, value, source, field}, that is, each element in the series
// The second argument doIt is the next function, which is used to execute the next validator or final callback as follows:
// if(options.first) {
// Execute the asyncSerialArray function to handle an array of parameter error objects, which calls the completeCallback callback directly, interrupting subsequent validator execution
// } else {
// Execute the asyncParallelArray function to organize the number of error objects for all verifiers into a single array for processing by the completeCallback callback
// }
// (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 format:
// 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;

  // Keep adding errors to the result array
  function count(errors) {
    results.push.apply(results, errors);
    // The number of errors is the same as the size of the array
    total++;
    if(total === arrLength) { callback(results); }}// Call the func method for each entry in the ARR, resulting in parallel processing
  arr.forEach((a) = > {
    func(a, count); // call func(element, count),
  });
}
Copy the code

asyncSerialArray

In asynchronous serial verification, if the verification fails, the system interrupts the execution immediately and returns the verification result directly.

/* Internal method, asynchronous order check, serialization */
AsyncSerialArray -> doIt -> asyncSerialArray
// Loop over each validation of the implementation
function asyncSerialArray(arr, func, callback) {
  let index = 0;
  const arrLength = arr.length;

  // Define a next internal method
  function next(errors) {
    // When errors has content
    if (errors && errors.length) {
      callback(errors); // Call errors with callback
      return;
    }
    // When errors has no content
    const original = index;
    index = index + 1; // Closure index + 1
    // The current index is shorter than length
    if (original < arrLength) {
      func(arr[original], next); // Execute func(element, next) to form recursion
    } else {
      callback([]); // Otherwise call callback([]);}}// All of these methods use callback as the final return
  next([]);
}
Copy the code

Process the error object and return

The reason you can return a result using either a callback function or a promise is because here, at the end of the return phase, you can return it in two ways.

callback(results);
return results.length
  ? reject(new AsyncValidationError(results, convertFieldsError(results)))
  : resolve();
Copy the code

The key point

There are several key points to note in the implementation of validate

The depth of the check

Rule is used to determine whether depth check is required. When deep validation is required, the deep rule is used to create a new Schema object for validation.

Type, rule-fields, and rule-defaultfield are used to determine whether depth check is performed. If so, the internal variable deep is set to true.
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(...)
}
// ...
Copy the code

Asynchronous check

The key to asynchronous verification is asyncMap -> asyncParallelArray -> doIt -> CB -> asyncSerialArray AsyncParallelArray (asyncSerialArray) ->… -> completeCallback (returns the final result), which loops over each verification implemented.

Unified error handling

The outer layer creates a result array, and the inner layer defines a next function that adds error to the result array in the closure wherever next is called, making it easier to return errors and field.

const results = [];
// The next function defined here is similar to the above, but with a total judgment
const pending = new Promise((resolve, reject) = > {
  const next = (errors) = > {
    results.push.apply(results, errors);
    // ...
  };
  // ...
});
Copy the code