React深度編程:受控組件與非受控組件

受控組件與非受控組件在官網與國內網上的資料都很少,有些人以爲它可有可不有,也不在乎。這偏偏顯示React的威力,知足不一樣規模大小的工程需求。譬如你只是作ListView這樣簡單的數據顯示,將數據拍出來,那麼for循壞與{}就足夠了,但後臺系統存在大量報表,不一樣的表單聯動,缺了受控組件真的不行。javascript

受控組件與非受控組件是React處理表單的入口。從React的思路來說,做者確定讓數據控制一切,或者簡單的理解爲,頁面的生成與更新得忠實地執行JSX的指令。java

可是表單元素有其特殊之處,用戶能夠經過鍵盤輸入與鼠標選擇,改變界面的顯示。界面的改變也意味着有一些數據被改動,比較明顯的是input的value,textarea的innerHTML,radio/checkbox的checked,不太明顯的是option的selectedselectedIndex,這兩個是被動修改的。node

<input value={this.state.value} />

當input.value是由組件的state.value拍出來的,當用戶進行輸入修改後,而後JSX再次重刷視圖,這時input.value是採起用戶的新值仍是state的新值?基於這個分歧,React給出一個折衷的方案,二者都支持,因而就產生了今天的主題了。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。dom

上面咱們已經說過,表單元素的顯示狀況是由內部的 persistValue 控制的,所以defaultXXX也會同步persistValue,而後再由persistValue同步DOM。但非受控組件的出發點是忠實於用戶操做,若是用戶在代碼中this

input.value = "xxxx"

之後code

<input defaultValue={this.state.value} />

就再不生效,一直是xxxx。

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

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"時,這是由用戶仍是框架修改的。

f (!dom._uncontrolled) {
    dom._uncontrolled = true;
    inputMonitor.observe(dom, name); //重寫defaultXXX的setter/getter
}
dom._observing = false;//此時是框架在修改視圖,所以須要關閉開關
dom[name] = val;
dom._observing = true;//打開開關,來監聽用戶的修改行爲

inputMonitor的實現以下

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來控制用戶的輸入的。

因而咱們能夠理解如下的代碼的行爲了

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 一直能修改。

另外一個例子:

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還有模糊匹配與精確匹配之分。

//精確匹配
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);//選中第三個
//模糊匹配
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之流則可能遇坑。

相關文章
相關標籤/搜索