tiodot / tiodot.github.io

总结归纳--做的东西不是每一件都留到现在 但是能通过实践,收获了如何构建一套系统,一套工具的方法论
https://xchb.work
8 stars 0 forks source link

async-validator源码分析 #15

Open tiodot opened 7 years ago

tiodot commented 7 years ago

缘由

在使用antd时,其表单验证使用一个单独的npm包async-validator实现,其使用可以参考async-validator使用。但一直很好奇:

  1. 如何实现异步校验的?
  2. 如何支持那么多中校验规则?

源码分析

1. 使用概述

首先看一个async-validator的使用例子:

var Schema = require('async-validator');
var descriptor = {
  name: {type: "string", required: true}
}
var validator = new Schema(descriptor);
validator.validate({name: "muji"}, (errors, fields) => {
  if(errors) {
    // validation failed, errors is an array of all errors
    // fields is an object keyed by field name with an array of
    // errors per field
    return handleErrors(errors, fields);
  }
  // validation passed
});

核心就是两步操作:

  1. 使用Schema构造函数构建一个实例validator;
  2. 调用validator的validate方法验证数据。

2. 纵观全局

结合上述示例,简化一下源码:

function Schema(descriptor) {
  this.rules = null;
  this._messages = defaultMessages;
  this.define(descriptor);
}
Schema.prototype = {
  define(rules){
    // 定义校验规则
    ....
  },
  validate(source, options, callback){
    // 校验数据
    ....
  }
}

主要API就这两个:一个define用来定义规则,一个validate用于根据规则校验来检测数据是否满足要求。

3. define实现

具体源码

Schema.prototype = {
  define(rules){
    .....
    // 重点来了
    this.rules = {};
    let z;
    let item;
    for (z in rules) {
      if (rules.hasOwnProperty(z)) {
        item = rules[z];
        this.rules[z] = Array.isArray(item) ? item : [item];
      }
    }
  }
  ....
}

这个api就是把定义的验证规则,整理一下然后存储到this.rules中,以备后用。 如上示例的rules是:

 {
    name: {type: "string", required: true}
 }

执行之后,将其保存到rules中:

this.rules = {
  name: [{type: "string", required: true}]
}

4. validate实现

这个API就比较有意思了,有意思的意思就是这里面的代码真的是简直了,各种callback,差些可以头冒金星。大致看看源码缩略版本:

/*这块是工具函数,放在是为了更好的看代码结构,为了更好的理解参数名称和源码有所出入,*/
function asyncMap(objArr, option, func, callback) {
  const objArrKeys = Object.keys(objArr);
  const objArrLength = objArrKeys.length;
  let total = 0;
  const results = [];
  // 是的,你没有看错这个也是个回调函数。
  const next = (errors) => {
    results.push.apply(results, errors);
    total++;
    if (total === objArrLength) {
      callback(results);
    }
  };
  objArrKeys.forEach((key) => {
    const arr = objArr[key];
    asyncParallelArray(arr, func, next);
  });
}
function asyncParallelArray(arr, func, next) {
  const results = [];
  let total = 0;
  const arrLength = arr.length;
  // 不要惊讶,这个也是个回调函数。
  function doIt(errors) {
    results.push.apply(results, errors);
    total++;
    if (total === arrLength) {
      next(results);
    }
  }

  arr.forEach((a) => {
    func(a, doIt);
  });
}
/*工具函数结束*/
// 源码精简版
Schema.prototype = {
    validate(rules) {
        //这个是内部函数1
        function complete(results) {/*用于处理返回的错误信息*/ }
        // 这块有所精简,源码中支持自定义错误提示信息,但无关大局,故暂不考虑,直接使用默认
        options.messages = this._messages;
        let arr;
        let value;
        const series = {};
        const keys = options.keys || Object.keys(this.rules);
        // 这个forEach主要是遍历需要验证的字段,然后找到对应的验证函数。
        keys.forEach((z) => {
            arr = this.rules[z];
            value = source[z];
            arr.forEach((r) => {
                let rule = r;
                rule = typeof rule === 'function' ? { validator: rule } : { ...rule };
                rule.validator = this.getValidationMethod(rule);
                rule.field = z;
                rule.fullField = rule.fullField || z;
                rule.type = this.getType(rule); // 返回rule.type || 'string'
                if (!rule.validator) return;
                series[z] = series[z] || [];
                series[z].push({ rule, value, source, field: z });
            });
        });
        // forEach 结束,然后series里面就包含验证规则,字段的值,验证函数等信息了。
        // 嗯,是不是还没有啥感觉,不过马上就应该会有了。
        asyncMap(series, options, (data, doIt) => {
            // 之前还没有觉得回调金字塔多么费劲,看来还是太年轻啊
            // 请注意这里又定义了一个内部函数
            function cb(e = []) {
                let errors = e;
                if (!Array.isArray(errors)) {
                    errors = [errors];
                }
                if (errors.length) {
                    warning('async-validator:', errors);
                }
                if (errors.length && rule.message) {
                    errors = [].concat(rule.message);
                }

                errors = errors.map(complementError(rule));
                // 高能, doIt回调函数是在这被调用,而这个函数有是一个回调函数。。。
                doIt(errors);
            }
            /** 这块就是调用对应的验证方法验证,例如对于{required: true}这种校验规则,其对应的validator为:
             * function required(rule, value, callback, source, options) {
                    const errors = [];
                    const type = Array.isArray(value) ? 'array' : typeof value;
                    rulesRequired(rule, value, source, errors, options, type);
                    callback(errors);
                }
                // 这里用到的rulesRequired为
                function rulesRequired(rule, value, source, errors, options, type) {
                    if (rule.required &&
                        (!source.hasOwnProperty(rule.field) || util.isEmptyValue(value, type || rule.type))) {
                        // util.format格式化输出结果
                        errors.push(util.format(options.messages.required, rule.fullField)); 
                    }
                }
             */
            rule.validator(rule, data.value, cb, data.source, options);

        },  /*不要忽视了这还有一个参数给asyncMap*/, (results) => {
            complete(results);
        })
    },
    getValidationMethod(rule) {
        // 根据rule中的type或者required返回相应的验证方法,如果是function的话直接返回
        if (typeof rule.validator === 'function') {
            return rule.validator;
        }
        const keys = Object.keys(rule);
        const messageIndex = keys.indexOf('message');
        if (messageIndex !== -1) {
            keys.splice(messageIndex, 1);
        }
        if (keys.length === 1 && keys[0] === 'required') {
            return validators.required;
        }
        return validators[this.getType(rule)] || false;
    }
}

来来来,不用怕,来分析分析,其流程就是:

  1. 转化和保证校验规则到series中,映射实际的校验函数,然后添加字段值等一些信息
  2. 调用asyncMap函数,提供两个回调函数,一个是实际验证数据使用,暂时给其命名为asyncValidateMethod;一个处理全部校验结果,可以称之为validateFinished
  3. asyncMap函数中对具体字段调用asyncParallelArray进行校验,然后收集字段的所有错误信息,收集完之后,调用validateFinished
  4. asyncParallelArray函数针对具体字段的单个规则调用asyncValidateMethod对其进行验证,同时这个函数里面会针对具体字段收集其错误信息,因为一个字段可以对应多个验证规则,所以可能会有多条错误信息,然后完之后,汇总到asyncMap中。
  5. 最后所有都执行完之后,然后调用validate的回调函数。

答疑解惑

1. 如何实现异步校验的?

看完源码之后,除了回调比较多之外,好像没有在哪发现使用了异步。为了验证这个想法, 首先我们构造一段代码:

const schema = new Schema({
    v: [{
        required,
        message: 'no',
    }],
});
let error;
console.log('validate starting....');
schema.validate({
    v: [],
}, (errors) => {
    console.log('validate callback function');
    error = errors;
})
console.log('validate end & result is: ', error);

如果验证是异步的话,打印结果应该是:

validate starting....
validate end & result is: undefined
validate callback function

然而实际结果是:

validate starting....
validate callback function
validate end & result is:  [{message: 'no', field: 'v'}]

所以这个其实不是异步校验,完全就是一个同步进行的过程。

2. 如何支持那么多中校验规则?

看看目录情况就比较好理解了: image 有一个rule和一个validator目录,validator目录放的就是支持的所有的验证规则,实际校验行为rule目录下的文件完成的。 例如最简单的必填校验({require: true}),首先会调用validator/required.js中方法:

import rules from '../rule/';

function required(rule, value, callback, source, options) {
  const errors = [];
  const type = Array.isArray(value) ? 'array' : typeof value;
  rules.required(rule, value, source, errors, options, type);
  callback(errors);
}

export default required;

这个里面又会使用到rule/required.js中的方法:

import * as util from '../util';
function required(rule, value, source, errors, options, type) {
  // 这个是真正的校验的地方
  if (rule.required &&
    (!source.hasOwnProperty(rule.field) || util.isEmptyValue(value, type || rule.type))) {
    errors.push(util.format(options.messages.required, rule.fullField));
  }
}

export default required;

待探索

  1. callback函数与异步的关系
  2. 如何更加优雅的实现校验(validate api)