Formik setFieldTouched同步校验为何能获取最新state值
查看Formik.tsx文件中的相关提交记录可以发现,校验逻辑最初(以touched状态触发校验的场景为例)是写在useEffect中的:
React.useEffect(() => { if ( prevState.touched !== state.touched && !!validateOnBlur && !state.isSubmitting && isMounted.current != null ) { const [validate, cancel] = makeCancelable(validateForm()); validate.then(x => x).catch(x => x); // 静默捕获Promise拒绝结果 return cancel; } return; }, [ prevState.touched, state.isSubmitting, state.touched, validateForm, validateOnBlur, isMounted, ]);
后续该逻辑被迁移到了setFieldTouched的React.useCallback实现中:
const setFieldTouched = React.useCallback( ( field: string, touched: boolean = true, shouldValidate: boolean = true ) => { dispatch({ type: 'SET_FIELD_TOUCHED', payload: { field, value: touched, }, }); return validateOnBlur && shouldValidate ? validateFormWithLowPriority(state.values) : Promise.resolve(); }, [validateFormWithLowPriority, state.values, validateOnBlur] );
再看当前的最新实现,setFieldTouched是通过useEventCallback包裹的:
const setFieldTouched = useEventCallback( (field: string, touched: boolean = true, shouldValidate?: boolean) => { dispatch({ type: 'SET_FIELD_TOUCHED', payload: { field, value: touched, }, }); const willValidate = shouldValidate === undefined ? validateOnBlur : shouldValidate; return willValidate ? validateFormWithHighPriority(state.values) : Promise.resolve(); } );
但后两种实现为什么、又是如何保证校验运行时能使用「最新的」表单值的呢?
在最初useEffect的实现逻辑中,开发者可能先dispatch值变更动作,再dispatch失焦动作,两个动作都会修改state,等state完成更新后会触发effect执行,此时effect中自然能拿到最新的当前值。
但在后两种实现中,顺着调用链路追踪,校验本身应该是同步运行的(已知结论:Promise的executor函数是同步执行的),只有错误状态的设置会等待Promise完成后才执行(存在显式的then调用)。按这个逻辑推断,当我调用setFieldValue后立刻调用setFieldTouched时,由于React的state更新是异步的,此时拿到的应该还是旧的state值——就像我们dispatch一个state变更动作后,立刻读取同一个state,拿到的仍然是旧值一样(可参考React state异步更新的相关示例)。
但实际测试Formik运行逻辑时会发现,setFieldTouched校验流程中传入validate函数的values确实是最新的实际值。我无法理解出现该现象的原因,可能是我对执行流程的理解存在偏差,因此提出该问题。
内容的提问来源于stack exchange,提问作者Dean

