React 深度程式設計:受控元件與非受控元件

發表於2017-12-25

受控元件與非受控元件在官網與國內網上的資料都不多,有些人覺得它可有可不有,也不在意。這恰恰顯示React的威力,滿足不同規模大小的工程需求。譬如你只是做ListView這樣簡單的資料顯示,將資料拍出來,那麼for循壞與{}就足夠了,但後臺系統存在大量報表,不同的表單聯動,缺了受控元件真的不行。

受控元件與非受控元件是React處理表單的入口。從React的思路來講,作者肯定讓資料控制一切,或者簡單的理解為,頁面的生成與更新得忠實地執行JSX的指令。

但是表單元素有其特殊之處,使用者可以通過鍵盤輸入與滑鼠選擇,改變介面的顯示。介面的改變也意味著有一些資料被改動,比較明顯的是input的value,textarea的innerHTML,radio/checkbox的checked,不太明顯的是option的selectedselectedIndex,這兩個是被動修改的。

當input.value是由元件的state.value拍出來的,當使用者進行輸入修改後,然後JSX再次重刷檢視,這時input.value是採取使用者的新值還是state的新值?基於這個分歧,React給出一個折衷的方案,兩者都支援,於是就產生了今天的主題了。

React認為value/checked不能單獨存在,需要與onInput/onChange/disabed/readOnly等控制value/checked的屬性或事件一起使用。 它們共同構成受控元件,受控是受JSX的控制。如果使用者沒有寫這些額外的屬性與事件,那麼框架內部會給它新增一些事件,如onClick, onInput, onChange,阻止你進行輸入或選擇,讓你無法修改它的值。在框架內部,有一個頑固的變數,我稱之為 persistValue,它一直保持JSX上次賦給它的值,只能讓內部事件修改它。

因此我們可以斷言,受控元件是可通過事件完成的對value的控制。

在受控元件中,persistValue總能被重新整理。

我們再看非受控元件,既然value/checked已經被佔用了,React啟用了HTML中另一組被忽略的屬性defaultValue/defaultChecked。一般認為它們是與value/checked相通的,即,value不存在的情況下,defaultValue的值就當作是value。

上面我們已經說過,表單元素的顯示情況是由內部的 persistValue 控制的,因此defaultXXX也會同步persistValue,然後再由persistValue同步DOM。但非受控元件的出發點是忠實於使用者操作,如果使用者在程式碼中

以後

就再不生效,一直是xxxx。

它怎麼做到這一點,怎麼辨識這個修改是來自框架內部或外部呢?我翻看了一下React的原始碼,原來它有一個叫valueTracker的東西跟蹤使用者的輸入

這個東西又是通過Object.defineProperty打進元素的value/checked的內部,因此就知曉使用者對它的取值賦值操作。

但value/checked還是兩個很核心的屬性,涉及到太多內部機制(比如說value與oninput, onchange, 輸入法事件oncompositionstart,
compositionchange, oncompositionend, onpaste, oncut),為了平緩地修改value/checked,
還要用到Object.getOwnPropertyDescriptor。如果我要相容IE8,沒有這麼高階的玩藝兒。我採取另一種更安全的方式,
只用Object.defineProperty修改defaultValue/defaultChecked

首先我為元素新增一個_uncontrolled的屬性,用來表示我已經劫持過defaultXXX。 然後描述物件 (Object.defineProperty的第三個引數)的set方法裡面再新增一個開關,_observing。在框架內部更新檢視,此值為false,更新完,它置為true。

這樣就知曉 input.defaultValue = “xxx”時,這是由使用者還是框架修改的。

inputMonitor的實現如下

又不小心貼了這麼燒腦的程式碼,這是碼農的壞毛病。不過,到這步,大家都明白,無論是官方react還是anu/qreact都是通過Object.defineProperty來控制使用者的輸入的。

於是我們可以理解以下的程式碼的行為了

由於使用者一直沒有手動修改 defaultValue,dom._setValue 一直為false/undefined,因此 _persistValue 一直能修改。

另一個例子:

純文字類:text, textarea, JSX的值,總是往字串轉換
type=”number”的控制,值總是為數字,不填或為“”則轉換為“0”
radio有聯動效果,同一父節點下的相同name的radio控制只能選擇一個。
select的value/defaultValue支援陣列,不做轉換,但使用者對底下的option元素做增刪操作,selected會跟著變動。

此外select還有模糊匹配與精確匹配之分。

凡此種種,React/anu都是做了大量工作,迷你如preact/react-lite之流則可能遇坑。

相關文章