浅谈React深度编程之受控组件与非受控组件


Posted in Javascript onDecember 26, 2017

受控组件与非受控组件在官网与国内网上的资料都不多,有些人觉得它可有可不有,也不在意。这恰恰显示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之流则可能遇坑。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
【消息提示组件】,兼容IE6/7&amp;&amp;FF2
Sep 04 Javascript
js取消单选按钮选中示例代码
Nov 14 Javascript
动态创建script在IE中缓存js文件时导致编码的解决方法
May 04 Javascript
jQuery ui实现动感的圆角渐变网站导航菜单效果代码
Aug 26 Javascript
jQuery xml字符串的解析、读取及查找方法
Mar 01 Javascript
node.js实现的装饰者模式示例
Sep 06 Javascript
vue.js将时间戳转化为日期格式的实现代码
Jun 05 Javascript
Vue和React组件之间的传值方式详解
Jan 31 Javascript
js常用正则表达式集锦
May 17 Javascript
微信公众号平台接口开发 获取微信服务器IP地址方法解析
Aug 14 Javascript
layer弹出框确定前验证:弹出消息框的方法(弹出两个layer)
Sep 21 Javascript
webpack常用配置总览(小结)
Nov 18 Javascript
使用vue实现简单键盘的示例(支持移动端和pc端)
Dec 25 #Javascript
vue的一个分页组件的示例代码
Dec 25 #Javascript
jQuery图片查看插件Magnify开发详解
Dec 25 #jQuery
AngularJS实现的生成随机数与猜数字大小功能示例
Dec 25 #Javascript
推荐10款扩展Web表单的JS插件
Dec 25 #Javascript
jQuery实现右侧抽屉式在线客服功能
Dec 25 #jQuery
用React-Native+Mobx做一个迷你水果商城APP(附源码)
Dec 25 #Javascript
You might like
php生成缩略图的类代码
2008/10/02 PHP
PHP正则表达式入门教程(推荐)
2016/05/18 PHP
php for 循环使用的简单实例
2016/06/02 PHP
thinkphp框架实现删除和批量删除
2016/06/29 PHP
Yii2创建表单(ActiveForm)方法详解
2016/07/23 PHP
php简单压缩css样式示例
2016/09/22 PHP
php中final关键字用法分析
2016/12/07 PHP
对JavaScript的eval()中使用函数的进一步讨论
2008/07/26 Javascript
javascript mouseover、mouseout停止事件冒泡的解决方案
2009/04/07 Javascript
js对象数组按属性快速排序
2011/01/31 Javascript
用正则表达式替换图片地址img标签
2013/11/22 Javascript
Json序列化和反序列化方法解析
2013/12/19 Javascript
Javascript定义类(class)的三种方法详解
2015/03/13 Javascript
JavaScript 计算笛卡尔积实例详解
2016/12/02 Javascript
Vue监听数据对象变化源码
2017/03/09 Javascript
微信小程序 在线支付功能的实现
2017/03/14 Javascript
VueJS 组件参数名命名与组件属性转化问题
2018/12/03 Javascript
列举Python中吸引人的一些特性
2015/04/09 Python
浅谈python中的面向对象和类的基本语法
2016/06/13 Python
利用python爬取软考试题之ip自动代理
2017/03/28 Python
对numpy 数组和矩阵的乘法的进一步理解
2018/04/04 Python
对Python中数组的几种使用方法总结
2018/06/28 Python
Python进阶之全面解读高级特性之切片
2019/02/19 Python
django配置连接数据库及原生sql语句的使用方法
2019/03/03 Python
python通过TimedRotatingFileHandler按时间切割日志
2019/07/17 Python
QML使用Python的函数过程解析
2019/09/26 Python
wxpython绘制圆角窗体
2019/11/18 Python
欧洲最大的笔和书写专家:The Pen Shop
2017/03/19 全球购物
园林施工员岗位职责
2013/12/11 职场文书
2014庆六一活动方案
2014/03/02 职场文书
法人任命书范本
2014/06/04 职场文书
个人催款函范文
2015/06/23 职场文书
趣味运动会赞词
2015/07/22 职场文书
大学生团支书竞选稿
2015/11/21 职场文书
优秀新员工事迹材料
2019/05/13 职场文书
SQLServer2019 数据库的基本使用之图形化界面操作的实现
2021/04/08 SQL Server