第4章:React组件基础——函数组件与类组件、JSX语法详解、组件的props与state、事件处理

好,咱们进入正题。React 的核心就是组件。你想想看,整个页面就是一堆组件拼起来的。这一章我会带你彻底搞懂组件的基础,包括怎么写、怎么用、怎么传数据、怎么处理交互。

4.1 函数组件与类组件

React 有两种定义组件的方式:函数组件和类组件。我个人习惯用函数组件,因为更简洁。但类组件你也要懂,老项目里到处都是。

4.1.1 函数组件

说白了就是一个 JavaScript 函数。接收 props,返回 JSX。

// 最简单的函数组件
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// 或者用箭头函数
const Welcome = (props) => {
  return <h1>Hello, {props.name}</h1>;
};

嗯,这里要注意:函数组件没有自己的实例,也没有生命周期。但在 React 16.8 之后,有了 Hooks,函数组件也能做类组件能做的事了。

4.1.2 类组件

类组件是用 ES6 class 定义的。必须继承 React.Component,而且必须有 render 方法。

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

我在项目中遇到过这样的情况:一个老项目全是类组件,后来新功能用函数组件写,结果团队成员在两种风格之间切换,容易搞混。我的建议是——统一风格。新项目直接用函数组件 + Hooks。

核心区别:

  • 函数组件:无状态(以前),简洁,容易测试
  • 类组件:有状态,有生命周期,适合复杂逻辑
  • 现在:函数组件 + Hooks 可以替代大部分类组件场景

4.2 JSX语法详解

JSX 是 JavaScript 的语法扩展。长得像 HTML,但其实是 JS。我第一次看到 JSX 时觉得怪怪的,但用了一天就真香了。

4.2.1 JSX 的基本规则

  • 必须有一个根元素:可以用 <div> 或者 <React.Fragment>(简写 <></>
  • 属性用驼峰命名className 代替 classonClick 代替 onclick
  • 花括号里写 JS 表达式{变量}{函数调用()}{条件 ? 'a' : 'b'}
  • 不能写 if/else 语句:但可以用三元表达式或者逻辑与
function Greeting({ isLoggedIn }) {
  return (
    <>
      <h1>{isLoggedIn ? '欢迎回来' : '请登录'}</h1>
      <p className="subtitle">今天是个好日子</p>
    </>
  );
}

我曾经踩过的坑:在 JSX 里直接写 if (condition) { ... },结果页面直接报错。记住,花括号里只能放表达式,不能放语句。

4.2.2 JSX 中的列表渲染

map() 方法。每个子元素必须有一个唯一的 key 属性。

const todos = ['学习React', '写项目', '复习'];

function TodoList() {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>{todo}</li>
      ))}
    </ul>
  );
}

小技巧:key 最好用数据的 id,不要用 index。为什么?因为列表顺序变了,React 会搞混。我吃过这个亏,列表删除一项后,后面的组件状态全乱了。

4.3 组件的 props 与 state

props 和 state 是组件的两个核心概念。说白了,props 是外部传进来的,state 是组件自己管的。

4.3.1 props:父组件给子组件的数据

props 是只读的。你不能在子组件里修改 props。这是 React 的核心理念——单向数据流。

// 父组件
function App() {
  return <User name="张三" age={25} />;
}

// 子组件
function User(props) {
  return (
    <div>
      <p>姓名:{props.name}</p>
      <p>年龄:{props.age}</p>
    </div>
  );
}

你想想看,如果子组件能随便改 props,那数据流就乱套了。所以 React 强制 props 只读。

4.3.2 state:组件内部的状态

state 是组件自己管理的数据。变了,组件就重新渲染。

类组件的 state:

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    return (
      <div>
        <p>计数:{this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          加1
        </button>
      </div>
    );
  }
}

函数组件的 state(用 useState):

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>
        加1
      </button>
    </div>
  );
}

重要提醒:

  • 类组件用 this.setState() 更新 state,是合并的
  • 函数组件用 setCount() 更新 state,是替换的
  • 永远不要直接修改 state:this.state.count = 1 是错的

4.4 事件处理

React 的事件处理跟原生 DOM 事件有点像,但有几个关键区别。

4.4.1 基本用法

  • 事件名用驼峰:onClickonChangeonSubmit
  • 传一个函数,不是字符串:onClick={handleClick},不是 onClick="handleClick()"
  • 要阻止默认行为,必须显式调用 e.preventDefault()
function Form() {
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单提交了');
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">提交</button>
    </form>
  );
}

4.4.2 事件中的 this 问题(类组件)

我记得刚学 React 时,在类组件里写事件处理,this 指向经常搞错。看这个例子:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOn: true };
    // 这行很重要!绑定 this
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isOn: !prevState.isOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isOn ? '开' : '关'}
      </button>
    );
  }
}

如果不绑定 this,handleClick 里的 this 就是 undefined。三种解决办法:

  1. 在 constructor 里 bind(如上)
  2. 用箭头函数定义方法:handleClick = () => { ... }
  3. 在 render 里用箭头函数:onClick={() => this.handleClick()}(不推荐,每次渲染都创建新函数)

我的建议:用函数组件 + Hooks 就没有 this 的问题了。这也是我推荐新项目用函数组件的原因之一。

4.4.3 向事件处理函数传参

有时候你需要传额外的参数。比如列表中的每一项都有一个删除按钮:

function TodoList() {
  const deleteItem = (id) => {
    console.log('删除', id);
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => deleteItem(todo.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}

这里用箭头函数包一层,就能把 todo.id 传进去。注意不要写成 onClick={deleteItem(todo.id)},那样会立即执行函数。

4.5 本章小结

这一章我们覆盖了 React 组件的基础知识。说白了,就是三件事:

  • 怎么写组件:函数组件还是类组件,我推荐函数组件
  • 怎么写 JSX:记住花括号里放表达式,属性用驼峰
  • 怎么管理数据:props 是外部传入的,state 是内部管理的
  • 怎么处理事件:注意 this 绑定,用箭头函数传参

下一章我们会深入组件的生命周期和 Hooks,到时候你会发现,函数组件真的香。