受控和非受控组件真的那么难理解吗?(React实际案例详解)

23,410 阅读11分钟

前言

你盼世界,我盼望你无bug。Hello 大家好!我是霖呆呆。

最近都没怎么输出了😂,不是停更通知就是"软文",还是有点不好意思的。问题不大,我道(皮)谦(厚)咯😄。

所以今天咱再来聊点技术相关的东西吧,也就是本篇的标题——受控和非受控组件。

写这篇文章的原因是呆呆在写HOC时有涉及到受控和非受控组件的内容,然后发现能说的内容还是挺多的,但是搜索了一下网络上的教材大多说的都比较混乱,对新手来说不太好理解。

所以呆呆也是希望能发挥自身所长将这部分内容说的短而精,方便大家理解。

(没错,这里的长就是你们想的长,而短不是你们想的短...)

好的👌,不皮了😁,来看看通过阅读本篇文章你可以学习到:

  • 受控组件基本概念
  • select受控组件
  • 动态表单受控组件案例
  • 非受控组件
  • 特殊的文件file标签

正文

受控组件基本概念

通过名称,我们可以猜测一下这两个词是什么意思:

  • 受控组件:受我们控制的组件
  • 非控组件:不受我们控制的组件

(这提莫的不是废话吗...)

咳咳,好吧,这里的受控和非控是什么意思呢?其实也就是我们对某个组件状态的掌控,它的值是否只能由用户设置,而不能通过代码控制

我们知道,在React中定义了一个input输入框的话,它并没有类似于Vuev-model的这种双向绑定功能。也就是说,我们并没有一个指令能够将数据和输入框结合起来,用户在输入框中输入内容,然后数据同步更新。

就像下面这个案例:

class TestComponent extends React.Component {
  render () {
    return <input name="username" />
  }
}

用户在界面上的输入框输入内容时,它是自己维护了一个"state",这样的话就能根据用户的输入自己进行UI上的更新。(这个state并不是我们平常看见的this.state,而是每个表单元素上抽象的state)

想想此时如果我们想要控制输入框的内容可以怎样做呢?唔...输入框的内容取决的是input中的value属性,那么我们可以在this.state中定义一个名为username的属性,并将input上的value指定为这个属性:

class TestComponent extends React.Component {
  constructor (props) {
    super(props);
    this.state = { username: 'lindaidai' };
  }
  render () {
    return <input name="username" value={this.state.username} />
  }
}

但是这时候你会发现input的内容是只读的,因为value会被我们的this.state.username所控制,当用户输入新的内容时,this.state.username并不会自动更新,这样的话input内的内容也就不会变了。

哈哈,你可能已经想到了,我们可以用一个onChange事件来监听输入内容的改变并使用setState更新this.state.username

class TestComponent extends React.Component {
  constructor (props) {
    super(props);
    this.state = {
      username: "lindaidai"
    }
  }
  onChange (e) {
    console.log(e.target.value);
    this.setState({
      username: e.target.value
    })
  }
  render () {
    return <input name="username" value={this.state.username} onChange={(e) => this.onChange(e)} />
  }
}

现在不论用户输入什么内容stateUI都会跟着更新了,并且我们可以在组件中的其它地方使用this.state.username来获取到input里的内容,也可以通过this.setState()来修改input里的内容。

OK👌,现在让我们来看看受控组件的定义:

在HTML的表单元素中,它们通常自己维护一套state,并随着用户的输入自己进行UI上的更新,这种行为是不被我们程序所管控的。而如果将React里的state属性和表单元素的值建立依赖关系,再通过onChange事件与setState()结合更新state属性,就能达到控制用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素就叫做受控组件

(额,呆呆认为上面👆这个总结就可以用在面试当中了)

select受控组件

在上面呆呆用input向大家演示了一个最基本的受控组件,那么其实对于其它的表单元素使用起来也差不多,可能就是属性名和事件不同而已。

例如input类型为text的表单元素中使用的是:

  • value
  • onChange

对于textarea标签也和它一样是使用valueonChange

<textarea value={this.state.value} onChange={this.handleChange} />

单选select

对于select表单元素来说,React中将其转化为受控组件可能和原生HTML中有一些区别。

在原生中,我们默认一个select选项选中使用的是selected,比如下面这样:

<select>
  <option value="sunshine">阳光</option>
  <option value="handsome">帅气</option>
  <option selected value="cute">可爱</option>
  <option value="reserved">高冷</option>
</select>

"可爱"的选项设置了selected,默认选中的就是它了。

但是如果是使用React受控组件来写的话就不用那么麻烦了,因为它允许在根select标签上使用value属性,去控制选中了哪个。这样的话,对于我们也更加便捷,在用户每次重选之后我们只需要在根标签中更新它,就像是这个案例🌰:

class SelectComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: 'cute' };
  }
  handleChange(event) {
    this.setState({value: event.target.value});
  }
  handleSubmit(event) {
    alert('你今日相亲对象的类型是: ' + this.state.value);
    event.preventDefault();
  }
  render() {
    return (
      <form onSubmit={(e) => this.handleSubmit(e)}>
        <label>
          你今日相亲对象的类型是:
          <select value={this.state.value} onChange={(e) => this.handleChange(e)}>
            <option value="sunshine">阳光</option>
            <option value="handsome">帅气</option>
            <option value="cute">可爱</option>
            <option value="reserved">高冷</option>
          </select>
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}
export default SelectComponent;

可以看到不论是input类型为text的控件还是textarea、select在实现为受控组件上都差不多。

多选select

多选select的话,对比单选来说,只有这两处改动:

  • select标签设置multiple属性为true
  • select标签value绑定的值为一个数组

呆呆这里也来小小的写一个案例吧:

class SelectComponent extends React.Component {
  constructor(props) {
    super(props);
    // this.state = { value: 'cute' };
    this.state = { value: ['cute'] };
  }

  handleChange(event) {
    console.log(event.target.value)
    const val = event.target.value;
    const oldValue = this.state.value;
    const i = this.state.value.indexOf(val);
    const newValue = i > -1 ? [...oldValue].splice(i, 1) : [...oldValue, val];
    this.setState({value: newValue});
  }

  handleSubmit(event) {
    alert('你今日相亲对象的类型是: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={(e) => this.handleSubmit(e)}>
        <label>
          你今日相亲对象的类型是:
          <select multiple={true} value={this.state.value} onChange={(e) => this.handleChange(e)}>
            <option value="sunshine">阳光</option>
            <option value="handsome">帅气</option>
            <option value="cute">可爱</option>
            <option value="reserved">高冷</option>
          </select>
        </label>
        <input type="submit" value="提交" />
      </form>
    );
  }
}
export default SelectComponent;

(但是呆呆在Mac,Chrome 测试这个多选好像是有问题的)

动态表单受控组件案例

上面👆咱们实现了一些简单的受控组件案例,接着来玩个稍微难点的。

先看一下我们的需求:

实现一个组件,传入以下数组,自动渲染出表单:

(CInput代表一个输入框,CSelect代表一个选择框)

// 决定表单的结构
const formConfig = [
  {
    Component: CInput,
    label: '姓名',
    field: 'name',
  },
  {
    Component: CSelect,
    label: '性别',
    field: 'sex',
    options: [{ label: '男', value: 'man' }, { label: '女', value: 'woman' }]
  }
]
// 决定表单的内容
this.state = {
  name: '霖呆呆',
  sex: 'man'
}

效果:

也就是来实现一个简单的动态表单,看看受控组件在其中的应用。

  • formConfig决定了表单的结构,也就是定义表单中会有哪些项

  • this.state中定义了表单中各项的值是什么,它与formConfig是靠formConfig中各项的field字段来建立链接的。

知道了上面👆这些东西,我们就能很快写出这个动态表单组件的大概样子了:

(我们就把这个组件命名为formComponent吧,重点看render部分)

import React, { Component } from 'react';
import { CInput, CSelect } from './components'
export default class FormComponent extends Component {
	constructor (props) {
    super(props);
    this.state = {
      name: '霖呆呆',
      sex: 'man'
    }
  }
  formConfig = [
    {
      Component: CInput,
      label: '姓名',
      field: 'name',
    },
    {
      Component: CSelect,
      label: '性别',
      field: 'sex',
      options: [{ label: '男', value: 'man' }, { label: '女', value: 'woman' }]
    }
  ]
  render () { // 重点在这
    return (
      <form style={{marginTop: '50px'}}>
        {
          this.formConfig.map((item) => { // 枚举formConfig
            const { Type, field, name } = item;
            const ControlComponent = item.Component; // 提取Component
            return (
              <ControlComponent key={field} />
            )
          })
        }
      </form>
    )
  }
}

可以看到,render部分我们做了这么几件事:

  • 枚举formConfig数组
  • 提取出每一项里的Component,赋值给ControlComponent
  • 渲染出每一项Component

ControlComponent变量的意义在于告诉React,需要渲染出哪一个组件,如果item.ComponentCInput,那么最终渲染出的就是<CInput />

这样就保证了能把formConfig数组中的每一个表单项都渲染出来,但是这些表单项现在还是不受我们控制的,我们需要用前面学到的valueonChange和每个ControlComponent建立联系,就像是这样:

<ControlComponent
  key={field}
  name={field}
  value={this.state[field]}
  onChange={this.onChange}
  {...item}
/>

我们把this.state[field]设置到value上,把this.onChange设置到onChange属性上。(可想而之,CInputCSelect组件中就能用this.props来接收传入的属性了,例如this.props.value)

那么这时候value已经确定了,它就是由this.state[field]决定的,如果field"sex"的话,value的值就是"man"

所以来看看onChange方法该怎样写:

onChange = (event, field) => {
  const target = event.target;
  this.setState({
    [field]: target.value
  })
}

这个方法其实也很简单,它接受一个eventfield,在event中就可以获取到用户输入/选择的值了。

好的👌,接下来让我们快速的看一下CInputCSelect是如何实现的吧:

components/CInput.jsx:

import React, { Component } from 'react';

export default class CInput extends Component {
  constructor (props) {
    super(props);
  }
  render () {
    const { name, field, value, onChange } = this.props;
    return (
      <>
        <label>
          {name}
        </label>
        <input name={field} value={value} onChange={(e) => onChange(e, field)} />
      </>
    )
  }
}

components/CSelect.jsx:

import React, { Component } from 'react';

export default class CSelect extends Component {
  constructor (props) {
    super(props);
  }
  render () {
    const { name, field, options, value, onChange } = this.props;
    return (
      <>
        <label>
          {name}
        </label>
        <select name={field} value={value} onChange={(e) => onChange(e, field)}>
          {options.length>0 && options.map(option => {
            return <option key={option.value} value={option.value}>{option.label}</option>
          })}
        </select>
      </>
    )
  }
}

当然,这里演示的仅仅是一个简单的动态表单的实现,如果你想要在项目中实现的话要远比这个复杂多了。

非受控组件

上面👆向大家展示的是受控组件的一些基本概念还有相关操作,对于受控组件,我们需要为每个状态更新(例如this.state.username)编写一个事件处理程序(例如this.setState({ username: e.target.value }))。

那么还有一种场景是:我们仅仅是想要获取某个表单元素的值,而不关心它是如何改变的。对于这种场景,我们有什么应对的方法吗🤔️?

唔...input标签它实际也是一个DOM元素,那么我们是不是可以用获取DOM元素信息的方式来获取表单元素的值呢?也就是使用ref

就像下面👇这个案例一样:

import React, { Component } from 'react';

export class UnControll extends Component {
  constructor (props) {
    super(props);
    this.inputRef = React.createRef();
  }
  handleSubmit = (e) => {
    console.log('我们可以获得input内的值为', this.inputRef.current.value);
    e.preventDefault();
  }
  render () {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        <input defaultValue="lindaidai" ref={this.inputRef} />
        <input type="submit" value="提交" />
      </form>
    )
  }
}

在输入框输入内容后,点击提交按钮,我们可以通过this.inputRef成功拿到inputDOM属性信息,包括用户输入的值,这样我们就不需要像受控组件一样,单独的为每个表单元素维护一个状态。

同时我们也可以用defaultValue属性来指定表单元素的默认值。

特殊的文件file标签

另外在input中还有一个比较特殊的情况,那就是file类型的表单控件。

对于file类型的表单控件它始终是一个不受控制的组件,因为它的值只能由用户设置,而不是以编程方式设置。

例如我现在想要通过状态更新来控制它:

import React, { Component } from 'react';

export default class UnControll extends Component {
  constructor (props) {
    super(props);
    this.state = {
      files: []
    }
  }
  handleSubmit = (e) => {
    e.preventDefault();
  }
  handleFile = (e) => {
    console.log(e.target.files);
    const files = [...e.target.files];
    console.log(files);
    this.setState({
      files
    })
  }
  render () {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        <input type="file" value={this.state.files} onChange={(e) => this.handleFile(e)} />
        <input type="submit" value="提交" />
      </form>
    )
  }
}

在选择了文件之后,我试图用setState来更新,结果却报错了:

所以我们应当使用非受控组件的方式来获取它的值,可以这样写:

import React, { Component } from 'react';

export default class FileComponent extends Component {
  constructor (props) {
    super(props);
    this.fileRef = React.createRef();
  }
  handleSubmit = (e) => {
    console.log('我们可以获得file的值为', this.fileRef.current.files);
    e.preventDefault();
  }
  render () {
    return (
      <form onSubmit={e => this.handleSubmit(e)}>
        <input type="file" ref={this.fileRef} />
        <input type="submit" value="提交" />
      </form>
    )
  }
}

这里获取到的files是一个数组哈,当然,如果你没有开启多选的话,这个数组的长度始终是1,开启多选也非常简单,只需要添加multiple属性即可:

<input type="file" multiple ref={this.fileRef} />

OK,相信大家对这两组概念已经有了一个清晰的认识。什么?你问我实际的应用场景?

唔...这个用React官方的话来说,绝大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React组件负责处理;当然如果选择受受控组件的话,表单数据就由DOM本身处理。

另外在学习两者的时候,呆呆也发现了一些写的比较好的文章,比这篇更深入,推荐给大家哟:

后语

你盼世界,我盼望你无bug。这篇文章就介绍到这里。

主要是向大家介绍了一下控件与非控件组件的区别和用法,呆呆也终于"重操旧业"写起了文章,而且今天意外的发现掘金年中总结竟然上榜了,再次谢谢大家的不吝喜欢❤️,哈哈,相信大家也会越来越好的!

喜欢「霖呆呆」的小伙还希望可以关注霖呆呆的公众号 LinDaiDai

(由于最近不能贴二维码,所以委屈大家了...)

我会不定时的更新一些前端方面的知识内容以及自己的原创文章🎉

你的鼓励就是我持续创作的主要动力 😊.

相关推荐:

《全网最详bpmn.js教材》

《【建议改成】读完这篇你还不懂Babel我给你寄口罩》

《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》

《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》

《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》

《【何不三连】做完这48道题彻底弄懂JS继承(1.7w字含辛整理-返璞归真)》

《霖呆呆的近期面试128题汇总(含超详细答案) | 掘金技术征文》