淺談React深度編程之受控組件與非受控組件
受控組件與非受控組件在官網(wǎng)與國內(nèi)網(wǎng)上的資料都不多,有些人覺得它可有可不有,也不在意。這恰恰顯示React的威力,滿足不同規(guī)模大小的工程需求。譬如你只是做ListView這樣簡單的數(shù)據(jù)顯示,將數(shù)據(jù)拍出來,那么for循壞與 {} 就足夠了,但后臺(tái)系統(tǒng)存在大量報(bào)表,不同的表單聯(lián)動(dòng),缺了受控組件真的不行。
受控組件與非受控組件是React處理表單的入口。從React的思路來講,作者肯定讓數(shù)據(jù)控制一切,或者簡單的理解為,頁面的生成與更新得忠實(shí)地執(zhí)行JSX的指令。
但是表單元素有其特殊之處,用戶可以通過鍵盤輸入與鼠標(biāo)選擇,改變界面的顯示。界面的改變也意味著有一些數(shù)據(jù)被改動(dòng),比較明顯的是input的 value ,textarea的 innerHTML ,radio/checkbox的 checked ,不太明顯的是option的 selected 與 selectedIndex ,這兩個(gè)是被動(dòng)修改的。
<input value="{this.state.value}"/>
當(dāng)input.value是由組件的state.value拍出來的,當(dāng)用戶進(jìn)行輸入修改后,然后JSX再次重刷視圖,這時(shí)input.value是采取用戶的新值還是state的新值?基于這個(gè)分歧,React給出一個(gè)折衷的方案,兩者都支持,于是就產(chǎn)生了今天的主題了。
React認(rèn)為value/checked不能單獨(dú)存在,需要與onInput/onChange/disabed/readOnly等控制value/checked的屬性或事件一起使用。 它們共同構(gòu)成 受控組件 ,受控是受JSX的控制。如果用戶沒有寫這些額外的屬性與事件,那么框架內(nèi)部會(huì)給它添加一些事件,如onClick, onInput, onChange,阻止你進(jìn)行輸入或選擇,讓你無法修改它的值。在框架內(nèi)部,有一個(gè)頑固的變量,我稱之為 persistValue,它一直保持JSX上次賦給它的值,只能讓內(nèi)部事件修改它。
因此我們可以斷言,受控組件是可通過 事件 完成的對value的控制。
在受控組件中,persistValue總能被刷新。
我們再看非受控組件,既然value/checked已經(jīng)被占用了,React啟用了HTML中另一組被忽略的屬性defaultValue/defaultChecked。一般認(rèn)為它們是與value/checked相通的,即,value不存在的情況下,defaultValue的值就當(dāng)作是value。
上面我們已經(jīng)說過,表單元素的顯示情況是由內(nèi)部的 persistValue 控制的,因此defaultXXX也會(huì)同步persistValue,然后再由persistValue同步DOM。但非受控組件的出發(fā)點(diǎn)是忠實(shí)于用戶操作,如果用戶在代碼中
input.value = "xxxx"
以后
<input defaultvalue="{this.state.value}"/>
就再不生效,一直是xxxx。
它怎么做到這一點(diǎn),怎么辨識這個(gè)修改是來自框架內(nèi)部或外部呢?我翻看了一下React的源碼,原來它有一個(gè)叫valueTracker的東西跟蹤用戶的輸入
var tracker = { getValue: function () { return currentValue; }, setValue: function (value) { currentValue = '' + value; }, stopTracking: function () { detachTracker(node); delete node[valueField]; } }; return tracker; }
這個(gè)東西又是通過Object.defineProperty打進(jìn)元素的value/checked的內(nèi)部,因此就知曉用戶對它的取值賦值操作。
但value/checked還是兩個(gè)很核心的屬性,涉及到太多內(nèi)部機(jī)制(比如說value與oninput, onchange, 輸入法事件oncompositionstart,
compositionchange, oncompositionend, onpaste, oncut),為了平緩地修改value/checked,
還要用到 Object.getOwnPropertyDescriptor 。如果我要兼容IE8,沒有這么高級的玩藝兒。我采取另一種更安全的方式,
只用Object.defineProperty修改 defaultValue/defaultChecked 。
首先我為元素添加一個(gè) _uncontrolled 的屬性,用來表示我已經(jīng)劫持過defaultXXX。 然后描述對象 ( Object.defineProperty的第三個(gè)參數(shù) )的set方法里面再添加一個(gè)開關(guān), _observing 。在框架內(nèi)部更新視圖,此值為false,更新完,它置為true。
這樣就知曉 input.defaultValue = “xxx”時(shí),這是由用戶還是框架修改的。
if (!dom._uncontrolled) { dom._uncontrolled = true; inputMonitor.observe(dom, name); //重寫defaultXXX的setter/getter } dom._observing = false;//此時(shí)是框架在修改視圖,因此需要關(guān)閉開關(guān) dom[name] = val; dom._observing = true;//打開開關(guān),來監(jiān)聽用戶的修改行為
inputMonitor的實(shí)現(xiàn)如下
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只會(huì)同步一次_persistValue var parsedValue = (this[controllProp] = value); this._persistValue = Array.isArray(value) ? value : parsedValue; this._setValue = true; } } else { //如果用戶私下改變defaultValue,那么_setValue會(huì)被抺掉 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) {} };
又不小心貼了這么燒腦的代碼,這是碼農(nóng)的壞毛病。不過,到這步,大家都明白,無論是官方react還是anu/qreact都是通過Object.defineProperty來控制用戶的輸入的。
于是我們可以理解以下的代碼的行為了
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");
由于用戶一直沒有手動(dòng)修改 defaultValue, dom._setValue 一直為 false/undefined ,因此 _persistValue 一直能修改。
另一個(gè)例子:
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"); //注意這個(gè)方法,用戶在renderTextarea中手動(dòng)改變了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"; // 這個(gè)又是什么回事了呢,因此非監(jiān)控屬性是在diffProps中批量處理的,在監(jiān)控屬性,則是在更后的方法中處理 // 檢測到node.value !== _persistValue,于是重寫 _persistValue = node.value,于是輸出cat renderTextarea(<textarea defaultValue="monkey" />, container); expect(node.value).toEqual("cat");
純文本類:text, textarea, JSX的值,總是往字符串轉(zhuǎn)換
type=”number”的控制,值總是為數(shù)字,不填或?yàn)椤啊眲t轉(zhuǎn)換為“0”
radio有聯(lián)動(dòng)效果,同一父節(jié)點(diǎn)下的相同name的radio控制只能選擇一個(gè)。
select的value/defaultValue支持?jǐn)?shù)組,不做轉(zhuǎn)換,但用戶對底下的option元素做增刪操作,selected會(huì)跟著變動(dòng)。
此外select還有模糊匹配與精確匹配之分。
//精確匹配 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);//選中第三個(gè)
//模糊匹配 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);//選中第二個(gè)
凡此種種,React/anu都是做了大量工作,迷你如preact/react-lite之流則可能遇坑。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
如何不使用eject修改create-react-app的配置
許多剛開始接觸create-react-app框架的同學(xué),不免都會(huì)有個(gè)疑問:如何在不執(zhí)行eject操作的同時(shí),修改create-react-app的配置。2021-04-04React?Native實(shí)現(xiàn)Toast輕提示和loading效果
這篇文章主要介紹了React Native實(shí)現(xiàn)Toast輕提示和loading效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09React項(xiàng)目配置prettier和eslint的方法
這篇文章主要介紹了React項(xiàng)目配置prettier和eslint的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06React+hook實(shí)現(xiàn)聯(lián)動(dòng)模糊搜索
這篇文章主要為大家詳細(xì)介紹了如何利用React+hook+antd實(shí)現(xiàn)聯(lián)動(dòng)模糊搜索功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02React找不到模塊“./index.module.scss”或其相應(yīng)的類型聲明及解決方法
這篇文章主要介紹了React找不到模塊“./index.module.scss”或其相應(yīng)的類型聲明及解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09react中如何使用定義數(shù)據(jù)并監(jiān)聽其值
這篇文章主要介紹了react中如何使用定義數(shù)據(jù)并監(jiān)聽其值問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01