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

程序源代码

共 9000字,需浏览 18分钟

 · 2020-08-12

前言

你盼世界,我盼望你无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,那么最终渲染出的就是

这样就保证了能把formConfig数组中的每一个表单项都渲染出来,但是这些表单项现在还是不受我们控制的,我们需要用前面学到的valueonChange和每个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 (
      <>
        
         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 (
       this.handleSubmit(e)}>
        
        
      
    )
  }
}

在输入框输入内容后,点击提交按钮,我们可以通过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 (
       this.handleSubmit(e)}>
         this.handleFile(e)} />
        
      
    )
  }
}

在选择了文件之后,我试图用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 (
       this.handleSubmit(e)}>
        
        
      
    )
  }
}

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

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

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

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

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

  • 《在实际业务中如何灵活运用受控组件与非受控组件》
    https://zhuanlan.zhihu.com/p/37579677
  • 《关于受控组件的思考》
    https://blog.csdn.net/neoveee/article/details/95873911

—————END—————



喜欢本文的朋友,欢迎关注公众号 达达前端,收看更多精彩内容



点个[在看],是对达达最大的支持!

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报