React 类组件你不知道的细节+案例

1,205 阅读15分钟

React基础-组件-类组件

1.组件概述

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


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

1.what is 组件啊?

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

React是采用组件的方式来构建用户界面,通过将多个组件进行组合形成完成的用户界面,就好比搭乐高一样

2.组件设计思想

组件的核心思想之一就是重复使用用,定义一次就可以在任何地方进行使用

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

组件的第二个核心思想就是解耦。

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

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


2.创建组件

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

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></App>)

1.组件单独存放

在实际的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></App>)

3.组件状态

1.组件状态什么玩意?

用一个例子来比喻就是说现实生活中的状态指同一个事物有不同形式,比如水,当水达到沸点那么就会变成水蒸气,如果水在零摄氏度以下就会结成冰

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

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

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

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

//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 {
  constrouct() {
    super()
    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.更改事件函数 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>
  }
}
​
//为什么改写成箭头函数以后可以解决this问题
//简写语法
class Person {
  fn = () => {}
}
​
class Person {
  constrouct() {
     // 由于箭头函数不绑定 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>
  }
}
class App extends Component {
  constrouct() {
    super() {
      // 在构造函数中将事件处理函数更改为组件对象
      // 构造函数中的 this 指向的是组件的实例对象
      // 这样即保证了事件处理函数为原型方法, 又确保了 this 指向的更改只执行一次
      // 所以从性能角度考虑, 这种方式是最为理想的
      // 推荐: ⭐️⭐️⭐️⭐️⭐️
      this.onClickHandler = this.onClickHandler.bind(this);
    }
    onClickHandler() {
      console.log(this)
    }
    render() {
      return <button onClickHandler={this.onClickHandler}></button>
    }
    
  }
}

4.状态不可变

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

React中关于状态有一个核心的思想理念就是状态不可变,该理念是需要被时刻严格遵守

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

state = {
  count:0,
  list:[1,2,3],
  person: {
    name:"张三",
    age:18
  }
}
//🙅写法
this.state.count = 200;
this.state.count-- 
this.state.list.push
this.state.person.name = "王武"
//正确写法
this.setState({
  //数组的删除写法
  list:[...this.state.list.slice(0,1),"p",...this.state.list.slice(2)],
  
})

如何理解 React 状态不可变?

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


4.非受控表单组件

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

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

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

非受控表单组件是指表单组件的状态由自己进行维护

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

import {Component,createRef} from "react";
​
class App extends Component {
  constrouct() {
    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>
      </>
    )
  }
}

5.受控表单组件

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

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} />
​
<input type="text" defaultValue={this.state.text} />

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

  • 添加只读属性是为了告诉 react 你就是要渲染一个只读的表单控件。警告消失。
  • 添加 onChange 事件处理函数是为了完成组件状态与表单控件的映射功能。警告消失。
  • 在使用非受控表单的情况下如果要为表单控制设置默认值可以使用 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 事件。


6.综合案例-评论

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/comment.css */
body {
  margin: 0;
}
.comments {
  background-color: #121212;
  color: #eee;
  padding: 0 30px;
  width: 1000px;
  margin: 70px auto 0;
  overflow: hidden;
}
.comm-head {
  color: #eee;
  font-size: 24px;
  line-height: 24px;
  margin-bottom: 24px;
}
.comm-head sub {
  font-size: 14px;
  color: #666;
  margin-left: 6px;
  bottom: 0.2em;
  position: relative;
}
​
.comm-head span {
  display: inline-block;
  line-height: 1;
  padding: 5px 16px;
  font-size: 14px;
  font-weight: normal;
  border-radius: 12px;
  background-color: rgba(255, 255, 255, 0.1);
  color: #999;
  cursor: pointer;
  margin-left: 30px;
}
.comm-head span:hover,
.comm-head span.active {
  color: #61f6ff;
}
​
.comm-list {
  list-style: none;
  padding: 0;
}
.comm-item {
  display: flex;
  margin-bottom: 24px;
}
.comm-item .avatar {
  width: 48px;
  height: 48px;
  line-height: 48px;
  border-radius: 24px;
  display: inline-block;
  cursor: pointer;
  background-position: 50%;
  background-size: 100%;
  background-color: #eee;
}
.comm-item .info {
  padding-left: 16px;
}
.comm-item .info p {
  margin: 8px 0;
}
.comm-item .info p.name {
  color: #999;
}
.comm-item .info p.vip {
  color: #ebba73;
}
.comm-item .info p.vip img {
  width: 14px;
  vertical-align: baseline;
  margin-left: 5px;
}
.comm-item .info p.time {
  color: #666;
  font-size: 14px;
  display: flex;
  align-items: center;
}
​
.comm-item .info .iconfont {
  margin-left: 20px;
  position: relative;
  top: 1px;
  cursor: pointer;
}
.comm-item .info .iconfont.icon-collect-sel {
  color: #ff008c;
}
.comm-item .info .del {
  margin-left: 20px;
  cursor: pointer;
}
.comm-item .info .del:hover {
  color: #ccc;
}
​
.comm-input {
  border-radius: 6px;
  padding: 18px;
  background-color: #25252b;
}
.comm-input textarea {
  border: 0;
  outline: 0;
  resize: none;
  background: transparent;
  color: #999;
  width: 100%;
  font-family: inherit;
  height: auto;
  overflow: auto;
}
.comm-input .foot {
  display: flex;
  justify-content: flex-end;
  justify-items: center;
}
.comm-input .foot .word {
  line-height: 36px;
  margin-right: 10px;
  color: #999;
}
.comm-input .foot .btn {
  background-color: #ff008c;
  font-size: 14px;
  color: #fff;
  line-height: 36px;
  text-align: center;
  border-radius: 18px;
  padding: 0 24px;
  cursor: pointer;
  user-select: none;
}
// src/index.js
import ReactDOM from "react-dom/client";
import Comment from "./Comment";
​
let root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Comment />);

2.渲染评论列表

// src/Comment.js
import React from "react";
import "./comment.css";
import dateformat from "dateformat";
import classNames from "classnames";
​
export default class Comment extends React.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>
          <span className="active">默认</span>
          <span>时间</span>
        </h3>
        <ul className="comm-list">
          {this.state.comments.map((item, index) => (
            <li className="comm-item">
              <div
                className="avatar"
                style={{ backgroundImage: `url(${item.avatar})` }}
              ></div>
              <div className="info">
                <p className="name 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")}
                  {/* 未收藏: icon-collect 已收藏: icon-collect-sel */}
                  <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>
      </>
    );
  }
}