专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
OSC开源社区  ·  Bun ... ·  昨天  
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  2 天前  
OSC开源社区  ·  2024: 大模型背景下知识图谱的理性回归 ·  4 天前  
程序员的那些事  ·  惊!小偷“零元购”后竟向 DeepSeek ... ·  4 天前  
51好读  ›  专栏  ›  SegmentFault思否

React 深度编程:受控组件与非受控组件

SegmentFault思否  · 公众号  · 程序员  · 2017-12-24 08:00

正文

受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意。这恰恰显示React的威力,满足不同规模大小的工程需求。譬如你只是做ListView这样简单的数据显示,将数据拍出来,那么for循坏与 {} 就足够了,但后台系统存在大量报表,不同的表单联动,缺了受控组件真的不行。

受控组件与非受控组件是React处理表单的入口。从React的思路来讲,作者肯定让数据控制一切,或者简单的理解为,页面的生成与更新得忠实地执行JSX的指令。

但是表单元素有其特殊之处,用户可以通过键盘输入与鼠标选择,改变界面的显示。界面的改变也意味着有一些数据被改动,比较明显的是input的 value ,textarea的 innerHTML ,radio/checkbox的 checked ,不太明显的是option的 selected selectedIndex ,这两个是被动修改的。

  1. 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. defaultValue={this.state.value} />

就再不生效,一直是xxxx。

它怎么做到这一点,怎么辨识这个修改是来自框架内部或外部呢?我翻看了一下React的源码,原来它有一个叫valueTracker的东西跟踪用户的输入

  1. var tracker = {

  2.    getValue: function () {

  3.      return currentValue;

  4.    },

  5.    setValue: function (value) {

  6.      currentValue = '' + value;

  7.    },

  8.    stopTracking: function () {

  9.      detachTracker(node);

  10.       delete node[valueField];

  11.    }

  12.  };

  13.  return tracker;

  14. }

这个东西又是通过 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. f (!dom._uncontrolled) {

  2.    dom._uncontrolled = true;

  3.    inputMonitor.observe(dom, name); //重写defaultXXX的setter/getter

  4. }

  5. dom._observing = false;//此时是框架在修改视图,因此需要关闭开关

  6. dom[name] = val;

  7. dom._observing = true;//打开开关,来监听用户的修改行为

inputMonitor的实现如下

  1. export var inputMonitor = {};

  2. var rcheck = /checked|radio/;

  3. var describe = {

  4.     set: function(value) {

  5.        var controllProp = rcheck.test(this.type) ? "checked" : "value";

  6.        if (this.type === "textarea") {

  7.            this.innerHTML = value;

  8.        }

  9.        if (!this._observing) {

  10.            if (!this._setValue) {

  11.                //defaultXXX只会同步一次_persistValue

  12.                var parsedValue = (this[controllProp] = value);

  13.                this._persistValue = Array.isArray(value) ? value : parsedValue;

  14.                this._setValue = true;

  15.            }

  16.        } else {

  17.            //如果用户私下改变defaultValue,那么_setValue会被抺掉

  18.            this._setValue = value == null ? false : true;

  19.        }

  20.        this._defaultValue = value;

  21.    },

  22.    get: function() {

  23.        return this._defaultValue;

  24.    },

  25.    configurable: true

  26. };

  27. inputMonitor.observe = function(dom, name) {

  28.    try {

  29.        if ("_persistValue" in dom) {

  30.            dom._setValue = true;

  31.        }

  32.        Object.defineProperty(dom, name, describe);

  33.    } catch (e) {}

  34. };

又不小心贴了这么烧脑的代码,这是码农的坏毛病。不过,到这步,大家都明白,无论是官方react还是anu/qreact都是通过Object.defineProperty来控制用户的输入的。

于是我们可以理解以下的代码的行为了

  1.    var a =  ReactDOM.render(

  2.    ReactDOM.render(

  3.     ReactDOM.render(

  4.    expect(a.defaultValue).toBe("noise");

  5.    expect(a.value).toBe("foo");

  6.    expect(a.textContent).toBe("noise");

  7.    expect(a.innerHTML).toBe("noise");

由于用户一直没有手动修改 defaultValue, dom._setValue 一直为 false/undefined ,因此 _persistValue 一直能修改。

另一个例子:

  1. var renderTextarea = function(component, container) {

  2.    if (!container) {

  3.        container = document.createElement("div");

  4.    }

  5.    const node = ReactDOM.render(component, container);

  6.    node.defaultValue = node.innerHTML.replace(/^\n/, "");

  7.    return node;

  8. };

  9. const container = document.createElement("div");

  10. //注意这个方法,用户在renderTextarea中手动改变了defaultValue,_setValue就变成true

  11. const node = renderTextarea(

  12. expect(node.value).toBe("giraffe");

  13. // _setValue后,gorilla就不能同步到_persistValue,因此还是giraffe







请到「今天看啥」查看全文