react中實現(xiàn)修改input的defaultValue
react中修改input的defaultValue
在使用 react 進行開發(fā)時,我們一般使用類組件的 setState 或者 hooks 實現(xiàn)頁面數據的實時更新,但在某些表單組件中,這一操作會失效,元素的數據卻無法更新,令人苦惱
比如下面這個例子
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 函數,并觸發(fā) Demo 組件重新渲染,input 展示最新值,但實際上 Input 值并沒有更新到最新
如下截圖:
從截圖可以看出,num 值確實已經更新到了最新,但是 Input 中的值卻始終沒有同步更新,如何解決這個問題呢,很簡單,在 input 上添加一個 key 即可。
但是僅僅知道解決方案還不夠,奔著打破砂鍋問到底的態(tài)度,我們今天就來探究下為啥通過修改 key 可以強制更新?
在開始之前,首先要明確一點: input 元素本身是沒有 defaultValue 這個屬性,如下圖(點我查看),這個屬性是 react 框架自己添加,一直以為是原生屬性的我留下了沒有技術的眼淚。
換句話說,如果不使用 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 標簽上不能直接設置 defaultValue,但是卻可以通過操作 HTMLInputElement 對象設置和獲取 defaultValue,需要注意的是,這里通過設置 defaultValue 也會同步修改 value 的值,但是因為 react 內部自定實現(xiàn)了 input 組件,所以在 react 中通過修改 defaultValue 并不會影響到 value 值,具體參看 ReactDOMInput.js。
以上是一些前置知識,接下來是具體的分析。
通過上面的介紹,我們首先要看下 react 是如何處理 defaultValue 這個屬性的,這個屬性是在 postMountWrapper 中設置的,源碼如下:
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 內部會獲取傳入的 defaultValue,然后同時掛載到 node 的 value 和 defaultValue上,這樣初次渲染的時候頁面就會展示傳入的默認屬性,注意這個函數只會在初始化的時候執(zhí)行。
接下來我們看下點擊按鈕后的邏輯,重點關注 mapRemainingChildren 函數:
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; }
這個函數會給每一個子元素添加一個 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 沒有改變,那么走的的就是復用的代碼,所以,如果使用 map 循環(huán)了多個 input 然后使用下標作為 key,就會出現(xiàn)修改后多個 input 狀態(tài)不一致的詳情,因此,表單組件不推薦使用下標作為 key,容易出 bug。
之后是更新代碼的邏輯,input 屬性的更新操作是在 updateWrapper 中進行的,我們看下這個函數的源碼:
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; } // 根據設置的 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 對象,但是由于在設置時僅獲取 props 中的 value,而沒有獲取 defaultValue,第 21 行不會執(zhí)行,所以頁面中的值也不會更新,但是第34行依然還是會執(zhí)行,而且頁面還出現(xiàn)了十分詭異的現(xiàn)象
如下圖:
頁面展示狀態(tài)和源碼狀態(tài)不一致,HTML中的屬性已經修改為了 666,但是頁面依然展示的 0,估計是 react 在實現(xiàn) input 時留下的一個隱藏 bug。
總結一下
react 內部會給 Demo 組件中的每一個子元素添加一個 key(傳入或下標),然后將 key 作為 set 的鍵,之后通過最新的 key 去獲取 set 中儲存的值,如果存在復用原來元素,更新屬性,如果不存在,重新創(chuàng)建,修改 key 可以達到每次都重新創(chuàng)建元素,而不是復用原來的元素,這就是修改 key 進而達到修改 defaultValue 的原因。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。