React 基础-组件-类组件

01. 组件概述

目标:了解 React 组件的作用和创建组件的方式


  • 什么是组件
  • 组件设计思想

1. 什么是组件

在前端开发中组件就是用户界面当中的一块独立的区域,在组件内部会包含这块区域中的视图代码、样式代码以及逻辑代码。

React 采用组件的方式构建用户界面,通过将多个组件进行组合形成完成的用户界面,就像搭积木。

2. 组件设计思想

组件的核心思想之一就是复用,定义一次到处使用。

组件可以用来封装用户界面中的重复区块,复用重复区块。

组件的另外一个核心思想是解耦。

在传统的 web 页面开发中,一个 html 文件就是一个页面,就是说当前页面中的所有代码都被写在了同一个文件中,这就很容器导致代码的冲突。

在组件化开发中,每个组件都有自己的作用域,组件与组件之间的代码不会发生冲突,从而避免在传统开发模式中经常出现的改A坏B的问题。

02. 创建组件

目标:掌握创建类组件的方式

import ReactDOM from "react-dom/client";
import { Component } from "react";

// 1. 组件名称首字母大写
// 2. 只有继承了 Component 类才是 React 组件
// 3. 类中必须包含 render 方法用于渲染用户界面, render 方法的名字是固定的, 渲染用户界面时返回 jsx,不渲染任何界面时返回 null,
class App extends Component {
  render() {
    return <div>头部组件</div>;
  }
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

在实际的 React 项目开发中,组件作为独立的个体一般都会被放置在单独的文件中方便维护。

// src/App.js
import { Component } from "react";

// 约定: 组件文件的名称和组件名称保持一致
class App extends Component {
  render() {
    return <div>头部组件</div>;
  }
}
// 导出组件, 以便在其他地方导入并使用组件
export default App;
// src/index.js
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

03. 组件状态

1. 什么是组件状态

目标:掌握什么是组件状态

在现实生活中状态就是指同一事物的不同形态。比如水,当水达到沸点以后就变成了水蒸汽,当水在零摄氏度以下时就会结成冰。

web 应用中的用户界面也是有状态的,比如有一块将要展示用户列表的区域,用户列表数据需要从服务端获取。该区域将会有以下几种状态。

状态解释
空闲在没有发出请求时该区域为空闲状态
加载中请求在发出后没有得到响应前该区域为加载中状态
加载成功当请求得到响应用户列表渲染成功后该区域为成功状态
加载失败当请求未得到正确的响应时该区域为失败状态
结束不论请求成功与失败请求都结束了该区域为结束状态

再比如导航链接,它具有默认状态和选中状态。下拉框,它具有收缩状态和展开状态。

那么如何在程序中表示用户界面(UI)的状态呢?在程序中可以通过声明变量进行状态的记录。

// idle: 空闲状态
// loading: 加载中
// success: 加载成功
// error: 加载失败
// finish: 结束
let status = "idle";
let navLink = "白色";
let activeNavLink = "绿色"

React 应用程序使用组件构建用户界面,所以用户界面的状态需要被声明在组件中,被声明在组件中的状态被叫做组件状态。

2. 操作组件状态

目标:掌握操作组件状态的方法

在类组件中组件状态必须声明在类的 state 属性中,state 属性的名字是固定的,对象类型。

在 render 方法中通过 this 关键获取 state 属性中的状态。

// 目标: 实现计数器案例, 即声明组件状态 count 用于存储数值, 点击按钮让数值 + 1
class App extends Component {
  // state 对象用于存储组件状态
  state = {
    // 状态 count, 初始值为 0
    count: 0,
  };
  render() {
    // render 方法中的 this 指向组件的实例对象
    // state 属性是类的实例属性
    // 所以通过 this 是可以获取到 state 对象的
    return <button>{this.state.count}</button>;
  }
}

类组件中的组件状态必须通过类实例对象下的 setState 方法进行修改,只有这样才能触发视图更新。

当前组件实例下并没有 setState 方法,setState 方法是父类 Component 提供的。

class App extends Component {
  state = {
    count: 0,
  };
  render() {
    return (
      <button
        onClick={() => {
          // 1. 更改状态, 接收对象作为参数, 改哪一个状态就传递哪一个状态即可. react 内部会帮助我们进行状态合并操作
          // 2. 更新视图
          this.setState({ count: this.state.count + 1 });
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

疑惑: 在之前的课程中,我们说事件处理函数中的 this 指向 undefined,为什么此处它又指向了组件实例对象呢?

因为此处事件处理函数是箭头函数,箭头函数不绑定 this,箭头函数中的 this 指向了箭头函数定义处的 this,由于当前的事件处理函数是在 render 方法中定义的,而 render 方法中的 this 指向了组件实例对象,所以该事件处理函数中的 this 指向了组件实例对象。

注意:在 state 对象存储多个状态的情况下,使用 setState 更新状态时只传递需要更新的状态即可,React 会先接收我们传递给 setState 方法的状态,再使用它和原有状态进行合并,从而产生新的状态。

import { Component } from "react";

export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: "张三"
    }
  }
  render() {
    return <div>
      <button onClick={() => this.setState({count: this.state.count + 1})}>{this.state.count}</button>
      <span>{this.state.name}</span>
    </div>;
  }
}

3. 调试工具

目标:安装 React 调试工具、掌握使用调试工具查看组件状态的方式

https://crxdl.com/

4. 完善选项卡案例切换功能

class Tab extends Component {
  state = {
    current: 0,
  };
  render() {
    return <h4 onClick={() => this.setState({ current: index })}>选项卡标题</h4>;
  }
}

5. 更改事件函数 this 指向

目标:掌握 React 中事件处理函数 this 关键字的指向如何更改

在大多数情况下,我们都希望事件处理函数中的 this 关键字指向组件实例对象。

class App extends Component {
  onClickHandler() {
    console.log(this);
  }
  render() {
    // 使用 render 函数中的 this 关键字调用真正的事件处理函数, 使其内部指向当前组件实例对象
    // 小问题: 组件状态发生更改后要更新视图, 也就是说 render 方法会重新执行, 当每次 render 方法重新执行时 JavaScript 执行引擎都会创建新的行内匿名箭头函数, 都会为元素绑定新的行内匿名箭头函数, 性能有一丢丢损失.
    return <button onClick={() => this.onClickHandler()}>button</button>;
  }
}
class App extends Component {
  // 将事件处理函数改写成箭头函数
  // 问题: 事件处理函数从原型方法变成了实例方法, 如果当前组件会被调用很多次, 就会产生很多个相同的事件处理函数, 浪费内存空间.
  onClickHandler = () => {
    console.log(this);
  };
  render() {
    return <button onClick={this.onClickHandler}>button</button>;
  }
}

// 为什么改写成箭头函数以后可以解决 this 问题?
// 简写语法
class Person {
  fn = () => {}
}

class Person {
  constructor () {
    // 由于箭头函数不绑定 this, 所以 fn 函数在被调用后, 函数内部的 this 实际上用的是 constructor 构建函数中的 this
    // 而构造函数中的 this 指向了组件实例对象, 所以 fn 函数中的 this 就指向了实例对象
    this.fn = () => {}
  }
}
class App extends Component {
  onClickHandler() {
    console.log(this);
  }
  render() {
    // 通过 bind 方法将事件处理函数中的 this 指向组件实例对象, bind 方法返回一个新的被更改了 this 指向的函数作为事件处理函数
    // 问题: render 方法每次重新执行都会为元素绑定新的有bind 方法返回的事件处理函数, 性能有一丢丢损失
    return <button onClick={this.onClickHandler.bind(this)}>button</button>;
  }
}
class App extends Component {
  constructor() {
    super();
    // 在构造函数中将事件处理函数更改为组件对象
    // 构造函数中的 this 指向的是组件的实例对象
    // 这样即保证了事件处理函数为原型方法, 又确保了 this 指向的更改只执行一次
    // 所以从性能角度考虑, 这种方式是最为理想的
    // 推荐: ⭐️⭐️⭐️⭐️⭐️
    this.onClickHandler = this.onClickHandler.bind(this);
  }
  onClickHandler() {
    console.log(this);
  }
  render() {
    return <button onClick={this.onClickHandler}>button</button>;
  }
}

6. 状态不可变

目标:理解 React 中状态不可变理念

在 React 中关于状态有一核心理念,就是状态不可变,该理念需要被严格遵守。

状态不可变是指在更新组件状态时不能直接操作现有状态,而是要基于现有状态值产生新的状态值。

state = {
  count: 0,
  list: [1, 2, 3],
  person: {
    name: "张三",
    age: 18,
  },
};
// 错误写法
this.state.count = 100;
this.state.count++; // this.state.count = this.state.count + 1
this.state.list.push(4); // push pop shift unshift splice sort reverse
this.state.person.name = "李四";
// 正确写法
this.setState({
  count: this.state.count + 1,
  list: [...this.state.list, 4],
  person: {
    ...this.state.person,
    age: 30,
  },
});
this.setState({
  // 数组的删除写法
  list: [...this.state.list.slice(0, 1), ...this.state.list.slice(2)],
});

this.setState({
  // 数组的修改写法
  list: [...this.state.list.slice(0, 1), 4, ...this.state.list.slice(2)],
});

如何理解 React 状态不可变?

因为 React 在更新真实 DOM 对象之前,要对新旧状态对象 state 进行对比找出要更新的部分,所以当前状态也就是旧状态是不能被直接更改的。

04. 非受控表单组件

目标:掌握非受控表单的使用方式

在 HTML 中表单控件可以维护自身的状态。

用户在表单控件中输入的值就是表单控件要维护的状态,表单控件会实时将状态存储到表单控件对应的 DOM 对象的 value 属性中。

非受控表单组件是指表单组件的状态由自身维护。

在非受控组件中开发者要获取表单控件需要先获取表单控件 DOM 对象,再通过 value 属性获取表单控件状态。

import { Component, createRef } from "react";

class App extends Component {
  constructor() {
    super();
    // 设置事件处理函数中的 this 关键字指向组件实例对象
    this.onClickHandler = this.onClickHandler.bind(this);
  }
  // createRef: 创建元素的引用对象
  inputRef = createRef();
  // 按钮点击事件的事件处理函数
  onClickHandler() {
    // 通过文本框的元素引用对象获取文本框状态
    console.log(this.inputRef.current.value);
  }
  render() {
    return (
      <>
        {/* 为元素引用对象绑定元素 */}
        <input type="text" ref={this.inputRef} />
        {/* 点击按钮时获取文本框控件自身管理的状态 */}
        <button onClick={this.onClickHandler}>button</button>
      </>
    );
  }
}

05. 受控表单组件

目标:掌握受控组件的使用方式、理解受控组件的执行过程

1. 受控表单组件使用方式

受控表单组件是指表单的状态由组件状态管理,就是将表单的状态和组件状态进行映射。

通过表单控件的 value 属性绑定组件状态,通过 onChange 事件调用 setState 更新组件状态。

class App extends Component {
  state = {
    text: "默认值",
  };
  onChangeHandler(event) {
    // 调用 setState 方法更新组件状态
    // 将组件状态的值更新为用户在文本框中输入的内容
    this.setState({
      text: event.target.value,
    });
  }
  render() {
    return (
      <input
        type="text"
        {/* 将组件状态和文本框的 value 属性进行绑定 */}
        value={this.state.text}
        {/* 为文本框绑定 change 事件, 当用户在文本框中输入时更新组件状态 */}
        onChange={this.onChangeHandler}
      />
    );
  }
}

注意:在只为表单控件添加 value 属性而没添加 onChange 事件的情况下,浏览器控制台会报警告。

<input type="text" value={this.state.text} />

你为表单控件供了 value 属性但是没有提供 onChange 事件的事件处理函数,这将渲染一个只读的表单控件。如果你只是想为文本框设置一个默认值,你应该使用 defaultValue,否则,你要么添加 onChange 事件的事件处理函数要么添加 readOnly 属性。

(1) 添加只读属性是为了告诉 react 你就是要渲染一个只读的表单控件。警告消失。

(2) 添加 onChange 事件处理函数是为了完成组件状态与表单控件的映射功能。警告消失。

(3) 在使用非受控表单的情况下如果要为表单控制设置默认值可以使用 defaultValue 属性。警告消失。

总结:在受控组件中,表单控件身上必须同时具有 value 属性和 onChange 事件。

2. 受控表单组件执行过程

(1) 用户触发表单控件的 onChange 事件,执行 onChange 事件的事件处理函数

(2) 在事件处理函数中通过事件对象获取到表单控件的最新状态并使用最新状态更新组件状态

(3) 组件状态更新完成后 render 方法重新执行,在 render 方法中通过最新的组件状态渲染视图

3. onChange 事件说明

在 React 中使用的 onChange 事件并不是原生 JS 中的 onChange 事件,原生 JS 中的 onChange 事件是在表单离开焦点后触发的,而 React 中的 onChange 事件是实时触发的。

在原生 JS 中,表单控件的实时改变事件是 input,但并不是所有的表单控件都有该事件,比如 select。所以 React 为了方便开发者实现受控组件,重新封装了 onChange 事件,让 onChange 事件变成实时触发事件且其他表单控件也可以使用 onChange 事件。

06. 综合案例-评论

1. 准备案例布局和样式

目标:创建评论组件并拷贝案例布局和样式

<!-- public/index.html -->
<!-- 收藏按钮字体图标 -->
<link href="https://at.alicdn.com/t/font_2998849_vtlo0vj7ryi.css" rel="stylesheet" />
// src/Comment.js
import React from "react";
import "./comment.css";

class Comment extends React.Component {
  render() {
    return (
      <div className="comments">
        <h3 className="comm-head">评论</h3>
        <div className="comm-input">
          <textarea placeholder="爱发评论的人,运气都很棒"></textarea>
          <div className="foot">
            <div className="word">0/100</div>
            <div className="btn">发表评论</div>
          </div>
        </div>
        <h3 className="comm-head">
          热门评论<sub>(5)</sub>
          <span className="active">默认</span>
          <span>时间</span>
        </h3>
        <ul className="comm-list">
          <li className="comm-item">
            <div className="avatar"></div>
            <div className="info">
              <p className="name vip">
                清风徐来
                <img
                  alt=""
                  src="https://gw.alicdn.com/tfs/TB1c5JFbGSs3KVjSZPiXXcsiVXa-48-48.png"
                />
              </p>
              <p className="time">
                2012-12-12
                {/* 未收藏: icon-collect 已收藏: icon-collect-sel */}
                <span className="iconfont icon-collect"></span>
                <span className="del">删除</span>
              </p>
              <p>
                这里是评论的内容!!!这里是评论的内容!!!这里是评论的内容!!!
              </p>
            </div>
          </li>
        </ul>
      </div>
    );
  }
}

export default Comment;
// src/index.js
import ReactDOM from "react-dom/client";
import Comment from "./Comment";

let root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Comment />);

2. 渲染评论列表

目标:完成现有评论列表数据的渲染

npm i dateformat classnames
// src/Comment.js
import dateFormat from "dateformat";
import classNames from "classnames";

class Comment extends Component {
  state = {
    // 当前用户
    user: {
      name: "清风徐来",
      vip: true,
      avatar: "https://static.youku.com/lvip/img/avatar/310/6.png",
    },
    // 评论列表
    comments: [
      {
        id: 100,
        name: "__RichMan",
        avatar: "https://r1.ykimg.com/051000005BB36AF28B6EE4050F0E3BA6",
        content:
          "这阵容我喜欢😍靳东&闫妮,就这俩名字,我就知道是良心剧集...锁了🔒",
        time: new Date("2022-10-12T10:10:23"),
        vip: true,
        collect: false,
      },
      {
        id: 101,
        name: "糖蜜甜筒颖",
        avatar:
          "https://image.9xsecndns.cn/image/uicon/712b2bbec5b58d6066aff202c9402abc3370674052733b.jpg",
        content:
          "突围神仙阵容 人民的名义第三部来了 靳东陈晓闫妮秦岚等众多优秀演员实力派 守护人民的财产 再现国家企业发展历程",
        time: new Date("2022-09-23T15:12:44"),
        vip: false,
        collect: true,
      },
      {
        id: 102,
        name: "清风徐来",
        avatar: "https://static.youku.com/lvip/img/avatar/310/6.png",
        content:
          "第一集看的有点费力,投入不了,闫妮不太适合啊,职场的人哪有那么多表情,一点职场的感觉都没有",
        time: new Date("2022-07-01T00:30:51"),
        vip: true,
        collect: false,
      },
    ],
  };
  render() {
    return (
      <div className="comments">
        <h3 className="comm-head">评论</h3>
        <div className="comm-input">
          <textarea placeholder="爱发评论的人,运气都很棒"></textarea>
          <div className="foot">
            <div className="word">0/100</div>
            <div className="btn">发表评论</div>
          </div>
        </div>
        <h3 className="comm-head">
          热门评论<sub>({this.state.comments.length})</sub>
        </h3>
        <ul className="comm-list">
          {this.state.comments.map((item) => (
            <li key={item.id} className="comm-item">
              <div
                className="avatar"
                style={{ backgroundImage: `url(${item.avatar})` }}
              ></div>
              <div className="info">
                <p className={classNames(["name", { vip: item.vip }])}>
                  {item.name}
                  {item.vip ? (
                    <img
                      alt=""
                      src="https://gw.alicdn.com/tfs/TB1c5JFbGSs3KVjSZPiXXcsiVXa-48-48.png"
                    />
                  ) : null}
                </p>
                <p className="time">
                  {dateFormat(item.time, "yyyy-mm-dd")}
                  <span
                    className={classNames([
                      "iconfont",
                      {
                        "icon-collect-sel": item.collect,
                        "icon-collect": !item.collect,
                      },
                    ])}
                  ></span>
                  {item.name === this.state.user.name ? (
                    <span className="del">删除</span>
                  ) : null}
                </p>
                <p>{item.content}</p>
              </div>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

3. 发表评论

目标:完成发表评论功能,对用户输入的评论内容字数进行限制。

(1) 将文本域组件更改为受控组件

// src/Comment.js
class Comment extends Component {
  // 构造函数
  constructor() {
    super();
    // 更改事件处理函数的 this 指向
    this.updateContent = this.updateContent.bind(this);
  }
  
  // 组件状态
  state = {
    // 用户输入的评论内容
    content: "",
  };

  // 同步用户在文本域中输入的状态
  updateContent(event) {
    this.setState({ content: event.target.value});
  }

  render() {
    // 将 value 属性和组件状态 content 进行绑定
    // 添加 onChange 事件用于更新组件状态
    return <textarea value={this.state.content} onChange={this.updateContent}></textarea>;
  }
}

(2) 展示用户输入的内容的数量并对数量进行限制

// src/Comment.js
class Comment extends Component {
  // 同步用户在文本域中输入的状态
  updateContent(event) {
    // 获取用户在文本域中输入的内容
    const value = event.target.value;
    // 如果内容长度大于100, 阻止程序继承执行
    if (value.length > 100) return;
    // 内容符合长度要求, 设置组件状态
    this.setState({ content: value });
  }

  render() {
    return <div className="word">{this.state.content.length}/100</div>
  }
}

(3) 实现发表评论、清空文本域

import { Component } from "react";
import dateFormat from "dateformat";
import "./comment.css";

class Comment extends Component {
  constructor() {
    // 将事件处理函数中的 this 指向组件实例
    this.publishComment = this.publishComment.bind(this);
  }
  
  // 发表评论
  publishComment() {
    // 如果用户没有输入评论内容, 阻止程序继续执行
    if (this.state.content.length === 0) return;
    // 更新组件状态
    this.setState({
      // 更新评论列表
      comments: [
        {
          id: Math.random(),
          content: this.state.content,
          ...this.state.user,
          collect: false,
          time: new Date().toDateString(),
        },
        ...this.state.comments,
      ],
      // 更新用户在文本域中输入的内容
      content: "",
    });
  }

  render() {
    return <div onClick={this.publishComment}>发表评论</div>;
  }
}

4. 删除评论

目标:实现删除评论功能


class Comment extends Component {
  constructor() {
    // 将事件处理函数中的 this 指向组件实例
    this.deleteComment = this.deleteComment.bind(this);
  }

  // 删除评论
  deleteComment(id) {
    this.setState({
      comments: this.state.comments.filter((item) => item.id !== id),
    });
  }

  render() {
    return <span onClick={() => this.deleteComment(item.id)}>删除</span>;
  }
}

5. 收藏评论

目标:实现收藏评论功能

import { Component } from "react";
import dateFormat from "dateformat";
import "./comment.css";

class Comment extends Component {
  constructor() {
    // 将事件处理函数中的 this 指向组件实例
    this.collectComment = this.collectComment.bind(this);
  }

  // 收藏评论
  collectComment(id) {
    this.setState({
      comments: this.state.comments.map((item) =>
        item.id === id ? { ...item, collect: !item.collect } : item
      ),
    });
  }

  render() {
    return <span onClick={() => this.collectComment(item.id)}></span>;
  }
}

6. 评论排序

目标:实现评论列表排序功能

npm i lodash
import orderBy from "lodash/orderBy";

class Comment extends React.Component {
  state = {
    // 排序: 按照 id 和 time 进行排序
    sortField: "id",
  };
  render() {
    return (
      <>
        <span
          className={classNames({ active: this.state.sortField === "id" })}
          onClick={() =>
            this.setState({
              sortField: "id",
              comments: orderBy(this.state.comments, ["id"], ["asc"]),
            })
          }
        >
          默认
        </span>
        <span
          className={classNames({ active: this.state.sortField === "time" })}
          onClick={() =>
            this.setState({
              sortField: "time",
              comments: orderBy(this.state.comments, ["time"], ["asc"]),
            })
          }
        >
          时间
        </span>
      </>
    );
  }
}