受控元件與非受控元件在官網與國內網上的資料都不多,有些人覺得它可有可不有,也不在意。這恰恰顯示React的威力,滿足不同規模大小的工程需求。譬如你只是做ListView這樣簡單的資料顯示,將資料拍出來,那麼for循壞與{}
就足夠了,但後臺系統存在大量報表,不同的表單聯動,缺了受控元件真的不行。
受控元件與非受控元件是React處理表單的入口。從React的思路來講,作者肯定讓資料控制一切,或者簡單的理解為,頁面的生成與更新得忠實地執行JSX的指令。
但是表單元素有其特殊之處,使用者可以通過鍵盤輸入與滑鼠選擇,改變介面的顯示。介面的改變也意味著有一些資料被改動,比較明顯的是input的value,textarea的innerHTML,radio/checkbox的checked,不太明顯的是option的selected與selectedIndex,這兩個是被動修改的。
1 |
<input value="{this.state.value}"/> |
當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。但非受控元件的出發點是忠實於使用者操作,如果使用者在程式碼中
1 |
input.value = "xxxx" |
以後
1 |
<input defaultvalue="{this.state.value}"/> |
就再不生效,一直是xxxx。
它怎麼做到這一點,怎麼辨識這個修改是來自框架內部或外部呢?我翻看了一下React的原始碼,原來它有一個叫valueTracker的東西跟蹤使用者的輸入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var tracker = { getValue: function () { return currentValue; }, setValue: function (value) { currentValue = '' + value; }, stopTracking: function () { detachTracker(node); delete node[valueField]; } }; return tracker; } |
這個東西又是通過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”時,這是由使用者還是框架修改的。
1 2 3 4 5 6 7 |
f (!dom._uncontrolled) { dom._uncontrolled = true; inputMonitor.observe(dom, name); //重寫defaultXXX的setter/getter } dom._observing = false;//此時是框架在修改檢視,因此需要關閉開關 dom[name] = val; dom._observing = true;//開啟開關,來監聽使用者的修改行為 |
inputMonitor的實現如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
export var inputMonitor = {}; var rcheck = /checked|radio/; var describe = { set: function(value) { var controllProp = rcheck.test(this.type) ? "checked" : "value"; if (this.type === "textarea") { this.innerHTML = value; } if (!this._observing) { if (!this._setValue) { //defaultXXX只會同步一次_persistValue var parsedValue = (this[controllProp] = value); this._persistValue = Array.isArray(value) ? value : parsedValue; this._setValue = true; } } else { //如果使用者私下改變defaultValue,那麼_setValue會被抺掉 this._setValue = value == null ? false : true; } this._defaultValue = value; }, get: function() { return this._defaultValue; }, configurable: true }; inputMonitor.observe = function(dom, name) { try { if ("_persistValue" in dom) { dom._setValue = true; } Object.defineProperty(dom, name, describe); } catch (e) {} }; |
又不小心貼了這麼燒腦的程式碼,這是碼農的壞毛病。不過,到這步,大家都明白,無論是官方react還是anu/qreact都是通過Object.defineProperty來控制使用者的輸入的。
於是我們可以理解以下的程式碼的行為了
1 2 3 4 5 6 7 |
var a = ReactDOM.render(<textarea defaultValue="foo" />, container); ReactDOM.render(<textarea defaultValue="bar" />, container); ReactDOM.render(<textarea defaultValue="noise" />, container); expect(a.defaultValue).toBe("noise"); expect(a.value).toBe("foo"); expect(a.textContent).toBe("noise"); expect(a.innerHTML).toBe("noise"); |
由於使用者一直沒有手動修改 defaultValue,dom._setValue
一直為false/undefined,因此 _persistValue
一直能修改。
另一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var renderTextarea = function(component, container) { if (!container) { container = document.createElement("div"); } const node = ReactDOM.render(component, container); node.defaultValue = node.innerHTML.replace(/^\n/, ""); return node; }; const container = document.createElement("div"); //注意這個方法,使用者在renderTextarea中手動改變了defaultValue,_setValue就變成true const node = renderTextarea(<textarea defaultValue="giraffe" />, container); expect(node.value).toBe("giraffe"); // _setValue後,gorilla就不能同步到_persistValue,因此還是giraffe renderTextarea(<textarea defaultValue="gorilla" />, container); // expect(node.value).toEqual("giraffe"); node.value = "cat"; // 這個又是什麼回事了呢,因此非監控屬性是在diffProps中批量處理的,在監控屬性,則是在更後的方法中處理 // 檢測到node.value !== _persistValue,於是重寫 _persistValue = node.value,於是輸出cat renderTextarea(<textarea defaultValue="monkey" />, container); expect(node.value).toEqual("cat"); |
純文字類:text, textarea, JSX的值,總是往字串轉換
type=”number”的控制,值總是為數字,不填或為“”則轉換為“0”
radio有聯動效果,同一父節點下的相同name的radio控制只能選擇一個。
select的value/defaultValue支援陣列,不做轉換,但使用者對底下的option元素做增刪操作,selected會跟著變動。
此外select還有模糊匹配與精確匹配之分。
1 2 3 4 5 6 7 8 9 10 11 |
//精確匹配 var dom = ReactDOM.render( <select value={222}> <option value={111}>aaa</option> <option value={"222"}>xxx</option> <option value={222}>bbb</option> <option value={333}>ccc</option> </select>, container ); expect(dom.options[2].selected).toBe(true);//選中第三個 |
1 2 3 4 5 6 7 8 9 10 |
//模糊匹配 var dom = ReactDOM.render( <select value={222}> <option value={111}>aaa</option> <option value={"222"}>xxx</option> <option value={333}>ccc</option> </select>, container ); expect(dom.options[2].selected).toBe(true);//選中第二個 |
凡此種種,React/anu都是做了大量工作,迷你如preact/react-lite之流則可能遇坑。