浅谈React深度编程之受控组件与非受控组件
受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意。这恰恰显示react的威力,满足不同规模大小的工程需求。譬如你只是做listview这样简单的数据显示,将数据拍出来,那么for循坏与 {} 就足够了,但后台系统存在大量报表,不同的表单联动,缺了受控组件真的不行。
受控组件与非受控组件是react处理表单的入口。从react的思路来讲,作者肯定让数据控制一切,或者简单的理解为,页面的生成与更新得忠实地执行jsx的指令。
但是表单元素有其特殊之处,用户可以通过键盘输入与鼠标选择,改变界面的显示。界面的改变也意味着有一些数据被改动,比较明显的是input的 value ,textarea的 innerhtml ,radio/checkbox的 checked ,不太明显的是option的 selected 与 selectedindex ,这两个是被动修改的。
<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。但非受控组件的出发点是忠实于用户操作,如果用户在代码中
input.value = "xxxx"
以后
<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”时,这是由用户还是框架修改的。
if (!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之流则可能遇坑。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
下一篇: 直播脚本模板怎么写(直播带货脚本文案)