📚 React 中的受控组件 vs 非受控组件:全面解析与最佳实践 🎛️

197 阅读4分钟

前言

在 React 开发中,表单处理是一个核心概念,而受控组件和非受控组件是处理表单数据的两种主要方式。本文将深入探讨这两种方法的区别、使用场景以及如何在实际项目中应用它们。

📜 目录

  1. 🔍 基本概念
  2. 🎮 受控组件详解
  3. 🕹️ 非受控组件详解
  4. ⚖️ 两者对比
  5. 🏆 最佳实践
  6. 🔚 总结

🔍 基本概念

什么是受控组件? 📝

受控组件是指表单元素的值由 React 的 state 控制,并通过 onChange 等事件处理器来更新 state 的组件。React 完全"控制"了这些组件的行为和值。

什么是非受控组件? 🎪

非受控组件则更像是传统的 HTML 表单元素,它们保持自己的内部状态,你可以使用 ref 来从 DOM 中获取表单值,而不是为每个状态更新编写事件处理程序。

// 🎯 简单示例对比
// 受控组件
<input value={this.state.value} onChange={this.handleChange} />

// 非受控组件
<input defaultValue="初始值" ref={this.inputRef} />

🎮 受控组件详解

工作原理 🔧

受控组件的工作流程:

  1. 组件渲染时从 React state 获取值
  2. 用户输入触发 onChange 事件
  3. 事件处理器更新 React state
  4. 组件使用新值重新渲染

代码示例 💻

class ControlledForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      username: '',  // 🏷️ 初始化state
      password: ''
    };
    
    // 🔗 绑定this
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    const { name, value } = event.target;
    this.setState({
      [name]: value  // 🎯 使用计算属性名动态更新state
    });
  }

  handleSubmit(event) {
    event.preventDefault();
    // 📤 提交时直接从state获取数据
    console.log('提交的数据:', this.state);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          用户名:
          <input
            type="text"
            name="username"
            value={this.state.username}  // 🔄 值绑定到state
            onChange={this.handleChange}  // 📢 监听变化
          />
        </label>
        <label>
          密码:
          <input
            type="password"
            name="password"
            value={this.state.password}
            onChange={this.handleChange}
          />
        </label>
        <button type="submit">提交</button>
      </form>
    );
  }
}

优点 ✅

  • 完全控制:可以实时验证、格式化或禁用提交按钮
  • 单一数据源:所有表单数据都存储在组件state中
  • 易于测试:状态完全由React控制,易于模拟和测试

缺点 ❌

  • 更多代码:需要为每个输入字段编写事件处理程序
  • 性能考虑:每次击键都会导致重新渲染,对于大型表单可能有性能问题

🕹️ 非受控组件详解

工作原理 🛠️

非受控组件的工作流程:

  1. 组件使用 defaultValue 或 defaultChecked 设置初始值
  2. 用户输入直接更新DOM
  3. 在需要时(如表单提交)通过ref获取DOM值

代码示例 🖥️

class UncontrolledForm extends React.Component {
  constructor(props) {
    super(props);
    // 🎣 创建ref来访问DOM元素
    this.usernameRef = React.createRef();
    this.passwordRef = React.createRef();
    
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(event) {
    event.preventDefault();
    // 🎯 通过ref.current.value获取输入值
    console.log('提交的数据:', {
      username: this.usernameRef.current.value,
      password: this.passwordRef.current.value
    });
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          用户名:
          <input
            type="text"
            defaultValue=""  // 🏁 设置初始值
            ref={this.usernameRef}  // 🔗 关联ref
          />
        </label>
        <label>
          密码:
          <input
            type="password"
            defaultValue=""
            ref={this.passwordRef}
          />
        </label>
        <button type="submit">提交</button>
      </form>
    );
  }
}

文件输入的特殊情况 📁

文件输入始终是非受控组件,因为它的值只能由用户设置,不能通过编程设置。

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.fileInput = React.createRef();
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleSubmit(event) {
    event.preventDefault();
    // 📂 访问文件列表
    alert(`选择的文件: ${this.fileInput.current.files[0].name}`);
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          上传文件:
          <input type="file" ref={this.fileInput} />
        </label>
        <button type="submit">提交</button>
      </form>
    );
  }
}

优点 ✅

  • 更简单:代码量少,不需要为每个输入编写事件处理程序
  • 性能更好:不会因每次击键而重新渲染
  • 更接近原生HTML:易于与第三方库集成

缺点 ❌

  • 即时验证困难:无法在用户输入时实时验证
  • 状态管理不便:表单数据分散在DOM中
  • 测试更复杂:需要模拟DOM操作

⚖️ 两者对比

特性受控组件 🎮非受控组件 🕹️
数据流双向绑定单向流动
状态存储React stateDOM节点
值获取从state读取通过ref读取
事件处理需要onChange不需要
即时验证容易实现难以实现
代码量较多较少
性能可能较差更好
文件输入不支持唯一选择

🏆 最佳实践

何时使用受控组件? 🤔

  • 需要实时验证或格式化输入
  • 根据输入值有条件地禁用提交按钮
  • 强制特定输入格式
  • 多个输入相互依赖
// 🌟 实时验证示例
class ValidatedInput extends React.Component {
  state = { email: '', isValid: false };

  validateEmail = (email) => {
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return re.test(email);
  };

  handleChange = (e) => {
    const email = e.target.value;
    this.setState({
      email,
      isValid: this.validateEmail(email)
    });
  };

  render() {
    return (
      <div>
        <input
          type="email"
          value={this.state.email}
          onChange={this.handleChange}
          style={{ borderColor: this.state.isValid ? 'green' : 'red' }}
        />
        {!this.state.isValid && this.state.email && (
          <p style={{ color: 'red' }}>请输入有效的邮箱地址</p>
        )}
      </div>
    );
  }
}

何时使用非受控组件? 🤔

  • 简单表单,不需要即时验证
  • 需要集成第三方库(如jQuery插件)
  • 大型表单,性能是关键考虑因素
  • 文件上传
// 🌟 与第三方库集成示例
class ThirdPartyIntegration extends React.Component {
  constructor(props) {
    super(props);
    this.datePickerRef = React.createRef();
  }

  componentDidMount() {
    // 🎨 初始化第三方日期选择器
    $(this.datePickerRef.current).datepicker({
      dateFormat: 'yy-mm-dd'
    });
  }

  componentWillUnmount() {
    // 🧹 清理
    $(this.datePickerRef.current).datepicker('destroy');
  }

  render() {
    return <input type="text" ref={this.datePickerRef} />;
  }
}

混合使用策略 🎛️

在实际项目中,你可以根据需求混合使用两种方法:

class HybridForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = { controlledValue: '' };
    this.uncontrolledRef = React.createRef();
  }

  handleControlledChange = (e) => {
    this.setState({ controlledValue: e.target.value });
  };

  handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      controlled: this.state.controlledValue,
      uncontrolled: this.uncontrolledRef.current.value
    });
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {/* 受控输入 */}
        <input
          value={this.state.controlledValue}
          onChange={this.handleControlledChange}
        />
        
        {/* 非受控输入 */}
        <input defaultValue="" ref={this.uncontrolledRef} />
        
        <button type="submit">提交</button>
      </form>
    );
  }
}

🔚 总结

受控组件非受控组件各有优势和适用场景:

  • 选择受控组件当:你需要对表单数据有完全控制,需要实时验证,或者表单逻辑复杂时。

  • 选择非受控组件当:你追求简单性、性能优化,或者需要与第三方库集成时。

现代 React 开发中,受控组件是更常见的选择,特别是在使用函数组件和 Hooks 的情况下:

// 🆕 使用Hooks的函数组件示例
function ControlledFormWithHooks() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ username, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={username}
        onChange={(e) => setUsername(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">登录</button>
    </form>
  );
}

记住,没有绝对的"正确"选择,只有最适合你特定场景的选择。理解两者的差异和适用场景将帮助你做出更好的架构决策! 🎯

希望这篇博客能帮助你更好地理解 React 中的受控和非受控组件! � 如有任何问题,欢迎在评论区讨论! 💬