React: 事件处理、表单处理及受控组件与非受控组件

19924
2022/11/16 01:54:04

本章讨论 React 的事件处理,以及表单处理时涉及两个策略:受控组件与非受控组件。

说到事件,我们接触最多的莫过于诸如单击按钮、下列框的改变等。

事件处理

正如官方文档说的那样

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

下面通常一个简单的示例来开启事件处理的序幕:

  • 有两个组件(用的都是纯 HTML 元素):显示信息的 div,和改变状态的 button
  • 初始状态下,显示的信息是欢迎来到AXUM中文网
  • 当点击按钮时,显示的信息变为 AXUM中文网的网址是:https://axum.rs

编写初始页面

import React from 'react';

function App() {
  return (
    <div>
      <div style={{ color: 'red', fontSize: '2rem' }}>欢迎来到AXUM中文网</div>
      <input type="button" value="给我变" />
    </div>
  );
}

export default App;

  • 不知道你有没有忘记最重要的一条规范:有且只有一个根元素。因为我们实际是有两个元素,所以需要将它们包裹在一个父元素上
  • 还记得style该怎么写吗?
    • style={{key:value}}
    • 和组件一样,CSS 样式的名称也是遵循小驼峰式命名。
    • 让我们来看一下原生 HTML 的写法和 JSX 写法的对比:
      • 原生HTML:<div style="color:red; font-size: 2rem;">你好帅</div>
      • JSX:<div style={{ color: 'red', fontSize: '2rem' }}>你好帅</div>
  • 另一条规范是:所有元素必须闭合,比如第7行的input

加入状态

import React, { useState } from 'react';

function App() {
  const [msg, setMsg] = useState('欢迎来到AXUM中文网');
  return (
    <div>
      <div style={{ color: 'red', fontSize: '2rem' }}>{msg}</div>
      <input type="button" value="给我变" />
    </div>
  );
}

export default App;

通过 useState ,我们加入了 msg 状态和与之相关的 setMsg方法

给按钮绑定单击事件

import React, { useState } from 'react';

function App() {
  const [msg, setMsg] = useState('欢迎来到AXUM中文网');

  function btnClickHandler() {
    setMsg('AXUM中文网的网址是:https://axum.rs');
  }
  return (
    <div>
      <div style={{ color: 'red', fontSize: '2rem' }}>{msg}</div>
      <input type="button" value="给我变" onClick={btnClickHandler} />
    </div>
  );
}

export default App;

  • 第6行:定义一个函数,它用来处理按钮的单击事件。这个函数非常简单:直接将 msg 的值进行设置。
  • 第12行:onClick={btnClickHandler} 将按钮的单击事件绑定到第6行定义的函数

本节代码:axum-rs-handling-events

事件源

我们知道,在原生DOM中,可以通过事件处理函数的参数获取到事件源,幸运的是在 React 中也是一样。

更改事件处理函数的签名

import React, { useState } from 'react';

function App() {
  const [msg, setMsg] = useState('欢迎来到AXUM中文网');

  function btnClickHandler(e) {
    console.log(e.target);
    setMsg('AXUM中文网的网址是:https://axum.rs');
  }
  return (
    <div>
      <div style={{ color: 'red', fontSize: '2rem' }}>{msg}</div>
      <input type="button" value="给我变" onClick={btnClickHandler} />
    </div>
  );
}

export default App;

我们只需要简单的给事件处理函数加上一个形参,比如第6行的形参 e。通过该参数就能获取到事件源,比如第7行。

获取发起事件的元素的值

import React, { useState } from 'react';

function App() {
  const [msg, setMsg] = useState('欢迎来到AXUM中文网');

  function btnClickHandler(e) {
    console.log(e.target);
    alert(`按钮的文本是:${e.target.value}`);
    setMsg('AXUM中文网的网址是:https://axum.rs');
  }
  return (
    <div>
      <div style={{ color: 'red', fontSize: '2rem' }}>{msg}</div>
      <input type="button" value="给我变" onClick={btnClickHandler} />
    </div>
  );
}

export default App;

本节代码:axum-rs-handling-events-2

表单处理

和原生 DOM 类似,要处理表单的提交事件,我们可以给表单的 onSubmit 绑定事件,需要注意的是,为了避免原生 DOM 上的表单提交行为(比如刷新页面等),我们需要在事件处理函数中阻止它的默认的行为:

function submitHandler(e) {
  e.preventDefault();
}

React 有两种表单处理模式:

  • 如果是使用 state 与表单进行绑定,称之为受控模式。此时组件被称为受控组件
  • 如果不是使用 state 与表单进行绑定,称之为受控模式。此时组件被称为非受控组件
  • 你可以单一的使用以上两种模式的某一个;也可以将两种模式结合使用
  • 除非你明确知道原因,否则首选受控模式

下面两节,将通过案例来演示使用受控组件和非受控组件实现相同的预期结果。现在先对这个案例进行简要说明:

  • 表单组件:用于输入用户信息:姓名、年龄、地区;以及提交按钮
  • 信息组件:将输入的用户信息显示出来

受控组件

React 把那些使用 state 和表单进行绑定的组件称之为受控组件。

初始页面

import React from 'react';

function App() {
  return (
    <div>
      <form>
        <div>
          <label>
            姓名:
            <input type="text" placeholder="输入你的姓名" />
          </label>
        </div>
        <div>
          <label>
            年龄:
            <input type="number" placeholder="输入你的年龄" />
          </label>
        </div>

        <div>
          <label>
            地区:
            <select>
              <option value="">--请选择--</option>
              <option value="上海">上海</option>
              <option value="广州">广州</option>
            </select>
          </label>
        </div>
        <div>
          <button>提交</button>
        </div>
      </form>
    </div>
  );
}

export default App;

都是最基础的 HTML,这里不再说明。

状态与事件

在继续写代码之前,让我们来思考两个问题:应该要创建哪些状态,以及应该使用何种事件?

状态

创建状态有两个方案,一是为每个表单输入项创建一个对应的状态:

表单项状态
姓名name
年龄age
地区region

对应的JSX大概是这样的:

const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [region, setRegion] = useState('');

如果是有10个、20个,甚至100个表单项呢?

我觉得更合理的方案是,所有表单项由一个状态维护,我们可以把表单项作为这个状态的每个键值对来处理:

const [formData, setFormData] = useState({
  name: '',
  age: 0,
  region: '',
});

事件

选定了状态的方案,下面思考另一个问题:如何为每个表单项选择合适的事件呢?我们再往回想,我们给每个表单项选择事件的原因是什么——因为我们使用的是受控组件,每个表单项的值都与对应的状态相关联,所以在表单值发生改变时,必须反馈给对应的状态;相反,某个状态发生改变时,也要反馈给对应的表单项。所以,我们的目标是找出某个事件,能在表单项的值发生改变时,及时触发,然后就可以在该事件触发时,更新状态。

看似最常用的文本框,在选择合适的事件时反而显示有点没思路。让我们成下拉框来看。很明显,下拉框应该使用 onChange 事件:每当选择不同的项时,都会触发该事件。

回到文本框,它当然也能在修改值之后触发 onChange,更进一步,它还能监听键盘事件,比如按下某个键、松开某个键等等。对于本案例而言,onChange 就足够了。

元素/表单项事件
表单onSubmit
姓名onChange
年龄onChange
地区onChange
提交按钮全自动传递给表单,无需单独设置事件

实现受控组件

分析了一波之后,我们可以实现受控组件了:

定义状态、事件处理函数和绑定状态及事件处理

import React, { useState } from 'react';

function App() {
  // 定义状态
  const [formData, setFormData] = useState({
    name: '',
    age: 0,
    region: '',
  });

  // 事件处理
  function onNameChangeHandler(e) {}
  function onAgeChangeHandler(e) {}
  function onRegionChangeHandler(e) {}
  function onFormSubmitHandler(e) {
    e.preventDefault();
  }
  return (
    <div>
      <form onSubmit={onFormSubmitHandler}>
        <div>
          <label>
            姓名:
            <input
              type="text"
              placeholder="输入你的姓名"
              value={formData.name}
              onChange={onNameChangeHandler}
            />
          </label>
        </div>
        <div>
          <label>
            年龄:
            <input
              type="number"
              placeholder="输入你的年龄"
              value={formData.age}
              onChange={onAgeChangeHandler}
            />
          </label>
        </div>
        <div>
          <label>
            地区:
            <select value={formData.region} onChange={onRegionChangeHandler}>
              <option value="">--请选择--</option>
              <option value="上海">上海</option>
              <option value="广州">广州</option>
            </select>
          </label>
        </div>
        <div>
          <button>提交</button>
        </div>
      </form>
    </div>
  );
}

export default App;

  • 第5~9行,定义了维护表单数据的状态,每个表单项对应一个键值对
  • 第12~17行,定义了所需要的事件处理函数
  • 第20行,绑定了表单的onSubmit事件的处理函数
  • 第27~28行,设置了“姓名”和状态的关联关系,同时绑定了onChange事件的处理函数
  • 第38~39行,设置了“年龄”和状态的关联关系,同时绑定了onChange事件的处理函数
  • 第46行,设置了“地区”和状态的关联关系,同时绑定了onChange事件的处理函数

显示表单输入的内容

import React, { useState } from 'react';

function App() {
  // ...
  return (
    <div>
      <form onSubmit={onFormSubmitHandler}>
        {/* ... */}
      </form>
      <ul>
        <li>姓名:{formData.name}</li>
        <li>年龄:{formData.age}</li>
        <li>地区:{formData.region}</li>
      </ul>
    </div>
  );
}

export default App;

为了显示表单输入的内容,我们加了一个 ul,很简单的显示各个表单项的内容

实现表单项的事件处理函数

import React, { useState } from 'react';

function App() {
  // ...
  // 事件处理
  function onNameChangeHandler(e) {
    setFormData({ ...formData, name: e.target.value });
  }
  function onAgeChangeHandler(e) {
    setFormData({ ...formData, age: e.target.value * 1 });
  }
  function onRegionChangeHandler(e) {
    setFormData({ ...formData, region: e.target.value });
  }
  
  // ... 
}

export default App;

我们来看一下事件处理函数是如何实现的:

onNameChangeHandler 为例:

  • 为什么要用 {...formData},而不是直接赋值?如果你知道什么是深拷贝和浅拷贝,那么我要告诉你的是,这里是为了深拷贝。你可以看一下这个文档
  • 还记得 e.target.value 是什么含义吗?忘的话往上翻。

onAgeChangeHandler中,因为我们期望的年龄是数字,而 HTML 传递过来的总是字符串,所以我们需要将其转换为数字。这里用了小技巧:Javascript 的隐式转换,当一个字符串和一个数字相乘时,总是返回数字。

本节代码:axum-rs-react-controlled-component

非受控组件

在有些课程里,会把 refs 放在和 stateprops 相同的地位,并称为“React 三大对象”。我认为不尽然,refs的地位显然无法和stateprops 相提并论,没有 refs 大不了就用受控组件,依然能愉快地玩耍,反之如果没有 stateprops将寸步难行。

创建 refs 有几种方式,这里使用回调方式:

import React from 'react';

function App() {
  const els = { name: null, age: null, region: null };

  function onFormSubmitHandler(e) {
    e.preventDefault();
    const msg = `
    姓名:${els.name?.value}\n
    年龄:${els.age?.value * 1}\n
    地区:${els.region?.value}\n
    `;
    alert(msg);
  }
  return (
    <div>
      <form onSubmit={onFormSubmitHandler}>
        <div>
          <label>
            姓名:
            <input
              type="text"
              placeholder="输入你的姓名"
              ref={(el) => (els.name = el)}
            />
          </label>
        </div>
        <div>
          <label>
            年龄:
            <input
              type="number"
              placeholder="输入你的年龄"
              ref={(el) => (els.age = el)}
            />
          </label>
        </div>
        <div>
          <label>
            地区:
            <select ref={(el) => (els.region = el)}>
              <option value="">--请选择--</option>
              <option value="上海">上海</option>
              <option value="广州">广州</option>
            </select>
          </label>
        </div>
        <div>
          <button>提交</button>
        </div>
      </form>
    </div>
  );
}

export default App;

由于使用了非控组件,所以不想引入新的状态。而 React 只有的状态改变时才会重新渲染页面,这就导致目前无法让ul正常显示输入的内容,所以本例改了直接使用 alert 来显示。

你可以使用状态来关联 ref,这样就能在正确的时机重新渲染页面。但既然用了状态,为什么还要用非受控组件?

  • 第4行:const els = { name: null, age: null, region: null }; 这是一个普通变量,而不是状态,用来关联表单项
  • 第24、34、41行:ref={(el) => (els.name = el)}使用回调形式,将 ref 和普通变量相关联

本节代码:axum-rs-react-uncontrolled-component