我们应该如何优雅的处理 React 中受控与非受控
引言
大家好,我是19组清风。有段时间没有和大家见面了,最近因为有一些比较重要的事情(陪女朋友和换了新公司)在忙碌所以销声匿迹了一小段时间,
后续会陆陆续续补充之前构建 & 编译系列中缺失的部分,提前预祝大伙儿圣诞节快乐!
受控 & 非受控
今天来和大家简单聊聊 React 中的受控和非受控的概念。
提到受控和非受控相信对于使用过 React 的朋友已经老生常谈了,在开始正题之前惯例先和大家聊一些关于受控 & 非受控的基础概念。
当然,已经有基础的小伙伴可以略过这个章节直接往下进行。
受控
在 HTML 中,表单元素(如
<input>
、<textarea>
和<select>
)通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用setState()
来更新。我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
上述的描述来自 React 官方文档,其实受控的概念也非常简单。通过组件内部可控的 state 来控制组件的数据改变从而造成视图渲染。
这种模式更像是 Vue 中在表单元素中的常用处理模式,举一个简单的例子,比如:
import { FC } from 'react';interface InputProps<T = string> {value: T;onChange: (value?: T) => void;
}const Input: FC<InputProps> = (props) => {const { onChange, value = '', ...rest } = props;const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {const value = e.target.value;onChange && onChange(value);};return <input value={value} onChange={_onChange} {...rest} />;
};export default Input;
上述的代码非常简单,我们声明了一个名为 Input 的自定义输入框组件,但是 Input 框中的值是由组件中的 controllerState
进行控制的。
这也就意味着,如果组件外部的状态并不改变(这里指组件的 props 中的 value)时,即使用户在页面上展示的 input 如何输入 input 框中渲染的值也是不会发生任何改变的。
当然,无论是通过 props 还是通过 state 只要保证表单组件的 value 接受的是一个非 undefined 的状态值,那么该表单元素就可以被称为受控(表单中的值是通过组件状态控制渲染的)。
非受控
既然存在受控组件,那么一定存在相反非受控的概念。
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。
熟悉 Ant-Design 等存在表单校验的 React 组件库的朋友,可以稍微回忆下它们的表单使用。
// ant-design 官方表单使用示例
import React from 'react';
import { Button, Checkbox, Form, Input } from 'antd';const App: React.FC = () => {const onFinish = (values: any) => {console.log('Success:', values);};const onFinishFailed = (errorInfo: any) => {console.log('Failed:', errorInfo);};return (<Formname="basic"labelCol={{ span: 8 }}wrapperCol={{ span: 16 }}initialValues={{ remember: true }}onFinish={onFinish}onFinishFailed={onFinishFailed}autoComplete="off"><Form.Itemlabel="Username"name="username"rules={[{ required: true, message: 'Please input your username!' }]}><Input /></Form.Item><Form.Itemlabel="Password"name="password"rules={[{ required: true, message: 'Please input your password!' }]}><Input.Password /></Form.Item><Form.Item name="remember" valuePropName="checked" wrapperCol={{ offset: 8, span: 16 }}><Checkbox>Remember me</Checkbox></Form.Item><Form.Item wrapperCol={{ offset: 8, span: 16 }}><Button type="primary" htmlType="submit">Submit</Button></Form.Item></Form>);
};export default App;
虽然说 React 官方推荐使用受控组件来处理表单数据,但如果每一个表单元素都需要使用方通过受控的方式来使用的话对于调用方来说的确是过于繁琐了。
所以大多数 React Form 表单我们都是通过非受控的方式来处理,那么所谓的非受控究竟是什么意思呢。我们一起来看看。
所谓非受控简单来说也就指的是表单元素渲染并不通过内部状态数据的改变而渲染,而是交由源生表单内部的 State 来进行自由渲染。
这其实是一种和受控组件完全相反的概念,比如:
import { FC } from 'react';interface InputProps<T = string> {defaultValue?: T;
}const Input: FC<InputProps> = (props) => {const { defaultValue } = props;return <input defaultValue={defaultValue} />;
};export default Input;
上述我们重新定义了一个名为 Input 的非受控组件,此时当你在使用该 Input 组件时,由于 defaultValue 仅会在 input 元素初始化时进行一次数据的初始化。
之后当用户在页面上的 input 元素中输入任何值表单值都会跟随用户输入而实时变化而并不受任何组件状态的控制,这就被称为非受控组件。
当然相较于受控组件获取值的方式,非受控组件获取的方式就会稍微显得繁琐一些,非受控组件需要通过组件实例也就是配合 ref 属性来获取对应组件/表单中的值,比如:
import { FC, useRef } from 'react';interface InputProps<T = string> {defaultValue?: T;
}const Input: FC<InputProps> = (props) => {const { defaultValue } = props;const instance = useRef<HTMLInputElement>(null);const getInstanceValue = () => {if (instance.current) {alert(instance.current.value);}};return (<div><input ref={instance} defaultValue={defaultValue} /><button onClick={() => getInstanceValue()}>获取input中的值</button></div>);
};export default Input;
上边的代码中,我们需要获取 unController input 的值。需要通过 ref 获得对应 input 的实例之后获得 input 中的值。
重要区分点
上边我们聊到了 React 中的受控和非受控的概念,在 React 中区分受控组件和非受控组件有一个最重要的 point 。
在 React 中当一个表单组件,我们显式的声明了它的 value (并不为 undefined 或者 null 时)那么该表单组件即为受控组件。
相反,当我们为它的 value 传递为 undefined 或者 null 时,那么该组件会变为非受控(unController)组件。
相信使用过 React 的小伙伴的同学或多或少都碰到过相关的 Warning :
input 组件的 value 从非 undefeind 变为 undefined (从受控强行改变为非受控组件),这是不被 React 推荐的做法。
当并未受控组件提供 onChange 选项时,此时也就意味着用户永远无法改变该 input 中的值。
当然,还有诸如此类非常多的 Warining 警告。相信大家在搞清楚受控 & 非受控的概念后这些对于大家来说都是小菜一碟。
当然在绝大多数社区组件库中都是将 undefined 作为了区分受控和非受控的标志。
useMergedState
在我们了解了 React 中的受控 & 非受控的基础概念后,趁热打铁我们再来聊聊 rc-util 中的一个 useMergedState Hook。
这个 Hook 其实并没有多少难度,大家完全不用担心看不懂它的代码哈哈。
在开始阅读它的代码之前,我会一步一步带你了解它的运作方式。
作用
首先,我们先来看看 useMergedState 这个 Hook 的作用。
通常在我们开发一些表单组件时,需要基于多层属性来传递 props 给基层的 input 之类的表单控件。
由于是公用的基础表单控件,所以无疑仅提供受控或者非受控单一的一种方式来说对于调用者并不是那么优雅和便捷。
所以此时,针对于表单控件的开发我们需要提供给开发者受控和非受控两种方式的支持。
类似 Ant-Design 中的 Input 组件。它既接收显示传入 value 和 onChange 的组合方式,同时也支持传入 defaultValue 的非受控方式实现。
所谓的 useMergedState 即是这样的一个作用:通过该 Hook 你可以自由定义表单控件的受控和非受控状态。
这么说其实稍微有点含糊,我们先来看看它的类型定义吧:
export default function useMergedState<T, R = T>(defaultStateValue: T | (() => T), option?: {defaultValue?: T | (() => T);value?: T;onChange?: (value: T, prevValue: T) => void;postState?: (value: T) => T;
}): [R, Updater<T>];
这个 hook 接收两个形参,分别为 defaultStateValue
和 option
:
- defaultStateValue 这个参数表示传入的默认 value 值,当传入参数不存在 option 中的 value 或者 defaultValue 时就会 defaultStateValue 来作为初始值。* option* defaultValue 可选,表示接收非受控的初始化默认值,它的优先级高于 defaultStateValue 。* value 可选,表示作为受控时的 value props,它的优先级高于 defaultValue 和 defaultStateValue。* onChange 可选,当内部值改变后会触发该函数。* postState 可选,表示对于传入值的 format 函数。
乍一看其实挺多的参数,相信没有了解过该函数的同学多多少少都会有些懵。
没关系,接下来我们会先抛开这个 Hook ,先自己来一步一步尝试如何来实现这样的组合受控 & 非受控的业务 Hook。
实现
接下来我们就先按照自己的思路来实现这个 Hook 。
首先,我们以一个 Input 组件为为例,假使我们需要编写一个 Input 输入框组件。
interface TextFieldextends Omit<InputHTMLAttributes<HTMLInputElement>, 'onchange'> {/** * onChange 函数 */onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}const TextField: React.FC<TextField> = (props) => {const { value, defaultValue, onChange, ...rest } = props;return <input />;
};
非受控处理
上述,我们编写了一个基础的 Input 组件的模版。
此时,让我们先来考虑传入该组件的非受控处理,也就是所谓的接受 defaultValue 作为非受控的 props 传入。
我们利用 defaultValue 作为 input 框非受控的值传递,以及配合 onChange 仅做事件的传递。
interface TextFieldextends Omit<InputHTMLAttributes<HTMLInputElement>, 'onchange'> {/** * onChange 函数 */onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}const TextField: React.FC<TextField> = (props) => {const { defaultValue, onChange, ...rest } = props;return <input defaultValue={defaultValue} onChange={onChange} {...rest} />;
};
看起来非常简单对吧,此时当调用者使用我们的组件时。只需要传入 defaultValue 的值就可以使用非受控状态的 input 。
受控处理
上述我们用非常简单的代码实现了非受控的 Input 输入框,此时我们再来看看如何兼顾受控状态的值。
我们提到过,在 React 中如果需要受控状态的表单控件是需要显式传入 value 和对应的 onChange 作为配合的,此时很容易我们想到这样改造我们的组件:
interface TextFieldextends Omit<InputHTMLAttributes<HTMLInputElement>, 'onchange'> {/** * onChange 函数 */onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}const TextField: React.FC<TextField> = (props) => {const { defaultValue, value, onChange, ...rest } = props;return (<inputvalue={value}defaultValue={defaultValue}onChange={onChange}{...rest}/>);
};export default TextField;
有些同学会很容易想到我们将 defaultValue 和 value 同时进行透传进来不就完成了吗。
没错,这样的确可以完成基础的需求。可是这对于一个组件来说并不是一种良好的做法,假如调用方这样使用我们的组件:
export default function App({ Component, pageProps }: AppProps) {const [state, setState] = useState('');const onChange = (e: ChangeEvent<HTMLInputElement>) => {const value = e.target.value;setState(value);};return (<TextField value={state} defaultValue={'hello world'} onChange={onChange} />);
}
上述我们在 App 页面中同时传入了 value 和 defaultValue 的值,虽然在使用上并没有任何问题。但是在开发模式下 React 会给予我们这样的警告:
它的大概意思是在说 React 无法解析出当前 TextField 中的 input 表单控件为受控还是非受控,因为我们同时传入了 value 和 defaultValue 的值。(但是它最终仍会将该 input 当做受控处理,因为 value 的优先级高于 defaultValue)
兼容两种模式
接下来就让我们来处理上述的 Warning 警告。
目前 TextField 内部 input 控件可以分别接受 value 和 defaultValue 两个值,这两个值完全由用户传入,显然是不太合理的。
我们先来思考下,我们需要解决这个警告的途径的思路:我们将 TextField 处理为无论外部传入的是 value 还是 defaultValue 都在 TextField 内部通过受控处理。
换句话说,无论调用者传入 defaultValue 还是 value ,对于调用方来说该表单控件是存在对应非受控和受控两种状态的。
但是对于 TextField 内部来说,我们会将外部传入的值全部当作受控来处理。
此时,我们来稍微改造改造我们的 TextField:
// ...
function fixControlledValue<T>(value: T) {if (typeof value === 'undefined' || value === null) {return ''}return String(value)
}const TextField: React.FC<TextField> = (props) => {const { defaultValue, value, onChange, ...rest } = props;// 内部为受控状态控制 input 控件const [_value, setValue] = useState(() => {if (typeof value !== 'undefined') {return value;} else {return defaultValue;}});/** * onChange 函数 * @param e */const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {const inputValue = e.target.value;// 当 onChange 触发时,需要判断// 1. 如果当前外部传入 value === undefined ,此时表示为非受控模式。那么组件内部应该直接进行控件 value 值的切换// 2. 相反,如果组件外部传入 value !== undefined,此时表示为受控模式。那么组件内部的值应该由外部的 props 中的 value 决定而不应该自主切换。if (typeof value === 'undefined') {setValue(inputValue);}onChange && onChange(e);};return <input value={(fixControlledValue(_value))} onChange={_onChange} {...rest} />;
};export default TextField;
基于上述的思路,我们做了以下几点的小改造:
1.将 TextField 内部之前基于外部传入的 value 和 defaultValue 全部通过内部 State 来进行初始化,在 TextField 内部进行受控处理。
2.在 onChange 时,如果传入的 value 如果为非 undefined 那么表示外部希望该组件模式为受控模式,此时我们并不会改变内部的 state 。
3.同时,我们定义了一个 fixedController 函数,保证内部 input 传入的 value 不为 undefined 或者 null,保证内部 input 是决定由受控状态改变的。
完成了上述功能点后,此时当我们传入 defaultValue 调用非受控的 TextField 时已经可以满足基础的功能点了:
// ...
<TextFielddefaultValue={'hello world'} onChange={onChange} />
当外部传入 value 使用受控的情况时:
export default function App({ Component, pageProps }: AppProps) {const [state, setState] = useState('');const onChange = (e: ChangeEvent<HTMLInputElement>) => {const value = e.target.value;setState(value);};return (<TextField value={state}onChange={onChange} />);
}
即使我们如何在页面的 input 中进行输入,此时传入的 onChange 的确会被触发同时通知 state 的值改变。
但是由于组件内部 useState 的值已经进行过初始化了,并不会由于组件的 props 改变而重新初始化组件内部的 state 状态。
// ...const [_value, setValue] = useState(() => {if (typeof value !== 'undefined') {return value;} else {return defaultValue;}});
此时就会造成,无论我们如何在页面上输入 onChange 会触发,外部 State 的值也会变化。
但是由于 TextField 中的 input 表单控件 value 是永远不会被改变,所以,页面不会发生任何变化。
那么,解决这个问题其实也非常简单。当 TextField 组件为受控状态时,内部表单的 value 值并不会跟随组件内部的 onChange 而改变表单的值。
而是,每当 props 中的 value 改变时,我们就需要及时改变对应表单的内部状态。
在 React 中我们不难想到这种场景应该利用的副作用函数,接下来我们再来为之前的 TextField 内部添加一个副作用 Hook :
const TextField: React.FC<TextField> = (props) => {const { defaultValue, value, onChange, ...rest } = props;// .../** 当外部 props.value 改变时,修改对应内部的 State*/useLayoutEffect(() => {setValue(value);}, [value]);return (<input value={fixControlledValue(_value)} onChange={_onChange} {...rest} />);
};
此时,上述 TextField 的受控状态我们也完成了。
当我们再次传入 defaultValue 和 value 时,由于内部统一作为了组件内部 state 来处理所以自然也不会出现对应的 Warning 警告了。
其实,这也就是所谓 useMergedState 的源码核心思路。
它无非是基于上述的思路多做了一些边界状态的处理以及一些额外辅助参数的支持。接下来,我们来一起看看看这个 Hook 的源码。
源码
相信在经过上述的章节后,对于 React 中的受控和非受控 Hook 大家已经可以了解到其中的核心思路。
现在,让我们来一起进入 react-component 中 useMergedState 的源码来一探究竟吧。
初始化
首先,我们来看看顶部的这段逻辑:
import * as React from 'react';
import useEvent from './useEvent';
import useLayoutEffect, { useLayoutUpdateEffect } from './useLayoutEffect';
import useState from './useState';enum Source {INNER,PROP,
}type ValueRecord<T> = [T, Source, T];/** We only think `undefined` is empty */
function hasValue(value: any) {return value !== undefined;
}/*** Similar to `useState` but will use props value if provided.* Note that internal use rc-util `useState` hook.*/
export default function useMergedState<T, R = T>(defaultStateValue: T | (() => T),option?: {defaultValue?: T | (() => T);value?: T;onChange?: (value: T, prevValue: T) => void;postState?: (value: T) => T;},
): [R, Updater<T>] {const { defaultValue, value, onChange, postState } = option || {};// ======================= Init =======================const [mergedValue, setMergedValue] = useState<ValueRecord<T>>(() => {let finalValue: T = undefined;let source: Source;// 存在 value 受控if (hasValue(value)) {finalValue = value;source = Source.PROP;} else if (hasValue(defaultValue)) {// 存在 defaultValuefinalValue =typeof defaultValue === 'function'? (defaultValue as any)(): defaultValue;source = Source.PROP;} else {// 两个都不存在finalValue =typeof defaultStateValue === 'function'? (defaultStateValue as any)(): defaultStateValue;source = Source.INNER;}return [finalValue, source, finalValue];});const chosenValue = hasValue(value) ? value : mergedValue[0];const postMergedValue = postState ? postState(chosenValue) : chosenValue;// ...
}
上述的这段初始化逻辑其实和我们刚才差不多,对于传入的参数在内部使用 useState 进行初始化。
1.首先判断是否存在 value ,存在 value 则作为受控处理同时将 source 置为 prop 处理。
2.其次,如果不存在有效 value ,则判断是否存在 defaultValue ,同时将 source 置为 prop 处理。
3.最后,如果 value 和 defaultValue 都不存在有效参数那么将会使用第一个参数 defaultStateValue 初始化内部 state 同时将 source 作为 inner 处理。
其次:
1.chosenValue 表示使用的 value ,props 中如果存在传入 value 的话,表示受控模式直接取 props.value
。否则取内部的 mergedValue[0]
。
2.postMergedValue 表示,如果传入了 postState 方法,会在每次执行前格式化 chosenValue。
相信上面的初始化逻辑对于大家来讲都是轻松拿捏,我们继续往下看。
Sync & Update
export default function useMergedState<T, R = T>(defaultStateValue: T | (() => T),option?: {defaultValue?: T | (() => T);value?: T;onChange?: (value: T, prevValue: T) => void;postState?: (value: T) => T;},
): [R, Updater<T>] {// ...// ======================= Sync =======================useLayoutUpdateEffect(() => {setMergedValue(([prevValue]) => [value, Source.PROP, prevValue]);}, [value]);// ====================== Update ======================const changeEventPrevRef = React.useRef<T>();const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {setMergedValue(prev => {const [prevValue, prevSource, prevPrevValue] = prev;const nextValue: T =typeof updater === 'function' ? (updater as any)(prevValue) : updater;// Do nothing if value not changeif (nextValue === prevValue) {return prev;}// Use prev prev value if is in a batch update to avoid missing data 解决批处理丢失上一次value问题const overridePrevValue =prevSource === Source.INNER &&changeEventPrevRef.current !== prevPrevValue? prevPrevValue: prevValue;return [nextValue, Source.INNER, overridePrevValue];}, ignoreDestroy);});// ...
}
接下来我们在看看所谓的同步和更新阶段。
同步 Sync
在同步阶段做的事情非常简单,它和我们上述自己写的 Demo 是一模一样的,是受控模式的特殊处理。
每当外部传入的 props.value
变化时,会调用 setMergedValue
同步更新 Hook 内部的 state 。
关于 useLayoutUpdateEffect
这个 Hook 也是 rc-util 中的一个辅助 hook:
export const useLayoutUpdateEffect: typeof React.useEffect = ( callback,deps, ) => {const firstMountRef = React.useRef(true);useLayoutEffect(() => {if (!firstMountRef.current) {return callback();}}, deps);// We tell react that first mount has passeduseLayoutEffect(() => {firstMountRef.current = false;return () => {firstMountRef.current = true;};}, []);
};
这个 Hook 的作为也非常简单,内部利用 ref 结合 useLayoutEffect 做到了仅在依赖值更新时调用 callback 首次渲染并不执行。
更新 Update
之后我们再来看看 Update 的逻辑。
const changeEventPrevRef = React.useRef<T>();const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {setMergedValue(prev => {const [prevValue, prevSource, prevPrevValue] = prev;const nextValue: T =typeof updater === 'function' ? (updater as any)(prevValue) : updater;// Do nothing if value not changeif (nextValue === prevValue) {return prev;}// Use prev prev value if is in a batch update to avoid missing dataconst overridePrevValue =prevSource === Source.INNER &&changeEventPrevRef.current !== prevPrevValue? prevPrevValue: prevValue;return [nextValue, Source.INNER, overridePrevValue];}, ignoreDestroy);});
首先,Update 的开头利用 changeEventPrevRef 这个 ref 值来确保每次更新时,获取到正确的 React 批处理的 prevValue。
这个值也许有些同学目前不太理解,没关系。我们会在稍后的 Tips 中结合实例来讲解它,目前如果你通过代码仍然不太理解它的话可以暂时不用过于在意。
useEvent
之后我们定义了一个 triggerChange 的方法,这个方法是利用 useEvent 来包裹的,首先我们先来 useEvent 是个什么东西:
import * as React from 'react';export default function useEvent<T extends Function>(callback: T): T {const fnRef = React.useRef<any>();fnRef.current = callback;const memoFn = React.useCallback<T>(((...args: any) => fnRef.current?.(...args)) as any,[],);return memoFn;
}
这个 useEvent 其实非常简单,它的作用仍然是使用 ref 和 useCallback 进行配合从而保证传入的 onChange 函数放在 fnRef 中。
从而确保每次 ReRender 时直接调用 fnRef.current 而无需在 Hook 重新生成一份传入的 onChange 定义。
同时这样的好处是,虽然 useCallback 依赖的是一个 []
但是由于 ref 的引用类型关系,即是外部 props.onChang 重新定义,内部 useEvent 包裹的 onChange 也会跟随生效。
它算作是一个小的优化点而已。
setState 中的 ignoreDestroy
其次,我们再来看看函数内部的操作。可以看到定义的 triggerChange 函数接受两个参数,分别为 updater 和 ignoreDestroy 。
这里我们先忽略 ignoreDestroy 以免造成干扰。
我们先来看看函数内部的逻辑:
const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {setMergedValue(prev => {// 结构出 state 中的值,分别为 // prevValue 上一次的 value // prevSource 上一次的更新类型// 以及 prevPrevValue 上上一次的 valueconst [prevValue, prevSource, prevPrevValue] = prev;// 判断传入的是否为函数,如果是的话传入 prevValue 调用得到 nextValueconst nextValue: T =typeof updater === 'function' ? (updater as any)(prevValue) : updater;// Do nothing if value not changeif (nextValue === prevValue) {return prev;}// 确保 Patch 处理获得正确上一次的值 稍后结合实例来看// ...}, ignoreDestroy);});
相信上述的代码对于大家来说都是非常简单的,无非就是针对于每次调用 triggerChange 时进行参数的冲载。
如果是函数那么传入 prevValue ,非函数就获得对应的 nextValue 以及进行值相同不更新的操作。
不过,细心的小伙伴可能发现了,当我们调用 setMergedValue 时还接受了第二个参数 ignoreDestroy 。
我们再来回忆下 Init 阶段所谓的 setMergedValue 是从哪里来的:
import useState from './useState';
注意,Hook 中的 useState 并非来自 React 的 useState 而是 Rc-util 中自定义的 useState。
之所以 useState 接受第二个参数 ignoreDestroy 也正是 rc-util 自定义的 hook 支持第二个参数。
// ...
// rc-util useState.ts 文件
export default function useSafeState<T>(defaultValue?: T | (() => T),
): [T, SetState<T>] {const destroyRef = React.useRef(false);const [value, setValue] = React.useState(defaultValue);// 每次 Render 后将 destroyRef.current 变为 falseReact.useEffect(() => {destroyRef.current = false;// 同时卸载后会将 destroyRef.current 变为 truereturn () => {destroyRef.current = true;};}, []);// 安全更新函数function safeSetState(updater: Updater<T>, ignoreDestroy?: boolean) {// 如果不为强制要求 ignoreDestroy 显示指定为 true// 同时组件已经卸载 destroyRef.current 为 trueif (ignoreDestroy && destroyRef.current) {// 那么调用更新 state 的函数没有任何作用return;}setValue(updater);}return [value, safeSetState];
}
上述为 rc-util useState.ts 文件,它的用法和 React 中的 useState 类型。
不过是 setState
额外接收一个 ignoreDestroy
参数确保销毁后不会在被调用 setState 设置已销毁的状态。
这样做的好处其实也是一个针对于 React 中内存泄漏的优化点而已。
批处理更新处理
搞清楚了上述的小 Tips 后,我们继续来看看所谓的针对于批处理更新的 changeEventPrevRef
作用。
首先,在 Init 阶段我们针对于每一种传入的方式,比如 value、defaultValue 以及 defaultValueState 都定义了不同的类型。
定义了他们究竟是来自于 INNER 还是 PROP,忘记了的同学可以翻阅 Init 阶段在稍稍回忆下。
之后我们提到过在 Sync
同步阶段,每次 value 变化时,都会执行这个 Effect:
useLayoutUpdateEffect(() => {setMergedValue(([prevValue]) => [value, Source.PROP, prevValue]);}, [value]);
当我们为该 Hook 传入 value 表示为受控时,此时每次 value 变化都会直接调用 setMergedValue 方法并且保证 value 的类型为 Source.PROP
。
自然,changeEventPrevRef 和受控模式也没有任何关系。
那么当传入 defaultValueState 和 defaultValue 时,Hook 中表示为非受控处理时。
每次内部 mergeValue 改变就会触发对应的 triggerChange 从而触发对应的 setMergedValue 。
这里我们首先明确 changeEventPrevRef 是和非受控状态相关的一个 ref 变量。
其次,在 React 中存在一个批处理更新(Batch Updating)的概念。
同时,不要忘记在 useMergeState 第二个 option 参数中接收一个名为 onChange 的函数。
我们来结合 useMergeState 中 update 更新的代码来看看:
// ... const changeEventPrevRef = React.useRef<T>(); const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {setMergedValue(prev => {// 结构出 state 中的值,分别为 // prevValue 上一次的 value // prevSource 上一次的更新类型// 以及 prevPrevValue 上上一次的 valueconst [prevValue, prevSource, prevPrevValue] = prev;// 判断传入的是否为函数,如果是的话传入 prevValue 调用得到 nextValueconst nextValue: T =typeof updater === 'function' ? (updater as any)(prevValue) : updater;// Do nothing if value not changeif (nextValue === prevValue) {return prev;}// Use prev prev value if is in a batch update to avoid missing data// 确保非受控状态下的 onChange 函数多次同一队列中获得正确的 preValue 值const overridePrevValue =prevSource === Source.INNER &&changeEventPrevRef.current !== prevPrevValue? prevPrevValue: prevValue;return [nextValue, Source.INNER, overridePrevValue];}, ignoreDestroy);});
比如这样的使用场景:
const InputComponent: React.FC = (props) => {const [mergeState, setMergeState] = useMergedState('default value', {onChange: (currentValue, preValue) => {// log "[inputValue] 2"console.log(currentValue, '当前value');// 这里的preValue仍然为上一次的 inputValue 而非 inputValue + '1'console.log(preValue, '上一次value'); },});const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {const inputValue = e.target.value;// 调用三次 setMergeStatesetMergeState(inputValue);setMergeState(inputValue + '1');setMergeState(inputValue + '2');};return <input value={mergeState} onChange={_onChange} />;
};export default InputComponent;
上述的 overridePrevValue 正是保证传入的 onChange 函数在内部多次 patch Updaing 后仍然可以通过 changeEventPrevRef
拿到正确的 prevPrevValue 值。
Change
最后,我们再来看看 Hook 最后的 Change 阶段:
// ...// ====================== Change ======================useLayoutEffect(() => {// 每次 render mergedValue 改变时const [current, source, prev] = mergedValue;// 当前 current !== prev 同时 source === Source.INNER (非受控状态下)时才会触发 onChangeFnif (current !== prev && source === Source.INNER) {onChangeFn(current, prev);// 同时再次更新 changeEventPrevRef.current 为 prev(overridePrevValue)changeEventPrevRef.current = prev;}}, [mergedValue]);// ...
上述的代码其实看上去就非常简单了。
当每次 mergedValue 的值更新时,会触发对应的 useLayoutEffect 。
同时判断如果 source === Source.INNER
表示非受控状态下内部值改变同时 current !== prev
为一次有效的变化时。
会触发对应外部传入的 onChangeFn(current,prev)
,同时更新内部 ref changeEventPrevRef.current
prev。
至此,整个 useMergedState 的源码我们就已经逐行解读完毕了。
如果仍有哪些地方你仍不是特别理解,那么你可以翻阅回去再次看看或者直接查阅它的代码。
结尾
这次的分享稍微显得有一些基础,不过我们可以发现一个看起非常简单的受控和非受控的概念在 useMergedState 中也的确藏着不少的知识点。
希望这篇文章可以在日常工作中对大家有所帮助。大家,加油!
最后
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享
相关文章:

我们应该如何优雅的处理 React 中受控与非受控
引言 大家好,我是19组清风。有段时间没有和大家见面了,最近因为有一些比较重要的事情(陪女朋友和换了新公司)在忙碌所以销声匿迹了一小段时间, 后续会陆陆续续补充之前构建 & 编译系列中缺失的部分,提…...

力扣热题100Day06:20. 有效的括号,21. 合并两个有序链表,22. 括号生成
20. 有效的括号 题目链接:20. 有效的括号 - 力扣(Leetcode) 思路:使用栈 (1)遇到左括号就将其对应的右括号压入到栈中 (2)如果遇到右括号 a. 如果弹出的元素与当前不等ÿ…...

【Yolov5】保姆级别源码讲解之-推理部分detect.py文件
推理部分之detect.py文件讲解1.下载Yolov5的源码2. 主函数讲解3.文件标头的注释4. main函数的5. run函数5.1 第一块参数部分5.2第二块,传入数据预处理5.3 第三块创建文件夹5.4 第四块 加载模型的权重5.5 第五块 Dataloader 加载模块5.6 第六块 推理部分 Run inferen…...

无重叠区间-力扣435-java贪心策略
一、题目描述给定一个区间的集合 intervals ,其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。示例 1:输入: intervals [[1,2],[2,3],[3,4],[1,3]]输出: 1解释: 移除 [1,3] 后,剩下的区间没有重叠。…...

Python使用VTK对容积超声图像进行体绘制(三维重建)
目录VTK简介什么是体绘制?体绘制效果图流程CodeQ&AReferenceVTK简介 VTK(Visualization Toolkit)是一个用于3D计算机图形学、图像处理和可视化的开源软件包。它包括一组C类和工具,可以让用户创建和处理复杂的3D图形和数据可视…...

JAVA设计模式之工厂模式讲解
目录 前言 开始表演 前言 Java中使用工厂模式的主要原因是为了实现代码的灵活性和可维护性。工厂模式是一种创建型设计模式,它提供了一种将对象的创建和使用进行分离的方式。具体来说,工厂模式可以将对象的创建过程封装在一个独立的工厂类中ÿ…...

近万字概述L3及以上自动驾驶故障运行和故障安全机制
本文描述了对ADS的FO和FS机制的评估方法。当系统不能按预期运行时,ADS将使用FO和FS机制。这些机制使ADS能够在最大程度上达到使车辆及其乘员脱离危险的MRC。定义、测试和验证实现MRC的FO和FS策略是确保ADS安全运行和部署的重要步骤。 MRC在SAE J3016中被定义为: 用户或ADS在…...

kafka入门到精通
文章目录一、kafka概述?1.定义1.2消息队列1.2.1 传统消息队列的使用场景1.2.2 消息队列好处1.2.3 消息队列两种模式1.3 kafka基础架构二、kafka快速入门1.1使用docker-compose安装kafka1.2测试访问kafka-manager1.3 查看kafka版本号1.4 查看zookeeper版本号1.5 扩展…...

es-09模糊查询
模糊查询 前缀搜索:prefix 概念:以xx开头的搜索,不计算相关度评分。 注意: 前缀搜索匹配的是term,而不是field。前缀搜索的性能很差前缀搜索没有缓存前缀搜索尽可能把前缀长度设置的更长 语法: GET <ind…...

57 - 深入解析任务调度
---- 整理自狄泰软件唐佐林老师课程 文章目录1. 问题1.1 思考1.2 实例分析:问题分析及解决2. 深入讨论2.1 任务调度的定义2.2 关于调度算法的分类2.3 什么时候进行任务调度2.4 任务的分类2.5 关于优先级调度2.6 问题2.7 调度算法的终极目标2.8 课后扩展1. 问题 系统…...

CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设
CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设 苏勇,2023年2月 文章目录CAN总线开发一本全(3) - 微控制器集成的FlexCAN外设引言硬件外设模块系统概要总线接口单元 - 寄存器清单数据结构 - 消息缓冲区MB初始化过…...

Elasticsearch7.8.0版本进阶——段合并
目录一、段的概述1.1、段的概念1.2、段的缺点1.3、如何解决段数量暴增问题二、段合并的流程三、段合并的注意事项一、段的概述 1.1、段的概念 每一 段 本身都是一个倒排索引。 1.2、段的缺点 由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量…...

Java版贪食蛇游戏
技术:Java等摘要:近年来Java作为一种新的编程语言,以其简单性、可移植性和平台无关性等优点,得到了广泛地应用,特别是Java与万维网的完美结合,使其成为网络编程和嵌入式编程领域的首选编程语言。MyEclipse是…...

2023年度数学建模竞赛汇总
本人7年数学建模竞赛经验,历史获奖率百分之百。团队成员都是拿过全国一等奖的硕博,有需要数模竞赛帮助的可以私信我。 下面主要列几年一些比较有含金量的数学建模竞赛(按比赛时间顺序) 1. 美国大学生数学建模竞赛 报名时间&…...

了解Python语言和版本
1.1 任务1了解Python语言和版本 Python 语言的名字来自于一个著名的电视剧"Monty Pythons Flying Cireus",Python之父 Guido van Rossum是这部电视剧的狂热爱好者,所以把他设计的语言命名为Python。 Python 是一门跨平台、开源、免费的解释型高级动态编…...

nvm (node版本管理工具)安装的详细步骤,并解决安装过程中遇到的问题
1、下载NVM,跳转下载链接后,如下图,下载红框后解压文件 2、安装 注意:双击安装之后,会有两个地址选择, 1、地址中不能存在空格 2、不要放在C盘中,后面需要改个设置文件,安装到C盘的…...

朴素贝叶斯笔记
贝叶斯公式在A 条件成立下,B的概率等于B的概率*在B条件成立下,A的概率/A的概率,推导假设一个学校中男生占总数的60%,女生占总数的40%。并且男生总是穿长裤,女生则一半穿长裤、一半穿裙子。1.正向概率。随机选取一个学生…...

【GUI】用于电动助力车性能分析的GUI(Matlab代码实现)
👨🎓个人主页:研学社的博客💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密…...

Android:反编译apk踩坑/apktool/dex2jar/JDGUI
需求描述 想要反编译apk文件,搜到了这篇博客:Android APK反编译就这么简单 详解(附图),非常有参考价值~但其中的工具下载链接都已404,而本杂鱼实际操作的过程中也出现了亿点点点点点点的问题,于…...

React 跨域的配置
1、为什么会出现跨域? 浏览器遵循同源政策(同源策略三要素:协议相同、域名相同、端口相同) 2、配置跨域代理 使用中间件 http-proxy-middleware(安装依赖) npm install http-proxy-middleware 创建setupP…...

Elasticsearch7.8.0版本进阶——持久化变更
目录一、持久化变更的概述二、事务日志(translog)三、持久化变更完整流程四、事务日志(translog)的作用五、事务日志(translog)的目的一、持久化变更的概述 没有用 fsync 把数据从文件系统缓存刷ÿ…...

CF Edu 127 A-E vp补题
CF Edu 127 A-D vp补题 继续每日一vp,今天晚上有课,时间不太多,回去就直接vp。前三题比较简单,过了之后排名rk2000,然后就去洗澡了。d题没怎么认真思考,其实也可做。最后rk4000。发挥还行,b题罚…...

剑指 Offer 05. 替换空格
摘要 剑指 Offer 05. 替换空格 一、字符替换 由于每次替换从1个字符变成3个字符,使用字符数组可方便地进行替换。建立字符数组地长度为 s 的长度的3倍,这样可保证字符数组可以容纳所有替换后的字符。 获得 s 的长度 length创建字符数组 array&#x…...

通过操作Cortex-A7核,串口输入相应的命令,控制LED灯进行工作
1.通过操作Cortex-A7核,串口输入相应的命令,控制LED灯进行工作 例如在串口输入led1on,开饭led1灯点亮 2.例如在串口输入led1off,开饭led1灯熄灭 3.例如在串口输入led2on,开饭led2灯点亮 4.例如在串口输入led2off,开饭led2灯熄灭 5.例如在串口输入led…...

Python实现某du文库vip内容下载,保存成PDF
前言 是谁,是谁在网页上搜索往年考试卷题答案的时候只能阅读前两页的选择题,是谁在搜几千字的文档资料只能看25%,是谁在百度文库找七找八的时候所有的东西都要付费才能继续看… 我先说 是我自己 我又不经常用,只有偶尔需要看看…...

vue3.0 模板语法
文章目录前言:1. 内容渲染指令1.1 v-text1.2 {{ }}插值表达式1.3 v-html2. 双向绑定指令2.1 v-model2.2 v-model的修饰符3. 属性绑定指令3.1 动态绑定多个属性值3.2 绑定class和style属性4.条件渲染指令4.1 v-if、v-else-if、v-else4.2 v-show4.3 v-if与v-show的区别…...

【GlobalMapper精品教程】054:标签(标注)功能案例详解
同ArcGIS标注一样,globalmapper提供了动态标注的功能,称为标签,本文详解标签的使用方法。 文章目录 一、标签配置二、创建标签图层三、标签图层选项1. 标签字段2. 标签样式3. 标签格式4. 标签语言5. 标签优先级一、标签配置 在配置页面的【矢量显示】→标签选项卡下,有标签…...

超详细树状数组讲解(+例题:动态求连续区间和)
树状数组的作用:快速的对数列的一段范围求和快速的修改数列的某一个数为什么要使用树状数组:大家从作用中看到快速求和的时候可能会想到为什么不使用前缀和只需要预处理一下就可以在O(1)的时间复杂度下实行对于数列的一段范围的和但是我们可以得到当我们…...

【学习笔记】AGC055
A - ABC Identity 如果只有AAA,BBB两种字符的话,我们发现要寻找p∈[1,n]p\in [1,n]p∈[1,n],使得[1:p][1:p][1:p]中AAA的数目与[p1:n][p1:n][p1:n]中BBB的数目相同。 如果有A,B,CA,B,CA,B,C三种字符,我们可以先将A,BA,BA,B分离出来…...

墨者——内部文件上传系统漏洞分析溯源 内部文件上传系统漏洞分析溯源
墨者——内部文件上传系统漏洞分析溯源 内部文件上传系统漏洞分析溯源 1.选择合适的文件上传 2.可以看到为*.asp文件 3.可以推测出此站点为IIS 4.上传shell.asp试试 5.上传报错,将其改名为shell.asp.txt上传,发现上传成功 6.有个问题就是服务器将我们所…...