浅谈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 相关文章推荐
javascript 写类方式之十
Jul 05 Javascript
关于JAVASCRIPT urldecode URL解码的问题
Jan 08 Javascript
javascript的document.referrer浏览器支持、失效情况总结
Jul 18 Javascript
javascript中的this详解
Dec 08 Javascript
jQuery中:input选择器用法实例
Jan 03 Javascript
js类式继承与原型式继承详解
Apr 07 Javascript
原生js封装的一些jquery方法(详解)
Sep 20 Javascript
Angular工具方法学习
Dec 26 Javascript
JS一个简单的注册页面实例
Sep 05 Javascript
详解Vue 动态组件与全局事件绑定总结
Nov 11 Javascript
element-ui 文件上传修改文件名的方法示例
Nov 05 Javascript
如何管理Vue中的缓存页面
Feb 06 Vue.js
使用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笔记之:php函数range() round()和list()的使用说明
2013/04/26 PHP
php中__destruct与register_shutdown_function执行的先后顺序问题
2014/10/17 PHP
简单了解将WordPress中的工具栏移到底部的小技巧
2015/12/31 PHP
thinkphp中多表查询中防止数据重复的sql语句(必看)
2016/09/22 PHP
php获取服务器操作系统相关信息的方法
2016/10/08 PHP
php实现 master-worker 守护多进程模式的实例代码
2019/07/20 PHP
jQuery UI AutoComplete 使用说明
2011/06/20 Javascript
JQuery为页面Dom元素绑定事件及解除绑定方法
2014/04/23 Javascript
我用的一些Node.js开发工具、开发包、框架等总结
2014/09/25 Javascript
跟我学习javascript的for循环和for...in循环
2015/11/18 Javascript
莱鸟介绍javascript onclick事件
2016/01/06 Javascript
BootStrap modal模态弹窗使用小结
2016/10/26 Javascript
vue学习笔记之v-if和v-show的区别
2017/09/20 Javascript
vue组件挂载到全局方法的示例代码
2018/08/02 Javascript
js根据需要计算数组中重复出现某个元素的个数
2019/01/18 Javascript
推荐一个基于Node.js的表单验证库
2019/02/15 Javascript
微信小程序在线客服自动回复功能(基于node)
2019/07/03 Javascript
js根据后缀判断文件文件类型的代码
2020/05/09 Javascript
ElementUI 修改默认样式的几种办法(小结)
2020/07/29 Javascript
Python利用flask sqlalchemy实现分页效果
2020/08/02 Python
python实现微信接口(itchat)详细介绍
2017/10/23 Python
Python发送http请求解析返回json的实例
2018/03/26 Python
基于Python中numpy数组的合并实例讲解
2018/04/04 Python
python求最大连续子数组的和
2018/07/07 Python
解决python ogr shp字段写入中文乱码的问题
2018/12/31 Python
Python字典的基本用法实例分析【创建、增加、获取、修改、删除】
2019/03/05 Python
Python上下文管理器类和上下文管理器装饰器contextmanager用法实例分析
2019/11/07 Python
自定义Django默认的sitemap站点地图样式
2020/03/04 Python
高中运动会入场词
2014/02/14 职场文书
努力学习演讲稿
2014/05/10 职场文书
真诚的求职信
2014/07/04 职场文书
擅自离岗检讨书
2014/09/12 职场文书
2014年十八届四中全会思想汇报范文
2014/10/17 职场文书
大学生逃课检讨书
2015/05/04 职场文书
2016年读书月活动总结范文
2016/04/06 职场文书
五年级作文之想象作文
2019/10/30 职场文书