我们应该如何优雅的处理 React 中受控与非受控
受控 & 非受控
今天來和大家簡單聊聊 React 中的受控和非受控的概念。
提到受控和非受控相信對于使用過 React 的朋友已經(jīng)老生常談了,在開始正題之前慣例先和大家聊一些關(guān)于受控 & 非受控的基礎(chǔ)概念。
當(dāng)然,已經(jīng)有基礎(chǔ)的小伙伴可以略過這個章節(jié)直接往下進(jìn)行。
受控
在 HTML 中,表單元素(如<input>、?<textarea>?和?<select>)通常自己維護(hù) state,并根據(jù)用戶輸入進(jìn)行更新。而在 React 中,可變狀態(tài)(mutable state)通常保存在組件的 state 屬性中,并且只能通過使用?setState()來更新。
我們可以把兩者結(jié)合起來,使 React 的 state 成為“唯一數(shù)據(jù)源”。渲染表單的 React 組件還控制著用戶輸入過程中表單發(fā)生的操作。被 React 以這種方式控制取值的表單輸入元素就叫做“受控組件”。
上述的描述來自 React 官方文檔,其實(shí)受控的概念也非常簡單。通過組件內(nèi)部可控的 state 來控制組件的數(shù)據(jù)改變從而造成視圖渲染。
這種模式更像是 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;復(fù)制代碼上述的代碼非常簡單,我們聲明了一個名為 Input 的自定義輸入框組件,但是 Input 框中的值是由組件中的 controllerState 進(jìn)行控制的。
這也就意味著,如果組件外部的狀態(tài)并不改變(這里指組件的 props 中的 value)時,即使用戶在頁面上展示的 input 如何輸入 input 框中渲染的值也是不會發(fā)生任何改變的。
當(dāng)然,無論是通過 props 還是通過 state 只要保證表單組件的 value 接受的是一個非 undefined 的狀態(tài)值,那么該表單元素就可以被稱為受控(表單中的值是通過組件狀態(tài)控制渲染的)。
非受控
既然存在受控組件,那么一定存在相反非受控的概念。
在大多數(shù)情況下,我們推薦使用?受控組件?來處理表單數(shù)據(jù)。在一個受控組件中,表單數(shù)據(jù)是由 React 組件來管理的。另一種替代方案是使用非受控組件,這時表單數(shù)據(jù)將交由 DOM 節(jié)點(diǎn)來處理。
熟悉 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; 復(fù)制代碼雖然說 React 官方推薦使用受控組件來處理表單數(shù)據(jù),但如果每一個表單元素都需要使用方通過受控的方式來使用的話對于調(diào)用方來說的確是過于繁瑣了。
所以大多數(shù) React Form 表單我們都是通過非受控的方式來處理,那么所謂的非受控究竟是什么意思呢。我們一起來看看。
所謂非受控簡單來說也就指的是表單元素渲染并不通過內(nèi)部狀態(tài)數(shù)據(jù)的改變而渲染,而是交由源生表單內(nèi)部的 State 來進(jìn)行自由渲染。
這其實(shí)是一種和受控組件完全相反的概念,比如:
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; 復(fù)制代碼上述我們重新定義了一個名為 Input 的非受控組件,此時當(dāng)你在使用該 Input 組件時,由于 defaultValue 僅會在 input 元素初始化時進(jìn)行一次數(shù)據(jù)的初始化。
之后當(dāng)用戶在頁面上的 input 元素中輸入任何值表單值都會跟隨用戶輸入而實(shí)時變化而并不受任何組件狀態(tài)的控制,這就被稱為非受控組件。
當(dāng)然相較于受控組件獲取值的方式,非受控組件獲取的方式就會稍微顯得繁瑣一些,非受控組件需要通過組件實(shí)例也就是配合 ref 屬性來獲取對應(yīng)組件/表單中的值,比如:
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; 復(fù)制代碼上邊的代碼中,我們需要獲取 unController input 的值。需要通過 ref 獲得對應(yīng) input 的實(shí)例之后獲得 input 中的值。
重要區(qū)分點(diǎn)
上邊我們聊到了 React 中的受控和非受控的概念,在 React 中區(qū)分受控組件和非受控組件有一個最重要的 point 。
在 React 中當(dāng)一個表單組件,我們顯式的聲明了它的 value (并不為 undefined 或者 null 時)那么該表單組件即為受控組件。
相反,當(dāng)我們?yōu)樗?value 傳遞為 undefined 或者 null 時,那么該組件會變?yōu)榉鞘芸?unController)組件。
相信使用過 React 的小伙伴的同學(xué)或多或少都碰到過相關(guān)的 Warning :
input 組件的 value 從非 undefeind 變?yōu)?undefined (從受控強(qiáng)行改變?yōu)榉鞘芸亟M件),這是不被 React 推薦的做法。
當(dāng)并未受控組件提供 onChange 選項時,此時也就意味著用戶永遠(yuǎn)無法改變該 input 中的值。
當(dāng)然,還有諸如此類非常多的 Warining 警告。相信大家在搞清楚受控 & 非受控的概念后這些對于大家來說都是小菜一碟。
當(dāng)然在絕大多數(shù)社區(qū)組件庫中都是將 undefined 作為了區(qū)分受控和非受控的標(biāo)志。
useMergedState
在我們了解了 React 中的受控 & 非受控的基礎(chǔ)概念后,趁熱打鐵我們再來聊聊 rc-util 中的一個 useMergedState Hook。
這個 Hook 其實(shí)并沒有多少難度,大家完全不用擔(dān)心看不懂它的代碼哈哈。
在開始閱讀它的代碼之前,我會一步一步帶你了解它的運(yùn)作方式。
作用
首先,我們先來看看 useMergedState 這個 Hook 的作用。
通常在我們開發(fā)一些表單組件時,需要基于多層屬性來傳遞 props 給基層的 input 之類的表單控件。
由于是公用的基礎(chǔ)表單控件,所以無疑僅提供受控或者非受控單一的一種方式來說對于調(diào)用者并不是那么優(yōu)雅和便捷。
所以此時,針對于表單控件的開發(fā)我們需要提供給開發(fā)者受控和非受控兩種方式的支持。
類似 Ant-Design 中的 Input 組件。它既接收顯示傳入 value 和 onChange 的組合方式,同時也支持傳入 defaultValue 的非受控方式實(shí)現(xiàn)。
所謂的 useMergedState 即是這樣的一個作用:通過該 Hook 你可以自由定義表單控件的受控和非受控狀態(tài)。
這么說其實(shí)稍微有點(diǎn)含糊,我們先來看看它的類型定義吧:
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>]; 復(fù)制代碼這個 hook 接收兩個形參,分別為 defaultStateValue 和 option:
-
defaultStateValue 這個參數(shù)表示傳入的默認(rèn) value 值,當(dāng)傳入?yún)?shù)不存在 option 中的 value 或者 defaultValue 時就會 defaultStateValue 來作為初始值。
-
option
- defaultValue 可選,表示接收非受控的初始化默認(rèn)值,它的優(yōu)先級高于 defaultStateValue 。
- value 可選,表示作為受控時的 value props,它的優(yōu)先級高于 defaultValue 和 defaultStateValue。
- onChange 可選,當(dāng)內(nèi)部值改變后會觸發(fā)該函數(shù)。
- postState 可選,表示對于傳入值的 format 函數(shù)。
乍一看其實(shí)挺多的參數(shù),相信沒有了解過該函數(shù)的同學(xué)多多少少都會有些懵。
沒關(guān)系,接下來我們會先拋開這個 Hook ,先自己來一步一步嘗試如何來實(shí)現(xiàn)這樣的組合受控 & 非受控的業(yè)務(wù) Hook。
實(shí)現(xiàn)
接下來我們就先按照自己的思路來實(shí)現(xiàn)這個 Hook 。
首先,我們以一個 Input 組件為為例,假使我們需要編寫一個 Input 輸入框組件。
interface TextFieldextends Omit<InputHTMLAttributes<HTMLInputElement>, 'onchange'> {/*** onChange 函數(shù)*/onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; }const TextField: React.FC<TextField> = (props) => {const { value, defaultValue, onChange, ...rest } = props;return <input />; }; 復(fù)制代碼非受控處理
上述,我們編寫了一個基礎(chǔ)的 Input 組件的模版。
此時,讓我們先來考慮傳入該組件的非受控處理,也就是所謂的接受 defaultValue 作為非受控的 props 傳入。
我們利用 defaultValue 作為 input 框非受控的值傳遞,以及配合 onChange 僅做事件的傳遞。
interface TextFieldextends Omit<InputHTMLAttributes<HTMLInputElement>, 'onchange'> {/*** onChange 函數(shù)*/onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; }const TextField: React.FC<TextField> = (props) => {const { defaultValue, onChange, ...rest } = props;return <input defaultValue={defaultValue} onChange={onChange} {...rest} />; }; 復(fù)制代碼看起來非常簡單對吧,此時當(dāng)調(diào)用者使用我們的組件時。只需要傳入 defaultValue 的值就可以使用非受控狀態(tài)的 input 。
受控處理
上述我們用非常簡單的代碼實(shí)現(xiàn)了非受控的 Input 輸入框,此時我們再來看看如何兼顧受控狀態(tài)的值。
我們提到過,在 React 中如果需要受控狀態(tài)的表單控件是需要顯式傳入 value 和對應(yīng)的 onChange 作為配合的,此時很容易我們想到這樣改造我們的組件:
interface TextFieldextends Omit<InputHTMLAttributes<HTMLInputElement>, 'onchange'> {/*** onChange 函數(shù)*/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; 復(fù)制代碼有些同學(xué)會很容易想到我們將 defaultValue 和 value 同時進(jìn)行透傳進(jìn)來不就完成了嗎。
沒錯,這樣的確可以完成基礎(chǔ)的需求。可是這對于一個組件來說并不是一種良好的做法,假如調(diào)用方這樣使用我們的組件:
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} />); } 復(fù)制代碼上述我們在 App 頁面中同時傳入了 value 和 defaultValue 的值,雖然在使用上并沒有任何問題。但是在開發(fā)模式下 React 會給予我們這樣的警告:
它的大概意思是在說 React 無法解析出當(dāng)前 TextField 中的 input 表單控件為受控還是非受控,因為我們同時傳入了 value 和 defaultValue 的值。(但是它最終仍會將該 input 當(dāng)做受控處理,因為 value 的優(yōu)先級高于 defaultValue)
兼容兩種模式
接下來就讓我們來處理上述的 Warning 警告。
目前 TextField 內(nèi)部 input 控件可以分別接受 value 和 defaultValue 兩個值,這兩個值完全由用戶傳入,顯然是不太合理的。
我們先來思考下,我們需要解決這個警告的途徑的思路:我們將 TextField 處理為無論外部傳入的是 value 還是 defaultValue 都在 TextField 內(nèi)部通過受控處理。
換句話說,無論調(diào)用者傳入 defaultValue 還是 value ,對于調(diào)用方來說該表單控件是存在對應(yīng)非受控和受控兩種狀態(tài)的。
但是對于 TextField 內(nèi)部來說,我們會將外部傳入的值全部當(dāng)作受控來處理。
此時,我們來稍微改造改造我們的 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;// 內(nèi)部為受控狀態(tài)控制 input 控件const [_value, setValue] = useState(() => {if (typeof value !== 'undefined') {return value;} else {return defaultValue;}});/*** onChange 函數(shù)* @param e*/const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {const inputValue = e.target.value;// 當(dāng) onChange 觸發(fā)時,需要判斷// 1. 如果當(dāng)前外部傳入 value === undefined ,此時表示為非受控模式。那么組件內(nèi)部應(yīng)該直接進(jìn)行控件 value 值的切換// 2. 相反,如果組件外部傳入 value !== undefined,此時表示為受控模式。那么組件內(nèi)部的值應(yīng)該由外部的 props 中的 value 決定而不應(yīng)該自主切換。if (typeof value === 'undefined') {setValue(inputValue);}onChange && onChange(e);};return <input value={(fixControlledValue(_value))} onChange={_onChange} {...rest} />; };export default TextField; 復(fù)制代碼基于上述的思路,我們做了以下幾點(diǎn)的小改造:
完成了上述功能點(diǎn)后,此時當(dāng)我們傳入 defaultValue 調(diào)用非受控的 TextField 時已經(jīng)可以滿足基礎(chǔ)的功能點(diǎn)了:
// ... <TextField defaultValue={'hello world'} onChange={onChange} /> 復(fù)制代碼當(dāng)外部傳入 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} />); } 復(fù)制代碼即使我們?nèi)绾卧陧撁娴?input 中進(jìn)行輸入,此時傳入的 onChange 的確會被觸發(fā)同時通知 state 的值改變。
但是由于組件內(nèi)部 useState 的值已經(jīng)進(jìn)行過初始化了,并不會由于組件的 props 改變而重新初始化組件內(nèi)部的 state 狀態(tài)。
// ...const [_value, setValue] = useState(() => {if (typeof value !== 'undefined') {return value;} else {return defaultValue;}}); 復(fù)制代碼此時就會造成,無論我們?nèi)绾卧陧撁嫔陷斎?onChange 會觸發(fā),外部 State 的值也會變化。
但是由于 TextField 中的 input 表單控件 value 是永遠(yuǎn)不會被改變,所以,頁面不會發(fā)生任何變化。
那么,解決這個問題其實(shí)也非常簡單。當(dāng) TextField 組件為受控狀態(tài)時,內(nèi)部表單的 value 值并不會跟隨組件內(nèi)部的 onChange 而改變表單的值。
而是,每當(dāng) props 中的 value 改變時,我們就需要及時改變對應(yīng)表單的內(nèi)部狀態(tài)。
在 React 中我們不難想到這種場景應(yīng)該利用的副作用函數(shù),接下來我們再來為之前的 TextField 內(nèi)部添加一個副作用 Hook :
const TextField: React.FC<TextField> = (props) => {const { defaultValue, value, onChange, ...rest } = props;// .../** 當(dāng)外部 props.value 改變時,修改對應(yīng)內(nèi)部的 State */useLayoutEffect(() => {setValue(value);}, [value]);return (<input value={fixControlledValue(_value)} onChange={_onChange} {...rest} />); }; 復(fù)制代碼此時,上述 TextField 的受控狀態(tài)我們也完成了。
當(dāng)我們再次傳入 defaultValue 和 value 時,由于內(nèi)部統(tǒng)一作為了組件內(nèi)部 state 來處理所以自然也不會出現(xiàn)對應(yīng)的 Warning 警告了。
其實(shí),這也就是所謂 useMergedState 的源碼核心思路。
它無非是基于上述的思路多做了一些邊界狀態(tài)的處理以及一些額外輔助參數(shù)的支持。接下來,我們來一起看看看這個 Hook 的源碼。
源碼
相信在經(jīng)過上述的章節(jié)后,對于 React 中的受控和非受控 Hook 大家已經(jīng)可以了解到其中的核心思路。
現(xiàn)在,讓我們來一起進(jìn)入 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;// ... } 復(fù)制代碼上述的這段初始化邏輯其實(shí)和我們剛才差不多,對于傳入的參數(shù)在內(nèi)部使用 useState 進(jìn)行初始化。
其次:
相信上面的初始化邏輯對于大家來講都是輕松拿捏,我們繼續(xù)往下看。
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);});// ... } 復(fù)制代碼接下來我們在看看所謂的同步和更新階段。
同步 Sync
在同步階段做的事情非常簡單,它和我們上述自己寫的 Demo 是一模一樣的,是受控模式的特殊處理。
每當(dāng)外部傳入的 props.value 變化時,會調(diào)用 setMergedValue 同步更新 Hook 內(nèi)部的 state 。
關(guān)于 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;};}, []); }; 復(fù)制代碼這個 Hook 的作為也非常簡單,內(nèi)部利用 ref 結(jié)合 useLayoutEffect 做到了僅在依賴值更新時調(diào)用 callback 首次渲染并不執(zhí)行。
更新 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);}); 復(fù)制代碼首先,Update 的開頭利用 changeEventPrevRef 這個 ref 值來確保每次更新時,獲取到正確的 React 批處理的 prevValue。
這個值也許有些同學(xué)目前不太理解,沒關(guān)系。我們會在稍后的 Tips 中結(jié)合實(shí)例來講解它,目前如果你通過代碼仍然不太理解它的話可以暫時不用過于在意。
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; } 復(fù)制代碼這個 useEvent 其實(shí)非常簡單,它的作用仍然是使用 ref 和 useCallback 進(jìn)行配合從而保證傳入的 onChange 函數(shù)放在 fnRef 中。
從而確保每次 ReRender 時直接調(diào)用 fnRef.current 而無需在 Hook 重新生成一份傳入的 onChange 定義。
同時這樣的好處是,雖然 useCallback 依賴的是一個 [] 但是由于 ref 的引用類型關(guān)系,即是外部 props.onChang 重新定義,內(nèi)部 useEvent 包裹的 onChange 也會跟隨生效。
它算作是一個小的優(yōu)化點(diǎn)而已。
setState 中的 ignoreDestroy
其次,我們再來看看函數(shù)內(nèi)部的操作。可以看到定義的 triggerChange 函數(shù)接受兩個參數(shù),分別為 updater 和 ignoreDestroy 。
這里我們先忽略 ignoreDestroy 以免造成干擾。
我們先來看看函數(shù)內(nèi)部的邏輯:
const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {setMergedValue(prev => {// 結(jié)構(gòu)出 state 中的值,分別為 // prevValue 上一次的 value // prevSource 上一次的更新類型// 以及 prevPrevValue 上上一次的 valueconst [prevValue, prevSource, prevPrevValue] = prev;// 判斷傳入的是否為函數(shù),如果是的話傳入 prevValue 調(diào)用得到 nextValueconst nextValue: T =typeof updater === 'function' ? (updater as any)(prevValue) : updater;// Do nothing if value not changeif (nextValue === prevValue) {return prev;}// 確保 Patch 處理獲得正確上一次的值 稍后結(jié)合實(shí)例來看// ...}, ignoreDestroy);}); 復(fù)制代碼相信上述的代碼對于大家來說都是非常簡單的,無非就是針對于每次調(diào)用 triggerChange 時進(jìn)行參數(shù)的沖載。
如果是函數(shù)那么傳入 prevValue ,非函數(shù)就獲得對應(yīng)的 nextValue 以及進(jìn)行值相同不更新的操作。
不過,細(xì)心的小伙伴可能發(fā)現(xiàn)了,當(dāng)我們調(diào)用 setMergedValue 時還接受了第二個參數(shù) ignoreDestroy 。
我們再來回憶下 Init 階段所謂的 setMergedValue 是從哪里來的:
import useState from './useState'; 復(fù)制代碼注意,Hook 中的 useState 并非來自 React 的 useState 而是 Rc-util 中自定義的 useState。
之所以 useState 接受第二個參數(shù) ignoreDestroy 也正是 rc-util 自定義的 hook 支持第二個參數(shù)。
// ... // 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 變?yōu)?falseReact.useEffect(() => {destroyRef.current = false;// 同時卸載后會將 destroyRef.current 變?yōu)?truereturn () => {destroyRef.current = true;};}, []);// 安全更新函數(shù)function safeSetState(updater: Updater<T>, ignoreDestroy?: boolean) {// 如果不為強(qiáng)制要求 ignoreDestroy 顯示指定為 true// 同時組件已經(jīng)卸載 destroyRef.current 為 trueif (ignoreDestroy && destroyRef.current) {// 那么調(diào)用更新 state 的函數(shù)沒有任何作用return;}setValue(updater);}return [value, safeSetState]; } 復(fù)制代碼上述為 rc-util useState.ts 文件,它的用法和 React 中的 useState 類型。
不過是 setState 額外接收一個 ignoreDestroy 參數(shù)確保銷毀后不會在被調(diào)用 setState 設(shè)置已銷毀的狀態(tài)。
這樣做的好處其實(shí)也是一個針對于 React 中內(nèi)存泄漏的優(yōu)化點(diǎn)而已。
批處理更新處理
搞清楚了上述的小 Tips 后,我們繼續(xù)來看看所謂的針對于批處理更新的 changeEventPrevRef 作用。
首先,在 Init 階段我們針對于每一種傳入的方式,比如 value、defaultValue 以及 defaultValueState 都定義了不同的類型。
定義了他們究竟是來自于 INNER 還是 PROP,忘記了的同學(xué)可以翻閱 Init 階段在稍稍回憶下。
之后我們提到過在 Sync 同步階段,每次 value 變化時,都會執(zhí)行這個 Effect:
useLayoutUpdateEffect(() => {setMergedValue(([prevValue]) => [value, Source.PROP, prevValue]);}, [value]); 復(fù)制代碼當(dāng)我們?yōu)樵?Hook 傳入 value 表示為受控時,此時每次 value 變化都會直接調(diào)用 setMergedValue 方法并且保證 value 的類型為 Source.PROP 。
自然,changeEventPrevRef 和受控模式也沒有任何關(guān)系。
那么當(dāng)傳入 defaultValueState 和 defaultValue 時,Hook 中表示為非受控處理時。
每次內(nèi)部 mergeValue 改變就會觸發(fā)對應(yīng)的 triggerChange 從而觸發(fā)對應(yīng)的 setMergedValue 。
這里我們首先明確 changeEventPrevRef 是和非受控狀態(tài)相關(guān)的一個 ref 變量。
其次,在 React 中存在一個批處理更新(Batch Updating)的概念。
同時,不要忘記在 useMergeState 第二個 option 參數(shù)中接收一個名為 onChange 的函數(shù)。
我們來結(jié)合 useMergeState 中 update 更新的代碼來看看:
// ...const changeEventPrevRef = React.useRef<T>();const triggerChange: Updater<T> = useEvent((updater, ignoreDestroy) => {setMergedValue(prev => {// 結(jié)構(gòu)出 state 中的值,分別為 // prevValue 上一次的 value // prevSource 上一次的更新類型// 以及 prevPrevValue 上上一次的 valueconst [prevValue, prevSource, prevPrevValue] = prev;// 判斷傳入的是否為函數(shù),如果是的話傳入 prevValue 調(diào)用得到 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// 確保非受控狀態(tài)下的 onChange 函數(shù)多次同一隊列中獲得正確的 preValue 值const overridePrevValue =prevSource === Source.INNER &&changeEventPrevRef.current !== prevPrevValue? prevPrevValue: prevValue;return [nextValue, Source.INNER, overridePrevValue];}, ignoreDestroy);}); 復(fù)制代碼比如這樣的使用場景:
const InputComponent: React.FC = (props) => {const [mergeState, setMergeState] = useMergedState('default value', {onChange: (currentValue, preValue) => {// log "[inputValue] 2"console.log(currentValue, '當(dāng)前value');// 這里的preValue仍然為上一次的 inputValue 而非 inputValue + '1'console.log(preValue, '上一次value'); },});const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {const inputValue = e.target.value;// 調(diào)用三次 setMergeStatesetMergeState(inputValue);setMergeState(inputValue + '1');setMergeState(inputValue + '2');};return <input value={mergeState} onChange={_onChange} />; };export default InputComponent; 復(fù)制代碼上述的 overridePrevValue 正是保證傳入的 onChange 函數(shù)在內(nèi)部多次 patch Updaing 后仍然可以通過 changeEventPrevRef 拿到正確的 prevPrevValue 值。
Change
最后,我們再來看看 Hook 最后的 Change 階段:
// ...// ====================== Change ======================useLayoutEffect(() => {// 每次 render mergedValue 改變時const [current, source, prev] = mergedValue;// 當(dāng)前 current !== prev 同時 source === Source.INNER (非受控狀態(tài)下)時才會觸發(fā) onChangeFnif (current !== prev && source === Source.INNER) {onChangeFn(current, prev);// 同時再次更新 changeEventPrevRef.current 為 prev(overridePrevValue)changeEventPrevRef.current = prev;}}, [mergedValue]);// ... 復(fù)制代碼上述的代碼其實(shí)看上去就非常簡單了。
當(dāng)每次 mergedValue 的值更新時,會觸發(fā)對應(yīng)的 useLayoutEffect 。
同時判斷如果 source === Source.INNER 表示非受控狀態(tài)下內(nèi)部值改變同時 current !== prev 為一次有效的變化時。
會觸發(fā)對應(yīng)外部傳入的 onChangeFn(current,prev),同時更新內(nèi)部 ref changeEventPrevRef.current prev。
至此,整個 useMergedState 的源碼我們就已經(jīng)逐行解讀完畢了。
如果仍有哪些地方你仍不是特別理解,那么你可以翻閱回去再次看看或者直接查閱它的代碼。
結(jié)尾
這次的分享稍微顯得有一些基礎(chǔ),不過我們可以發(fā)現(xiàn)一個看起非常簡單的受控和非受控的概念在 useMergedState 中也的確藏著不少的知識點(diǎn)。
總結(jié)
以上是生活随笔為你收集整理的我们应该如何优雅的处理 React 中受控与非受控的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 几种特殊焊盘总结
- 下一篇: OpenJDK源码赏析之二:java虚拟