最熟悉的陌生人rc-form

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰! javascript

這是第 107 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客:最熟悉的陌生人rc-form前端

清風.png

rc-form 是誰?

咱們也許會常用例如 Ant Design、Element UI、Vant 等第三方組件庫來快速在項目中完成頁面的佈局效果和簡單的交互功能。java

可是咱們可能會忽略掉在這些優秀的第三方庫中的某些組件可能也依賴於其餘優秀的庫!正如咱們使用頻率很高的 Ant Design 中的 Form 組件(這裏我說的是 React 版本的)。react

其實這些優秀的開源庫內部使用了優秀的第三方庫 rc-form,正如咱們常用的 getFieldDecorator、getFieldsValue、setFieldsValue、validateFields 等這些 Api,其實這些都是 rc-form 暴露出來的方法。ios

爲何要使用 rc-form?

咱們都知道 React 框架設計模式和 Vue 不一樣,Vue 中做者已經幫咱們實現了數據的雙向綁定,數據驅動視圖,視圖驅動數據的改變,可是 React 中須要咱們手動調用 setState 實現數據驅動視圖的改變,請看下面的代碼。git

import React, { Component } from "react";

export default class index extends Component {
  state = {
    value1: "peter",
    value2: "123",
    value3: "23",
  };

  onChange1 = ({ target: { value } }) => {
    this.setState({ value1: value });
  };

  onChange2 = ({ target: { value } }) => {
    this.setState({ value2: value });
  };
  
  onChange3 = ({ target: { value } }) => {
    this.setState({ value3: value });
  };
  
	submit = async () => {
    const { value1, value2, value3 } = this.state;
    const obj = {
      value1,
      value2,
      value3,
    };
    const res = await axios("url", obj)
  };

  render() {
    const { value1, value2, value3 } = this.state;
    return (
      <div> <form action=""> <label for="">用戶名: </label> <input type="text" value={value1} onChange={this.onChange1} /> <br /> <label for="">密碼: </label> <input type="text" value={value2} onChange={this.onChange2} /> <br /> <label for="">年齡: </label> <input type="text" value={value3} onChange={this.onChange3} /> <br /> <button onClick={this.submit}>提交</button> </form> </div>
    );
  }
}
複製代碼
  • 上面是一個表單登陸的簡單功能!要想實現表單數據的實時更新須要在表單 onChange 的時候手動更新 state 狀態;axios

  • 從上面代碼中能夠看出,這樣寫功能也能實現,可是當咱們的表單多的時候,難道頁面要寫十幾個 onChange 事件去實現頁面的數據驅動視圖的更新嗎?這樣考慮一下實際上是不妥的;後端

  • 這個時候 rc-form 就應運而生了,rc-form 建立一個數據集中管理倉庫,這個倉庫負責統一收集表單數據驗證、重置、設置、獲取值等邏輯操做,這樣咱們就把重複無用功交給 rc-form 來處理了,以達到代碼的高度可複用性!設計模式

主要 Api 簡要說明

Api 名稱 說明 類型
getFieldDecorator 用於和表單進行雙向綁定, Function(name)
getFieldsValue 獲取一組字段名對應的值,會按照對應結構返回。默認返回現存字段值,當調用 getFieldsValue(true) 時返回全部值 (nameList?: NamePath[], filterFunc?: (meta: { touched: boolean, validating: boolean }) => boolean) => any
getFieldValue 獲取對應字段名的值 (name: NamePath) => any
setFieldsValue 設置一組表單的值 (values) => void
setFields 設置一組字段狀態 (fields: FieldData[]) => void
validateFields 觸發表單驗證 (nameList?: NamePath[]) => Promise
isFieldValidating 檢查一組字段是否正在校驗 (name: NamePath) => boolean
getFieldProps 獲取對應字段名的屬性 (name: NamePath) => any

使用 rc-form

import { createForm } from "../../rc-form";
// import ReactClass from './ReactClass'

const RcForm = (props) => {
  const {
    form: { getFieldDecorator, validateFields },
  } = props;

  const handleSubmit = (e) => {
    e && e.stopPropagation();
    validateFields((err, value) => {
      if (!err) {
        console.log(value);
      }
    });
  };
  return (
    <div style={{ padding: 20, background: "#fff" }} > <form> <label>姓名:</label> {getFieldDecorator("username", { rules: [{ required: true, message: "請輸入用戶名!" }], initialValue:'initialValue', })(<input type="text" />)} <br /> <label>密碼:</label> {getFieldDecorator("password", { rules: [ { required: true, message: "請輸入密碼!" }, { pattern: /^[a-z0-9_-]{6,18}$/, message:'只容許數字!' } ], })(<input type="password" style={{ marginTop: "15px" }} /> )} <br /> <button onClick={handleSubmit} style={{ marginTop: "15px" }}> 提交 </button> </form> </div>
  );
};
export default createForm()(RcForm);

複製代碼

注意: 通過 createForm 方法處理的組件(就是 Ant Design 中 Form 的 create( ) 方法),會自動向組件沒注入 form 對象,組件自己也就擁有了這些 Api 。數組

  • Demo 只是簡單的基於 rc-form 實現了表單的裝飾、表單驗證、數據收集等功能。那麼如何實現更加具備針對性的,適用多種業務場景的表單組件呢?

  • 繞開優秀的開源的組件庫不說,若是哪一天這些優秀的開源做品再也不開源了,那咱們怎麼辦?

  • 爲了不這種狀況發生,或者若是僅是爲了咱們本身的職業生涯規劃,使本身更上一層樓的話也是有必要的去學習一下優秀的三方庫的設計理念。就算看一下別人的代碼風格也是有必要的。其實仍是須要咱們本身瞭解 rc-form 的設計思路的;只有瞭解了這些優秀開源做品的精髓,咱們即便不用開源庫,也能夠封裝本身的代碼庫以及相似 Ant Design 中 Form 這些優秀的組件的。

從 createForm 開始

都知道咱們平時編寫業務組件通常只要用到表單都會用到 createForm 或者 Form.create( ) 這些方法對本身的組件進行包裝,那麼咱們就從這裏開始咱們的故事。

import createBaseForm from './createBaseForm';

function createForm(options) {
  return createBaseForm(options, [mixin]);
}

export default createForm;
複製代碼

能夠看到其實 createForm 只是作了一層封裝,真正的調用函數是 createBaseForm,那麼着重看一下 createBaseForm 函數內部實現。

createBaseForm

上面的圖片中能夠看出這個函數利用閉包特性返回一個新函數,這個函數的參數其實就是你的業務組件對象,通過 createBaseForm 內部加工以後返回給你的是一個注入了 form 對象的組件。也就是咱們常說的這個 createBaseForm 是一個高階組件。

那麼也就清楚了 Ant Design 的 Form.create() 方法就是 rc-form 中的 createBaseForm 方法的替代!通過 createBaseForm 包裝的組件將會注入 form 對象, 而 form 屬性中提供的 getFieldDecorator 以及 fieldsStore 實例則是實現數據自動收集的關鍵。

淺析內部實現

咱們就先從最初的的渲染表單的邏輯開始,咱們業務場景中用到的表單組件都會使用 getFieldDecorator 包裝一下。固然,我說的是 Ant Design 4.0 之前的版本, 那麼咱們就先從這裏開始看起。

這裏首先說明一下,此篇文章我只是淺析一下整個表單數據雙向綁定的簡單過程,由於這個是 rc-form 的核心,精力有限具體的細節處理留待之後慢慢研究。那麼咱們就來看一下 getFieldDecorator 方法作了些什麼?

getFieldDecorator(name, fieldOption) {
  const props = this.getFieldProps(name, fieldOption);
  return fieldElem => {
    // We should put field in record if it is rendered
    this.renderFields[name] = true;

    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    const originalProps = fieldElem.props;
    fieldMeta.originalProps = originalProps;
    fieldMeta.ref = fieldElem.ref;
    const decoratedFieldElem = React.cloneElement(fieldElem, {
      ...props,
      ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
    });
    return supportRef(fieldElem) ? (
      decoratedFieldElem
    ) : (
      <FieldElemWrapper name={name} form={this}> {decoratedFieldElem} </FieldElemWrapper>
  );
};
},
複製代碼

此處我刪除了一些可有可無的代碼,由於這樣看起來更加清晰明瞭。 首先對傳入的表單組件調用 getFieldProps 方法進行了 props 的構建處理,接着返回一個函數,這個函數參數就是咱們使用 getFieldDecorator 傳入的表單組件,調用 fieldsStore 中的 getFieldMeta 獲取表單組件的配置數據,兼容原有組件的配置屬性以及對不支持 ref 組件的處理,最終返回一個克隆後的掛載處理後的一些配置對象的組件!

fieldsStore

既然用到了 fieldsStore,那麼這裏要說一下 fieldsStore,fieldsStore 中包含了當前 form 的主要信息和一些處理表單數據的方法。

class FieldsStore {
  constructor(fields) {
    this.fields = internalFlattenFields(fields);
    this.fieldsMeta = {};
  }
}
複製代碼

fieldMeta 能夠當作是一個表單項的描述,以傳入的 name 爲索引 key,支持嵌套、存儲表單數據, 即配置信息不涉及值的問題,主要包括:

  • name 字段的名稱
  • originalProps 被 getFieldDecorator( ) 裝飾的組件的原始 props
  • rules 校驗的規則
  • trigger 觸發數據收集的時機 默認 onChange
  • validate 校驗規則和觸發事件
  • valuePropName 子節點的值的屬性,例如 checkbox 應該設爲 checked
  • getValueFromEvent 如何從 event 中獲取組件的值
  • hidden 爲 true 時,校驗或者收集數據時會忽略這個字段

fields 主要用於記錄每一個表單的實時屬性,主要包括:

  • dirty 數據是否已經改變,但未校驗

  • errors 校驗文案

  • name 字段名稱

  • touched 數據是否更新過

  • value 字段的值

  • validating 校驗狀態

那麼接下來仍是要看一下 getFieldProps 方法內部是如何實現 props 構建的?

getFieldProps(name, usersFieldOption = {}) {
  // 從新組裝 props
  const fieldOption = {
    name,
    trigger: DEFAULT_TRIGGER,
    valuePropName: 'value',
    validate: [],
    ...usersFieldOption,
  };
  const {
    rules,
    trigger,
    validateTrigger = trigger,
    validate,
  } = fieldOption;
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  // 初始值處理
  if ('initialValue' in fieldOption) {
    fieldMeta.initialValue = fieldOption.initialValue;
  }
	// 組裝 inputProps
  const inputProps = {
    ...this.fieldsStore.getFieldValuePropValue(fieldOption),
    ref: this.getCacheBind(name, `${name}__ref`, this.saveRef),
  };
  if (fieldNameProp) {
    inputProps[fieldNameProp] = formName ? `${formName}_${name}` : name;
  }
	
  // 收集驗證規則
  const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
  const validateTriggers = getValidateTriggers(validateRules);
  validateTriggers.forEach((action) => {
    if (inputProps[action]) return;
    inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
  });

  // 不走效驗的組件使用 onCollect 收集組件的值
  if (trigger && validateTriggers.indexOf(trigger) === -1) {
    inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
  }

  return inputProps;
},
複製代碼

刪除了一些細節代碼, 先來看看 getFieldProps 首先進行了默認值的處理,若是用戶沒有設置 triggervaluePropName 則使用默認值,隨後調用 fieldsStore 中的getFieldMeta 方法,fieldsStore 實例對象在整個過程當中尤其關鍵,它的做用是做爲一個數據中心,讓咱們免除了手動去維護 form 中綁定的各個值。那麼咱們看一下 fieldsStore.getFieldMeta 作了那些工做?

getFieldMeta(name) {
  this.fieldsMeta[name] = this.fieldsMeta[name] || {};
  return this.fieldsMeta[name];
}
複製代碼

此函數做用在於根據組件傳遞的 name 屬性獲取數據中心的 fieldMeta,若是沒有則默認空對象,也就是首次渲染返回初始值。 重要的是 inputProps 的組裝環節,第一步調用 getFieldValuePropValue 方法獲取當前 props,而後加入 ref 屬性,接下來是效驗規則的收集。

const validateRules = normalizeValidateRules(validate, rules, validateTrigger);
const validateTriggers = getValidateTriggers(validateRules);
validateTriggers.forEach((action) => {
    if (inputProps[action]) return;
    inputProps[action] = this.getCacheBind(name, action, this.onCollectValidate);
});

if (trigger && validateTriggers.indexOf(trigger) === -1) {
    inputProps[trigger] = this.getCacheBind(name, trigger, this.onCollect);
}
複製代碼

validateRules 便是全部的表單組件效驗規則,validateTriggers 即全部效驗規則觸發的事件名, 那麼咱們就看一下 nomalizeValidateRules 以及 getValidateTriggers 方法是如何收集驗證規則的。

function normalizeValidateRules(validate, rules, validateTrigger) {
  const validateRules = validate.map((item) => {
    const newItem = {
      ...item,
      trigger: item.trigger || [],
    };
    if (typeof newItem.trigger === 'string') {
      newItem.trigger = [newItem.trigger];
    }
    return newItem;
  });
  if (rules) {
    validateRules.push({
      trigger: validateTrigger
      ? [].concat(validateTrigger)
      : [],
      rules,
    });
  }
  return validateRules;
}

function getValidateTriggers(validateRules) {
  return validateRules
    .filter(item => !!item.rules && item.rules.length)
    .map(item => item.trigger)
    .reduce((pre, curr) => pre.concat(curr), []);
}

複製代碼

其會將 validaterules 組合,返回一個數組,其內部的元素爲一個個規則對象,而且每一個元素都存在一個能夠爲空的 trigger 數組,而且將 validateTrigger 做爲 ruletriggers 推入 validateRules 中,咱們回回頭看一下 validateTrigger

const fieldOption = {
     name,
     trigger: DEFAULT_TRIGGER,
     valuePropName: 'value',
     validate: [],
     ...usersFieldOption,
 };

const {
    rules,
    trigger,
    validateTrigger = trigger,
    validate,
} = fieldOption;
複製代碼

這裏能夠看出若是用戶配置了觸發驗證方法時默認使用配置的 trigger,若是用戶沒有設置 trigger 則默認使用默認 onChange

getValidateTriggers 則是將全部觸發事件統一收集至一個數組,隨後經過 forEach 循環將全部 validateTriggers 中的事件都綁定上同一個處理函數 getCacheBind 上。

validateTriggers.forEach((action) => {
 	if (inputProps[action]) return;
 	inputProps[action] = this.getCacheBind(
    name, 
    action, 
    this.onCollectValidate
  );
 });
複製代碼

下面再來看一下觸發驗證規則綁定事件 action 的 getCacheBind 函數作了哪些操做?

getCacheBind(name, action, fn) {
  if (!this.cachedBind[name]) {
    this.cachedBind[name] = {};
  }
  const cache = this.cachedBind[name];
  if (
    !cache[action] ||
    cache[action].oriFn !== fn 
  	) {
    cache[action] = {
      fn: fn.bind(this, name, action),
      oriFn: fn,
    };
  }
  return cache[action].fn;
},
複製代碼

暫且忽略 cachedBind 方法,這裏能夠看到 getCacheBind 方法主要對傳入的 fn 作了一個改變 this 指向的邏輯處理,真正的處理函數則是 onCollectValidate,那咱們來看一下 onCollectValidate 作了什麼?

onCollectValidate(name_, action, ...args) {
  const { field, fieldMeta } = this.onCollectCommon(name_, action, args);
  const newField = {
    ...field,
    dirty: true,
  };
  this.fieldsStore.setFieldsAsDirty();
  
  this.validateFieldsInternal([newField], {
    action,
    options: {firstFields: !!fieldMeta.validateFirst,},
  });
},

複製代碼

onCollectValidate 被調用,也就是數據校驗函數被觸發時,首先調用了 onCollectCommon 方法,那麼這個函數是幹什麼的?

onCollectCommon(name, action, args) {
  const fieldMeta = this.fieldsStore.getFieldMeta(name);
  if (fieldMeta[action]) {
    fieldMeta[action](...args);
  } else if (fieldMeta.originalProps && fieldMeta.originalProps[action]) {
    fieldMeta.originalProps[action](...args);
  }
  const value = fieldMeta.getValueFromEvent ?
        fieldMeta.getValueFromEvent(...args) :
  getValueFromEvent(...args);
  if (onValuesChange && value !== this.fieldsStore.getFieldValue(name)) {
    const valuesAll = this.fieldsStore.getAllValues();
    const valuesAllSet = {};
    valuesAll[name] = value;
    Object.keys(valuesAll).forEach(key => set(valuesAllSet, key, valuesAll[key]));
    onValuesChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, set({}, name, value), valuesAllSet);
  }
  const field = this.fieldsStore.getField(name);
  return ({ name, field: { ...field, value, touched: true }, fieldMeta });
},

複製代碼

onCollectCommon 主要是獲取了包裝組件最新的值,隨後將其包裝在對象中返回,返回後將其組裝爲一個新的名爲 newField 的對象。

fieldsStore.setFieldsAsDirty 則是標記包裝組件的校驗狀態,暫且略過,隨後執行 validateFieldsInternal,咱們看一下 validateFieldsInternal 函數。

validateFieldsInternal( fields, { fieldNames, action, options = {} }, callback, ) {
  const allRules = {};
  const allValues = {};
  const allFields = {};
  const alreadyErrors = {};
  fields.forEach(field => {
    const name = field.name;
    if (options.force !== true && field.dirty === false) {
      if (field.errors) {
        set(alreadyErrors, name, { errors: field.errors });
      }
      return;
    }
    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    const newField = {
      ...field,
    };
    newField.errors = undefined;
    newField.validating = true;
    newField.dirty = true;
    allRules[name] = this.getRules(fieldMeta, action);
    allValues[name] = newField.value;
    allFields[name] = newField;
  });
  this.setFields(allFields);
  // in case normalize
  Object.keys(allValues).forEach(f => {
    allValues[f] = this.fieldsStore.getFieldValue(f);
  });
  if (callback && isEmptyObject(allFields)) {
    callback(
      isEmptyObject(alreadyErrors) ? null : alreadyErrors,
      this.fieldsStore.getFieldsValue(fieldNames),
    );
    return;
  }
  // console.log(allRules);
  const validator = new AsyncValidator(allRules);
  if (validateMessages) {
    // console.log(validateMessages);
    validator.messages(validateMessages);
  }
  validator.validate(allValues, options, errors => {
    const errorsGroup = {
      ...alreadyErrors,
    };
    // ...
    const expired = [];
    const nowAllFields = {};
    Object.keys(allRules).forEach(name => {
      const fieldErrors = get(errorsGroup, name);
      const nowField = this.fieldsStore.getField(name);
      // avoid concurrency problems
      if (!eq(nowField.value, allValues[name])) {
        expired.push({
          name,
        });
      } else {
        nowField.errors = fieldErrors && fieldErrors.errors;
        nowField.value = allValues[name];
        nowField.validating = false;
        nowField.dirty = false;
        nowAllFields[name] = nowField;
      }
    });
    this.setFields(nowAllFields);
    // ...
  }

複製代碼

由於 validateFieldsInternal 主要邏輯都是在調用 AsyncValidator 進行異步校驗以及對特殊場景的處理,咱們暫時略過只看數據收集部分,咱們看到在最後調用了 this.setFields(allFields); 並傳入了新的值,接下來就看一下 setFields 方法。

setFields(maybeNestedFields, callback) {
  const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields);
  this.fieldsStore.setFields(fields);
  if (onFieldsChange) {
    const changedFields = Object.keys(fields)
    .reduce((acc, name) => set(acc, name, this.fieldsStore.getField(name)), {});
    onFieldsChange({
      [formPropName]: this.getForm(),
      ...this.props
    }, changedFields, this.fieldsStore.getNestedAllFields());
  }
  this.forceUpdate(callback);
},

複製代碼

咱們能夠看到,setFields 首先對傳入的值進行與初始化類似的驗證,隨後調用 fieldsStore 實例中的 setFields 方法將值存入 fieldsStore, 暫時忽略 onFieldsChange,以後調用 forceUpdate 更新視圖。到此,咱們簡單的描述了整個流程。

表單數據雙向綁定

表單數據更新大體流程以下:

forceUpdate

總結:

  • 用戶輸入或者選擇表單組件的行爲都會觸發 getFieldDecorator(HOC) 高階組件,進而調用 getFieldProps 組裝組件 props,這個方法中若是表單組件中配置了 validateRules 以及 validateTriggers 的話(也就是 rules 對象)就調用 onCollectValidate 方法收集效驗規則。而後就是設置表單組件的最新的值到 fieldsStore 中, 並調用 this.forceUpdate( ) 更新 UI 視圖!

  • 若是咱們沒有配置 validateRules 以及 validateTriggers 等規則,那就使用 onCollect 方法收集最新的數據並更新到 fieldsStore 中。不對錶單進行單獨驗證,,從而在設置最新值 setFields 方法中調用 this.forceUpdate( ) 更新 UI 視圖!

總體設計思路

fremework

總結:

  • 總之 rc-form 內部有本身的狀態管理,fieldsStore 記錄着全部表單項的信息,經過 getFieldDecorator 和表單進行雙向綁定;
  • 真正的區別在於用不用表單規則驗證,不用就 onCollect,不然使用 onCollectValidate,可是必然都會使用 onCollectCommon;
  • onCollectCommon 方法內部展現了 onCollect 取值的細節,forceUpdate 在更新組件後,觸發 render 方法,接着又回到一開始 getFieldDecorator 中獲取 fieldStore 內的值,返回被修改後的組件。

想一下假如當我改變輸入框的值得時候是否是會引發表單的從新渲染的問題。 因此這也就致使了渲染性能的問題! 那麼必然會有優化的方法,有興趣的能夠看看 rc-field-form。

文章只是總體淺析實現思路,若有不一樣意見,歡迎聯繫我交流!

推薦閱讀

Vite 特性和部分源碼解析

我在工做中是如何使用 git 的

Serverless Custom (Container) Runtime

開源做品

  • 政採雲前端小報

開源地址 www.zoo.team/openweekly/ (小報官網首頁有微信交流羣)

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索