react中實現(xiàn)修改input的defaultValue
react中修改input的defaultValue
在使用 react 進(jìn)行開發(fā)時,我們一般使用類組件的 setState 或者 hooks 實現(xiàn)頁面數(shù)據(jù)的實時更新,但在某些表單組件中,這一操作會失效,元素的數(shù)據(jù)卻無法更新,令人苦惱
比如下面這個例子
import React, { useState } from "react"; function Demo() { const [num, setNum] = useState(0); return ( <> <input defaultValue={num} /> <button onClick={() => setNum(666)}>button</button> </> ); } export default Demo;
理論上按鈕點擊后會執(zhí)行 setNum 函數(shù),并觸發(fā) Demo 組件重新渲染,input 展示最新值,但實際上 Input 值并沒有更新到最新
如下截圖:
從截圖可以看出,num 值確實已經(jīng)更新到了最新,但是 Input 中的值卻始終沒有同步更新,如何解決這個問題呢,很簡單,在 input 上添加一個 key 即可。
但是僅僅知道解決方案還不夠,奔著打破砂鍋問到底的態(tài)度,我們今天就來探究下為啥通過修改 key 可以強制更新?
在開始之前,首先要明確一點: input 元素本身是沒有 defaultValue 這個屬性,如下圖(點我查看),這個屬性是 react 框架自己添加,一直以為是原生屬性的我留下了沒有技術(shù)的眼淚。
換句話說,如果不使用 react 框架,在 input 中是無法使用 defaultValue 屬性的。
下面是一個使用 defaultValue 的簡單例子
<head> <script type="text/javascript"> function GetDefValue() { var elem = document.getElementById("myInput"); var defValue = elem.defaultValue; var currvalue = elem.value; if (defValue == currvalue) { alert("The contents of the input field have not changed!"); } else { alert("The default contents were " + defValue + "\n and the new contents are " + currvalue); } } </script> </head> <body> <button onclick="GetDefValue ();">Get defaultValue!</button> <input type="text" id="myInput" value="Initial value"> The initial value will not be affected if you change the text in the input field. </body>
雖然 input 標(biāo)簽上不能直接設(shè)置 defaultValue,但是卻可以通過操作 HTMLInputElement 對象設(shè)置和獲取 defaultValue,需要注意的是,這里通過設(shè)置 defaultValue 也會同步修改 value 的值,但是因為 react 內(nèi)部自定實現(xiàn)了 input 組件,所以在 react 中通過修改 defaultValue 并不會影響到 value 值,具體參看 ReactDOMInput.js。
以上是一些前置知識,接下來是具體的分析。
通過上面的介紹,我們首先要看下 react 是如何處理 defaultValue 這個屬性的,這個屬性是在 postMountWrapper 中設(shè)置的,源碼如下:
export function postMountWrapper( element: Element, props: Object, isHydrating: boolean, ) { const node = ((element: any): InputWithWrapperState); if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) { const type = props.type; const isButton = type === 'submit' || type === 'reset'; if (isButton && (props.value === undefined || props.value === null)) { return; } const initialValue = toString(node._wrapperState.initialValue); if (!isHydrating) { if (initialValue !== node.value) { node.value = initialValue; } } node.defaultValue = initialValue; } }
通過源碼可以看出,react 內(nèi)部會獲取傳入的 defaultValue,然后同時掛載到 node 的 value 和 defaultValue上,這樣初次渲染的時候頁面就會展示傳入的默認(rèn)屬性,注意這個函數(shù)只會在初始化的時候執(zhí)行。
接下來我們看下點擊按鈕后的邏輯,重點關(guān)注 mapRemainingChildren 函數(shù):
function mapRemainingChildren( returnFiber: Fiber, currentFirstChild: Fiber, ): Map<string | number, Fiber> { // Add the remaining children to a temporary map so that we can find them by // keys quickly. Implicit (null) keys get added to this set with their index // instead. const existingChildren: Map<string | number, Fiber> = new Map(); let existingChild = currentFirstChild; while (existingChild !== null) { if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; }
這個函數(shù)會給每一個子元素添加一個 key 值,并添加到一個 set 中,之后會執(zhí)行 updateFromMap 方法
function updateFromMap( existingChildren: Map<string | number, Fiber>, returnFiber: Fiber, newIdx: number, newChild: any, lanes: Lanes, ): Fiber | null { // ... if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; return updateElement(returnFiber, matchedFiber, newChild, lanes); } } } // ... return null; }
在這個方法會通過最新傳入的 key 獲取 上面 set 中的值,然后將值傳入到 updateElement 中
function updateElement( returnFiber: Fiber, current: Fiber | null, element: ReactElement, lanes: Lanes, ): Fiber { const elementType = element.type; if (current !== null) { if ( current.elementType === elementType || (enableLazyElements && typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === current.type) ) { // Move based on index const existing = useFiber(current, element.props); existing.ref = coerceRef(returnFiber, current, element); existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; existing._debugOwner = element._owner; } return existing; } } // Insert const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, current, element); created.return = returnFiber; return created; }
因為我們在更新的時候修改了 key 值,所以這里的 current 是不存在的,走的是重新創(chuàng)建的代碼,如果我們沒有傳入 key 或者 key 沒有改變,那么走的的就是復(fù)用的代碼,所以,如果使用 map 循環(huán)了多個 input 然后使用下標(biāo)作為 key,就會出現(xiàn)修改后多個 input 狀態(tài)不一致的詳情,因此,表單組件不推薦使用下標(biāo)作為 key,容易出 bug。
之后是更新代碼的邏輯,input 屬性的更新操作是在 updateWrapper 中進(jìn)行的,我們看下這個函數(shù)的源碼:
export function updateWrapper(element: Element, props: Object) { const node = ((element: any): InputWithWrapperState); updateChecked(element, props); // 重點,這里只會獲取 value 的值,不會再獲取 defaultValue 的值 const value = getToStringValue(props.value); const type = props.type; if (value != null) { if (type === 'number') { if ( (value === 0 && node.value === '') || // We explicitly want to coerce to number here if possible. // eslint-disable-next-line node.value != (value: any) ) { node.value = toString((value: any)); } } else if (node.value !== toString((value: any))) { node.value = toString((value: any)); } } else if (type === 'submit' || type === 'reset') { // Submit/reset inputs need the attribute removed completely to avoid // blank-text buttons. node.removeAttribute('value'); return; } // 根據(jù)設(shè)置的 value 或者 defaultValue 來 input 元素的屬性 if (props.hasOwnProperty('value')) { setDefaultValue(node, props.type, value); } else if (props.hasOwnProperty('defaultValue')) { setDefaultValue(node, props.type, getToStringValue(props.defaultValue)); } }
這里的 element 其實就是 input 對象,但是由于在設(shè)置時僅獲取 props 中的 value,而沒有獲取 defaultValue,第 21 行不會執(zhí)行,所以頁面中的值也不會更新,但是第34行依然還是會執(zhí)行,而且頁面還出現(xiàn)了十分詭異的現(xiàn)象
如下圖:
頁面展示狀態(tài)和源碼狀態(tài)不一致,HTML中的屬性已經(jīng)修改為了 666,但是頁面依然展示的 0,估計是 react 在實現(xiàn) input 時留下的一個隱藏 bug。
總結(jié)一下
react 內(nèi)部會給 Demo 組件中的每一個子元素添加一個 key(傳入或下標(biāo)),然后將 key 作為 set 的鍵,之后通過最新的 key 去獲取 set 中儲存的值,如果存在復(fù)用原來元素,更新屬性,如果不存在,重新創(chuàng)建,修改 key 可以達(dá)到每次都重新創(chuàng)建元素,而不是復(fù)用原來的元素,這就是修改 key 進(jìn)而達(dá)到修改 defaultValue 的原因。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
ReactJS?應(yīng)用兼容ios9對標(biāo)ie11解決方案
這篇文章主要為大家介紹了ReactJS?應(yīng)用兼容ios9對標(biāo)ie11解決方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01React?中使用?Redux?的?4?種寫法小結(jié)
這篇文章主要介紹了在?React?中使用?Redux?的?4?種寫法,Redux 一般來說并不是必須的,只有在項目比較復(fù)雜的時候,比如多個分散在不同地方的組件使用同一個狀態(tài),本文就React使用?Redux的相關(guān)知識給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-06-06React特征學(xué)習(xí)Form數(shù)據(jù)管理示例詳解
這篇文章主要為大家介紹了React特征學(xué)習(xí)Form數(shù)據(jù)管理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09