一、组件的概念
使用组件方式进行编程,可以提高开发效率,提高组件的复用性、提高代码的可维护性和可扩展性
React定义组件的方式有两种
-
类组件:React16.8版本之前几乎React使用都是类组件
-
函数组件:React16.8之后,函数式组件使用的越来越多
二、组件定义
1、ES6类的回顾
类组件:类组件是指使用ES6中class定义的组件称为类组件
回顾类的定义
class 类名{
}
类是由属性和方法组成
-
类中的属性:表示的事物的特征
-
类中方法:表示的是对象的行为
class 类名{
属性名1;
属性名2;
方法名1(){
}
方法名2(){
}
}
案例1:类的定义
/*
定义一个学生类
class 类名{
}
类名的规定
1、类名是由字母、数字、下划线或者$符号组成
2、名称不能以数字开头
3、类名之间不能由空格、不能是关键字或者保留字
4、不能是true,false,null
5、类名采用驼峰式命名法,每个单词的首字母要大写
6、要见名知意
*/
class Student{
sno; //学号
sname; //姓名
gender; //性别
education; //学历
major; //专业
introduce(){
return `学号:${this.sno}\n姓名:${this.sname}\n性别:${this.gender}\n学历:${this.education}\n专业:${this.major}`
}
}
/*
创建对象的语法
const/let 对象名=new 类名()
给对象赋值的语法
对象名.属性名=值
调用对象中的方法
对象名.方法名()
*/
const s1=new Student()
s1.sno="XAWNW1001"
s1.sname="张资源"
s1.gender="男"
s1.education="本科"
s1.major="土木工程"
let info=s1.introduce()
console.log(info);
console.log('*******************************');
const s2=new Student()
s2.sno="XAWNW1002"
s2.sname="李敏"
s2.gender="女"
s2.education="本科"
s2.major="英语"
console.log(s2.introduce());
案例2:构造方法的使用
class Student{
constructor(sno,sname,gender,education,major){
this.sno=sno
this.sname=sname
this.gender=gender
this.education=education
this.major=major
}
introduce(){
return `学号:${this.sno}\n姓名:${this.sname}\n性别:${this.gender}\n学历:${this.education}\n专业:${this.major}`
}
}
/*
实例化对象的同时进行初始化
let/const 对象名=new 类名(实参1,实参1,....,实参n)
*/
const s1=new Student('WNXAK1001','何新雨','男','专科','通讯工程')
console.log(s1.introduce());
console.log("*********************************************");
const s2=new Student('WNXAK1002','李乐','男','专科','英语')
console.log(s2.introduce());
案例3:类的继承
class Teacher{
constructor(name,school){
this.name=name
this.school=school
}
introduce(){
return `我是${this.school}的${this.name}`
}
giveLession(){
console.log('讲解本章目标');
console.log('讲解本章内容');
console.log('进行本章总结');
console.log('安排今日作业');
}
}
/**
* 定义一个子类
* 继承的语法
* class 子类的名称 extends 父类名称{
* }
* super关键字的使用
* suepr()直接调用父类的构造方法,它的位置必须放在子类构造方法的首行
* super.父类中的方法/父类中的属性
* 方法的重写:是在继承关系中,子类中的方法名和父类中的方法名,参数个数相同的这么一种情况,称为方法的重写
* 方法重写的结果就是子类中的内容完全覆盖父类中方法中的内容
*/
class WebTeacher extends Teacher{
constructor(name,school){
super(name,school)
}
giveLession(){
console.log('首先打开vscode开发环境');
super.giveLession()
}
}
class TestTeacher extends Teacher{
constructor(name,school){
super(name,school)
}
giveLession(){
console.log('打开postman或者vm虚拟机');
super.giveLession()
}
}
let zhaijizhe=new WebTeacher('吉','学苑')
console.log(zhaijizhe.introduce());
zhaijizhe.giveLession()
console.log('********************************');
let xuhaidong=new WebTeacher('东','学苑')
console.log(xuhaidong.introduce());
xuhaidong.giveLession()
console.log('**************************************');
const wangxiaofeng=new TestTeacher('峰','学苑')
console.log(wangxiaofeng.introduce())
wangxiaofeng.giveLession()
2、定义类组件
类组件:是指通过ES6类来定义的组件称为类组件,React中定义类组件有如下约定
-
类名首字母大写
-
类组件必须要继承React.Component父类,这个父类中的相关的方法和属性就能被继承下来
-
类组件中必须要有一个render方法
-
这个render必须要要有返回值,返回值的内容就是这个类组件的结构(jsx)
-
由于这个类组件要被别的组件引用,所以使用ES6的默认导出将其导出,便于别的组件引用
import React from "react"
export default class Hello extends React.Component{
render(){
return(
<>
<h1>Hello组件</h1>
</>)
}
}
类组件定义之后,引入这个类组件
import ReactDOM from 'react-dom/client'
import Hello from './components/Hello'
const template=(<>
<Hello></Hello>
</>)
const root=ReactDOM.createRoot(document.querySelector('#root'))
root.render(template)
3、定义函数组件【后面重点讲】
在React中除了定义类组件之外,也可以定义函数组件
函数组件:所谓函数组件是指通过普通函数或者箭头函数所定义出来的组件称为函数组件
函数组件有如下规定
-
函数名必须首字母大写
-
使用ES6的export将其默认导出,便于别的组件引用
-
函数必须要有一个返回值,这个返回的内容是JSX,返回的是该函数组件的结构
export default function HelloWorld(){
return (
<>
<h1>Hello World函数组件</h1>
</>)
}
注意:如果不返回任何内容,假设返回一个null,页面将没有任何内容
在实际开发过程中,由于这些类组件和函数组件它的结构都是固定的,所以可以使用一些插件将其生成出来
使用rcc
生成类组件,使用rfc
生成函数组件
三、React的事件处理
1、React的事件处理
在React中通过onXx属性来实现单击事件的绑定的,常见的写法有四种
-
直接在标签中通过
onClick={()=>{}}
来实现事件绑定 -
在标签中通过
onClick={this.类中的普通的成员方法}
的方式来进行绑定 -
在标签中通过
onClick={this.类中的箭头函数}
的方式绑定 -
在标签中通过
onClikc={()=>{this.函数名称()}}
import React, { Component } from 'react'
export default class Hello extends Component {
/*
不要使用这种方法
*/
handleClick(){
console.log('类中定义普通方法',this);
}
handleClick2=()=>{
console.log('类中定义的箭头函数',this);
}
render() {
return (
<>
<button onClick={()=>{
console.log('我是按钮1,我被点击了~~~~');
}}>按钮1</button>
<button onClick={this.handleClick}>按钮2</button>
<button onClick={this.handleClick2}>按钮3</button>
<button onClick={()=>{this.handleClick2()}}>按钮4</button>
</>
)
}
}
2、this指向的回顾
let teacher={
name:'teacher'
}
let student={
name:'student'
}
let person={
name:'person',
show(age,sex){
return `我叫${this.name},今年${age}岁,我的性别是${sex}`
}
}
console.log(person.show(38,'男'))
//改变this指向,改变this执行的方式有三个,第一个是call,call的作用调用函数,还可以改变this执行
console.log(person.show.call(teacher,48,'女'));
//通过apply的方式也可以调用函数,这种方式调用方法同时,改变this指向
console.log(person.show.apply(student,[22,'男']));
//通过bind的方式来改变this指向
let teaherShow=person.show.bind(teacher)
console.log(teaherShow(55,'男'));
如果使用第二方式来进行事件绑定的时候,会存在this执行为空的情况,解决办法如下
-
使用箭头函数写法代替普通方法(建议)
-
通过bind方式来改变this执行(不建议)
this执行改变的代码可以写在多个位置,比如写在构造函数中(经典的写法)
export default class Hello extends Component {
constructor(){
super()
//改变this执行
this.handleClick=this.handleClick.bind(this)
}
}
也可以在调用的同时去改变this指向(不建议)
<button onClick={this.handleClick.bind(this)}>按钮2</button>
3、事件传参
import React, { Component } from 'react'
/*
React的事件传值的形式有如下三种
1、进行事件传值的时候,没有实参,默认形参接收的event对象
2、进行事件调用的同时,传递额外的参数
*/
export default class Hello extends Component {
handleClick=(e)=>{
console.log('e',e);
}
handleClick2=(arg1,arg2,arg3)=>{
console.log('参数1:',arg1);
console.log('参数2:',arg2);
console.log('参数3:',arg3);
}
handleClick3=(arg1,arg2,arg3,arg4)=>{
console.log('参数1:',arg1);
console.log('参数2:',arg2);
console.log('参数3:',arg3);
console.log('参数4:',arg4);
}
render() {
return (
<>
<button onClick={this.handleClick}>按钮1</button>
<button onClick={()=>{this.handleClick2(22,33,56)}}>按钮2</button>
<button onClick={(e)=>{this.handleClick3(89,e,99,78)}}>按钮3</button>
</>
)
}
}
4、合成事件的原理
4.1、底层实现原理
<div onClick={this.handleClick.bind(this)}>点我</div>
React并不是将click事件绑定到了div的真实DOM上
-
在React16版本中,是在document处 监听了所有的事件,当事件发⽣并且冒泡到document处的时候,React将事 件内容封装并交由真正的处理函数运⾏。
-
在React17版本中,是在根容器(
#root
)处 监听了所有的事件,当事件发⽣并且冒泡到根容器处的时候,React将事 件内容封装并交由真正的处理函数运⾏。
这样的⽅式不仅仅减少了内存的消 耗,还能在组件挂载销毁时统⼀订阅和移除事件。 除此之外,冒泡到document上的事件也不是原⽣的浏览器事件,⽽是由 react⾃⼰实现的合成事件(SyntheticEvent)。
实现合成事件的⽬的如下:
-
合成事件⾸先抹平了浏览器之间的兼容问题,另外这是⼀个跨浏览器原⽣ 事件包装器,赋予了跨浏览器开发的能⼒;
-
对于原⽣浏览器事件来说,浏览器会给监听器创建⼀个事件对象。如果你 有很多的事件监听,那么就需要分配很多的事件对象,造成⾼额的内存分 配问题。但是对于合成事件来说,有⼀个事件池专⻔来管理它们的创建和 销毁,当事件需要被使⽤时,就会从池⼦中复⽤对象,事件回调结束后, 就会销毁事件对象上的属性,从⽽便于下次复⽤事件对象。
4.2、 React的事件和普通的HTML事件有什么不同
区别:
-
对于事件名称命名⽅式,原⽣事件为全⼩写,react 事件采⽤⼩驼峰;
-
对于事件函数处理语法,原⽣事件为字符串,react 事件为函数;
-
react 事件不能采⽤ return false 的⽅式来阻⽌浏览器的默认⾏为,⽽必须要地明确地调⽤preventDefault()来阻⽌默认⾏为。
合成事件是 react 模拟原⽣ DOM 事件所有能⼒的⼀个事件对象,其优点如 下:
-
兼容所有浏览器,更好的跨平台;
-
将事件统⼀存放在⼀个数组,避免频繁的新增与删除(垃圾回收)。
-
⽅便 react 统⼀管理和事务机制。
事件的执⾏顺序为原⽣事件先执⾏,合成事件后执⾏,合成事件会冒泡绑定到 document 上,所以尽量避免原⽣事件与合成事件混⽤,如果原⽣事件阻⽌冒 泡,可能会导致合成事件不执⾏,因为需要冒泡到document 上合成事件才会 执⾏。
四、类组件的state
vue框架和React框架最大的一个好处就是不需要开发人员去操作DOM,只要大家操作了数据,自动DOM元素会发生变化,这种操作称为响应式d
在vue中响应式数据主要来自两个部分
-
组件内部的响应式数据是定义在data选项
-
来子组件外部的是通过props来完成定义的
在React中也是一样,如果要定义响应式数据,组件内部的数据是定义在组件的state中,组件外部的数据是定义在props中
1、有状态组件和无状态组件
类组件是有状态组件:因为一个组件的状态是存放在类的实例上,state,props都是存在this上,所以类组件被称为有状态组件
函数组件是无状态组件:函数组件都没有this,函数是不能存放状态的
类组件比较强大,函数组件比较单一,之前类组件可以完成复杂的功能,但是函数组件是简单的组件
在React16.8版本之后引入hooks,可以让函数组件也能操作状态、
总结:React16.8之前函数组件是无状态组件,几乎很少用
2、基本使用步骤
使用state定义数据一共有三步骤
第一步:定义数据
定义数据可以在构造函数内部定义,也可以在构造函数外部定义
第二步:获取数据
在使用数据的时候为了提高读取性能,最好使用解构赋值方式
第三步:修改数据
修改数据的时候一定要使用setState({})来修改数据,这个方法是一个异步方法
第1步、定义数据
-
定义数据的时候可以在构造函数中定义数据,如下所示
class Counter extends React.Component{
constructor(){
super();
this.state={
count:0
}
}
render(){
return (
<div>
<h2>计数器</h2>
</div>
)
}
}
export default Counter;
-
也可以在构造函数外部定义,这种是利用ES6属性的简化语法,如下所示
class Counter extends React.Component{
//简化语法
state={
count:0
}
render(){
return (
<div>
<h2>计数器</h2>
</div>
)
}
}
export default Counter;
第2步、获取数据
通过this.state获取数据
class Counter extends React.Component{
constructor(){
super();
this.state={
count:0
}
}
render(){
return (
<div>
<h2>计数器</h2>
<span>{this.state.count}</span>
</div>
)
}
}
export default Counter;
在使用数据的时候,最好使用解构赋值的方式,这样能够提高性能
import React, { Component } from 'react'
export default class Counter extends Component {
state = {
num: 0
}
constructor() {
super()
}
render() {
const { num } = this.state;
return (
<div>
<h1>计数器</h1>
<span>{num}</span>
</div>
)
}
}
第3步、修改数据
-
状态是可以改变的
-
语法:this.setState({要修改的数据})
-
注意:不要直接修改state中的值,这样是错误的
-
setState()作用:1.修改state 2.更新UI
import React, { Component } from 'react'
export default class Counter extends Component {
state = {
num: 0
}
constructor() {
super()
}
render() {
const { num } = this.state;
return (
<div>
<h1>计数器</h1>
<span>{num}</span>
<button onClick={() => {
this.setState({
num: this.state.num + 1
})
}}>+1</button>
</div>
)
}
}
3、购物车案例
实现步骤
-
在compotents下创建ShopcartList.jsx,并在App.jsx中引入这个自定义组件
-
ShopcartList.jsx中的关键代码如下
import React, { Component } from 'react'
import '../assets/css/shopcartList.scss'
export default class ShopcartList extends Component {
constructor() {
super()
//定义状态数据
this.state = {
shopcartList: [
{
pid: '1001',
pname: '欧莱雅男士护肤',
price: 38,
num: 1
},
{
pid: '1002',
pname: 'OLAY女士防皱润肤露',
price: 108,
num: 1
},
{
pid: '1003',
pname: '自然堂女士护肤',
price: 108,
num: 2
},
{
pid: '1004',
pname: '兰蔻香水',
price: 1038,
num: 1
},
{
pid: '1005',
pname: '大宝SOD,每个人选择',
price: 8,
num: 1
}
]
}
}
//改变数量的方法
changeNum = (sign, index) => {
switch (sign) {
case '+':
//如下这个操作,它只能将数据进行更新,页面没有进行变化
this.state.shopcartList[index].num++
break
case '-':
if (this.state.shopcartList[index].num > 1) {
this.state.shopcartList[index].num--
} else {
window.alert('数量不能少于0')
}
break
}
this.setState({
shopcartList: this.state.shopcartList
})
}
//计算总价的函数
total = ary => `¥${ary.reduce((pre, cur) => pre + cur.price * cur.num, 0).toFixed(2)}`
//删除方法
deleteShopcartList = index => {
if (window.confirm('您确定要删除吗?')) {
//数组中的splice方法的参数的含义
//第一参数:表示要操作数组的下标
//第二个参数:表示的是要删除几个数据
this.state.shopcartList.splice(index, 1)
//使用this.setState来页面
this.setState({
shopcartList: this.state.shopcartList
})
}
}
render() {
const { shopcartList } = this.state
return (
<div>
<h2>购物车</h2>
<table>
<thead>
<tr>
<td>序号</td>
<td>名称</td>
<td>价格</td>
<td>数量</td>
<td>小计</td>
<td>操作</td>
</tr>
</thead>
<tbody>
{
shopcartList.map((item, index) => <tr key={item.pid}>
<td>{item.pid}</td>
<td>{item.pname}</td>
<td>{item.price}</td>
<td>
<button className='operbtn' onClick={() => { this.changeNum('-', index) }}>-</button>
{item.num}
<button className='operbtn' onClick={() => { this.changeNum('+', index) }}>+</button>
</td>
<td>{item.price * item.num}</td>
<td>
<button className='delBtn' onClick={() => { this.deleteShopcartList(index) }}>删除</button>
</td>
</tr>)
}
</tbody>
<tfoot>
<tr>
<td colSpan={6}>
{this.total(shopcartList)}
</td>
</tr>
</tfoot>
</table>
</div>
)
}
}
4、setState同步还是异步
setState是同步还是异步的
-
React18之后setState是异步的,如果是React18之前,setState根据情况来决定,可能是同步的也可以能是异步的
-
React18版本之后的
-
在React的事件处理中(合成事件),setState是异步的
-
在setTimeout、setInterval、原生js中它也是异步的(重点区别)
-
如果要在react18版本中将this.setState由异步变成同步,需要使用
flushSync
-
componentDidMount() {
console.log('1、', this.state.count);
document.querySelector('#btn').addEventListener('click', () => {
flushSync(()=>{{
this.setState({
count: this.state.count + 1
})
console.log('2、', this.state.count);
}})
flushSync(()=>{
this.setState({
count: this.state.count + 1
})
console.log('3、', this.state.count);
})
})
}
-
React18版本之前
-
在React的事件处理中(合成事件),setState是异步的
-
在setTimeout、setInterval、原生js中它也是同步的
-
5、setState的另外一种写法
setState((state,props)=>{},()=>{})