受控组件与非受控组件在官网与国内网上的资料都很少,有些人以为它可有可不有,也不在乎。这偏偏显示React的威力,知足不一样规模大小的工程需求。譬如你只是作ListView这样简单的数据显示,将数据拍出来,那么for循坏与{}
就足够了,但后台系统存在大量报表,不一样的表单联动,缺了受控组件真的不行。javascript
受控组件与非受控组件是React处理表单的入口。从React的思路来说,做者确定让数据控制一切,或者简单的理解为,页面的生成与更新得忠实地执行JSX的指令。java
可是表单元素有其特殊之处,用户能够经过键盘输入与鼠标选择,改变界面的显示。界面的改变也意味着有一些数据被改动,比较明显的是input的value,textarea的innerHTML,radio/checkbox的checked,不太明显的是option的selected与selectedIndex,这两个是被动修改的。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之流则可能遇坑。