一、组件的概念
使用组件的方式进行编程,可以提高开发效率,提高组件的复用性、提高代码的可维护性和可扩展性
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===1?'男':'女'}\n学历:${this.education}\n专业:${this.major}\n` }}/* 创建对象的语法 const/let 对象名 = new 类名() 给对象赋值的语法 对象名.属性名=值 调用对象中的方法 对象名.方法名()*/
案例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('1001','张三','男','专科','通讯工程')console.log(s1.introduce());console.log("*********************************************");const s2=new Student('1002','李四','男','专科','英语')console.log(s2.introduce());
案例三:继承的使用
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 zhangsan=new WebTeacher('张三','xxx')console.log(zhangsan.introduce()); zhangsan.giveLession()console.log('********************************');let lisi=new WebTeacher('李四','xxx')console.log(lisi.introduce());lisi.giveLession()console.log('**************************************');const wanger=new TestTeacher('王二','xxx') console.log(wanger.introduce())wanger.giveLession()
2、定义类组件
类组件:是指通过ES6类来定义的组件称为类组件,React中定义类组件有如下约定
类组件首字母大写
类组件必须要继承React.Component父类,这个父类中的相关方法和属性都能被继承
类组件中必须要有一个render方法
这个render必须要有返回值,返回值的内容就是这个类组件的结构(jsx)
由于这个类组件要被别的组件引用,所以使用ES6的默认导出将其导出,便于别的组件引用
import React from "react"export default class Hello extends React.Component{ render(){ return( Hello组件
) }}
类组件定义之后,引用这个类组件
import ReactDOM from 'react-dom/client'import Hello from './components/Hello'const template=( )const root=ReactDOM.createRoot(document.querySelector('#root'))root.render(template)
3、定义函数组件
在React中除了类组件之外,也可以定义函数组件
函数租价:所谓函数组件是指通过普通函数或者箭头函数所定义出来的组件称为函数组件
函数组件有如下规定
函数名必须首字母大写
使用ES6的export将默认导出,便于别的组件引用
函数必须要有一个返回值,这个返回的内容是JSX,返回的是该函数组件的结构
export default function HelloWorld(){ return ( Hello World函数组件
)}
注意:如果不返回任何内容,假设返回一个null,页面将没有任何内容
在实际开发过程中,由于这些组件和函数组件它的结构都是固定的,所以可以使用一些插件将其生成出来
使用rcc
生成类组件,使用rfc
生成函数组件
三、类组件的事件处理
1、React的事件处理
vue中通过什么绑定事件处理函数
v-on指令来实现绑定,可以通过他的简写@事件类型方式来绑定事件的
在React中通过onClick属性来实现单击事件的绑定的,常见的写法
直接在标签中通过onClick={()=>{}}来实现事件的绑定
在标签中通过onClick={this.类中的普通成员方法}来进行绑定
在标签中通过onClick={this.类中的箭头函数}来进行绑定
在标签中通过onClick={()=>{this.函数名称()}}
import React,{Component} from 'react'export default class Hello extends Component {handleClick(){ console.log("类中的定义的普通方法",this)}handleClick2=()=>{ console.log("类中定义的箭头函数",this)}render(){ return ( ) }}
2、this指向的回顾
letteacher={name:'teacher'}letstudent={name:'student'}letperson={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指向letteaherShow=person.show.bind(teacher)console.log(teaherShow(55,'男'));
如果使用第二方式来进行事件绑定的时候,会存在this执行为空的情况,解决办法如下
- 使用箭头函数写法代替普通方法(建议)
- 通过bind方式来改变this执行(不建议)
this执行改变的代码可以写在多个位置,比如写在构造函数中(经典的写法)
exportdefaultclassHelloextendsComponent{constructor(){super()//改变this执行this.handleClick=this.handleClick.bind(this)}}
也可以在调用的同时去改变this指向(不建议)
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 ( ) }}
四、类组件的state
vue框架和React框架最大的一个好处就是不需要开发人员去操作DOM,只要大家操作了数据,自动DOM元素会发生改变,这种操作称为响应式
在vue中响应式数据主要来自两个部分
组件内部响应式数据是定义在data选项
来自子组件外部通过props来定义的
在React中也是一样,如果要定义响应式数据,组件内部的数据是定义在组件的state中,组件外部的数据是定义在props中
1、有状态组件和无状态组件
类组件是有状态组件:因为一个组件的状态是存放在类的实例上,state,props都是存在this上,所以类组件被称为有状态组件
函数组件是无状态组件:函数组件都没有this,函数不能存放状态
类组件比较强大,函数组件比较单一,类组件可以完成复杂的功能,但是函数组件是简单的组件
在React16.8版本之后引入hooks可以让函数组件也能操作状态
总结:React16.8之前函数组件是无状态组件,几乎很少使用
2、基本使用步骤
使用state定义数据一共有三步骤
第一步:定义数据
定义数据可以在构造函数内部定义,也可以在构造函数外部定义
第二步:获取数据
在使用数据的时候为了提高读取性能,最好使用解构赋值方式
第三步:修改数据
修改数据的时候一定要使用setState({})来修改数据,这个方法是一个异步方法
第1步、定义数据
- 定义数据的时候可以在构造函数中定义数据,如下所示
classCounterextendsReact.Component{constructor(){super();this.state={count:0}}render(){return(计数器
)}}exportdefaultCounter;
- 也可以在构造函数外部定义,这种是利用ES6属性的简化语法,如下所示
classCounterextendsReact.Component{//简化语法state={count:0}render(){return(计数器
)}}exportdefaultCounter;
第2步、获取数据
通过this.state获取数据
classCounterextendsReact.Component{constructor(){super();this.state={count:0}}render(){return(计数器
{this.state.count})}}exportdefaultCounter;
在使用数据的时候,最好使用解构赋值的方式,这样能够提高性能
importReact, {Component}from'react'exportdefaultclassCounterextendsComponent{state = {num:0}constructor() {super()}render() {const{ num } =this.state;return(计数器
{num})}}
第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 ( 计数器
{num} ) }}
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.shopcarList[index].num > 1){ this.state.shopcarList[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)}`//删除的方法deleteShopcarList = index =>{ if(window.confirm("您确定要删除吗?")){ //数组中的splice方法的参数含义 //参数1:表示要操作的数组的下标 //参数2:表示要删除几个数据 this.state.shopcartList.splice(index,1) //使用this.setState来更新页面 this.setState({ shopcartList:this.state.shopcartList }) }}render(){ const {shopcartList} = this.state return ( 购物车
序号 名称 价格 数量 小计 操作 { shopcartList.map((item,index)=> {item.pid} {item.pname} {item.price} {item.num} {item.price*item.num} ) } {this.total(shopcartList)}
) }}
4、setSate是同步还是异步
需要分情况
在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)=>{},()=>{})
五、类组件的props属性
组件中的数据,除了组件内部的状态使用state之外,状态也可以来自组件的外部,外部的状态使用类组件实例上另外一个属性来表示props
1、基本的使用
在components下创建UserInfo组件
import React, { Component } from 'react'import '../assets/css/userinfo.scss'export default class UserInfo extends Component { render() { return ( {this.props.title}
姓名:{this.props.name} 年龄:{this.props.age} 性别:{this.props.gender===1" /> - 通过props传递进来的引用数据,不能直接更改,如果更改会报如下错误
- 通过props传递进来的引用数据类型,可以更改引用数据类型的属性
六、表单组件
表单的处理:从表单中获取内容
vue中如果获取表单的内容:v-model:是一个双向绑定的指令,react不是一个双向绑定
1、受控表单
1.1、什么是受控组件
在HTML中,表单元素(如、、和)之类的表单元素同茶馆自己维护state,并根据用户输入进行更新。而在React中,可以改变状态(mutable state)通常保存在组件的state属性中,并且只能通过setState()来更新
我们可以把两者结合起来,使React的state成为“唯一的数据源”。渲染表单的React组件还控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入严肃叫做“受控组件”
1.2、受控组件的使用步骤
在state中添加一个状态name
state={ name:"}
我们在input标签里面定义了value属性,这个属性就是文本框的值,将state对象里面的name绑定到文本框里
onChange是必要的,这个函数相当于绑定了一个事件,当数据发生变化的时候,可以执行帮的的函数handleName
定义handleName函数,将文本框的值动态绑定赋值给state对象
handleName=(e)=>{ this.setState({ name:e.target.vale })}
参数e代表事件对象,可以通过e来获取到target目标对象,通过value值来获取文本框的值。
2、非受控表单
有时使用受控组件会很麻烦,因为你需要为数据变化的每种方式都编写事件处理函数,并通过一个 React 组件传递所有的输入 state。当你将之前的代码库转换为 React 或将 React 应用程序与非 React 库集成时,这可能会令人厌烦。在这些情况下,你可能希望使用非受控组件, 这是实现输入表单的另一种方式。
在大多数情况下,我们推荐使用 受控组件 来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。
要编写一个非受控组件,有两种方式
2.1、第1种方式
使用步骤
- 在表单元素中添加ref属性
this.nameEle=input}/>
- 在通过this.nameEle.value来获取到当前节点的值
register=(e)=>{console.log(this.nameEle.value);e.preventDefault();}
2.2 、第2种方式
使用步骤
- 调用React.createRef()方法创建一个ref对象
constructor(){super();this.username=React.createRef();}
- 将创建好的ref对象添加到文本框中
- 通过ref对象获取到文本框的值
register=(e)=>{console.log(this.username.current.value);e.preventDefault();}
七、组件的生命周期
生命周期指的就是从组件的加载初始化-数据的改变-组件的卸载阶段。描述的就是组件从创建到死亡的阶段,react的生命周期中提供了很多个生命周期钩子函数来方便我们操作。
react的生命周期主要分为以下的几个步骤:
1、挂载阶段:组件数据的初始化,及组件数据的渲染
2、运行阶段:这个节点是最长的阶段,主要用户对数据进行修改,进行状态改变以及重绘
3:卸载阶段:这个阶段也是销毁阶段,组件运行完成后,或者从页面移除,那么组件就应该被销毁,这个阶段我们可以执行一些资源的回收,性能优化的代码,在此阶段完成
组件的生命周期流程图
1、组件挂载阶段
组件的挂载阶段也是组件创建-数据初始化-数据渲染的过程。接下来通过代码来演示执行的流程。
1.1 constructor构造器执行的阶段
构造执行那就意味着组件正在被创建,并且数据也可以在这里初始化。
通常,在 React 中,构造函数仅用于以下两种情况:
- 通过给
this.state
赋值对象来初始化内部 state - 为事件处理函数绑定实例
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
先执行constructor构造器将数据初始化,其中props和state的数据就会被加载。接着执行render函数来渲染。
1.2 componentDidMount函数执行
界面渲染完毕(DOM挂载完毕)的一个通知,类似于vue组件中的created,我们可以在这个生命周期做
很多事情。
- 获取DOM节点
- 发送请求获取后台数据
- 设置定时器、延时器等等
- 绑定全局事件,例如document的点击事件等等
asynccomponentDidMount(){console.log("===componentDidMount==");const{data:{movies}}=awaitaxios.get('https://www.fastmock.site/mock/4441a79ad5f884f83b784cf4c588bdb6/movies/getHotMovies')}
2、组件更新阶段
每当组件的props或者state变化之后,都会导致组件的更新,我们可以使用钩子函数在组件更新之前或者之后做一些逻辑操作。
2.1、创建shouldComponentUpdate钩子函数来拦截数据修改
shouldComponentUpdate(){returnfalse;}
定义在重新render之前被调用,可以返回一个布尔值来决定一个组件是否更新, 如果返回false,那么前面的流程都不会被触发。这个方法默认的返回值都是true。
这里我们再来完成一个功能就是将外部传递进来的数据赋给内部变量data,当点击按钮的时候每次增加1
importReact, {Component}from'react'importaxiosfrom'axios'exportdefaultclassLifeCycleextendsComponent{constructor(props){super(props)this.state={title:props.title}}changeTitle=()=>{this.setState({title:"计数器演示"})}render() {console.log("====调用render方法开始执行数据渲染");return({this.state.title}
更改标题)}shouldComponentUpdate(nextProps,nextState){console.log(this.state.title+"---------->"+nextState.title);returntrue}}
但是从控制台的显示结果来看,每次点击之后都是上次的数据,如果要想得到更改后的数据我们可以使用如下参数来解决这个问题
参数:nextProps代表更改后的props值;nextState代表更改过后的state值。我们可以获取到这个数据进行判断
shouldComponentUpdate(nextProps,nextState){console.log(this.state.data+"---->"+nextState.data);returntrue;}
控制台显示效果如下所示
2.3 componentDidUpdate钩子函数来执行通知。
componentDidUpdate(){console.log("数据修改成功");}
3、组件卸载阶段
componentWillUnmount,在组件被卸载和销毁之前调用的方法,可以在这里做一些清理的工作。我们要完成组件的卸载,那需要创建两个组件,在父组件中引入子组件,当条件为true的时候就加载组件,条件为false的时候就卸载组件。
exportdefaultclassParentComextendsComponent{state = {isShow:true}changePanel =()=>{this.setState({isShow:false})}render() {return(ParentComponent
{this.state.isShow &&}{/* {this.state.isShow && } */}点击切换)}}
花括号中嵌入逻辑与 (&&) 运算符。它可以很方便地进行元素的条件渲染,&&是我们的短路运算符,一旦this.state.boo的值为false,那后面
的组件就不会渲染。
当点击按钮我们动态修改isShow的值,然后判断条件false组件就卸载。
exportdefaultclassChildComextendsComponent{render() {return(ChildComponent
)}componentWillUnmount(){console.log("ChildCom组件正在卸载");}}
一旦组件卸载那就执行componentWillUnmount钩子函数完成调用,那我们可以完成清理工作。
执行结果为:
ChildCom组件正在卸载
4、资源清理
在componentWillUnmount钩子函数中我们可以执行一些资源清理工作。比如事件解绑,定时器清除等等工作。
先在ChildCom子组件里面定义事件和定时器等等代码。
componentDidMount(){this.timer=setInterval(function(){console.log("定时器执行");},1000);}
在componentDidMount组件里面设置一个定时器,我们在父组件里面将组件卸载。但是定时器依然在执行所以,所以在组件卸载的时候我们需要清楚定时器。
componentWillUnmount(){console.log("ChildCom组件正在卸载");clearInterval(this.timer);}
执行完组件的卸载,那定时器就被清除。
八、兄弟组件通信
- 状态提升
- 事件总线
- 发布订阅模式
1、状态提升
React中的状态提升概括来讲,就是将多个组件需要共享的状态提升到他们最近的父组件上,再在父组件上改变这个状态,然后通过props分发给子组件
2、事件总线方式
兄弟之间传递参数我们有多种方案,本节中我们就给大家带来。基于事件总线的方式来设计
EventBus 又称为事件总线。在Vue中可以使用 EventBus 来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的灾难,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。
但在React中没有EventBus的概念,可以通过 node events模块进行模拟,在React中可以依赖一个使用较多的库 events 来完成对应的操作。
1)安装events
yarn add events
2)创建事件中心
新建一个文件MyEventListener.js文件用于作为事件中心。
import{EventEmitter}from'events';exportdefaultnewEventEmitter();
我们只需要引入EventEmitter对象,将这个对象实例化过后返回出去,调用的时候执行对应的API就可以完成事件的绑定和通知
events常用的API:
创建EventEmitter对象:eventBus对象;
发出事件:eventBus.emit("事件名称", 参数列表);
监听事件:eventBus.addListener("事件名称", 监听函数);
移除事件:eventBus.removeListener("事件名称", 监听函数);
3)监听事件
importReact, {Component}from'react'importMyEventListenerfrom'./MyEventListener'exportdefaultclassBorther1extendsComponent{state={className:'web05'}//在组件完毕后就监听dataChange事件componentDidMount(){MyEventListener.addListener("dataChange",this.changeData);}//当组件卸载的时候就移除事件监听componentWillUnmount(){MyEventListener.removeListener('dataChange',this.changeData);}changeData=(params)=>{this.setState({className:params});}render() {return(兄弟1
数据为:{this.state.className}
)}}
在生命周期函数里面绑定事件监听和移除事件监听,当事件中心里面加入了dataChange事件,当前组件就能监听到,接着执行我们绑定的changeData。
4)发送事件
在Brother2组件里面我们添加一个按钮,往事件中心发出一个dataChange事件,并将值传递给调用者
importReact, {Component}from'react'importMyEventListenerfrom'./MyEventListener'exportdefaultclassBorther2extendsComponent{changeData=()=>{MyEventListener.emit("dataChange","web08");}render() {return(兄弟2
修改数据)}}
其中emit这个API就可以完成事件发送。这样就可以完成兄弟组件之间的参数传递,当然事件总线这种设计模式可以应用在任何组件之间。实现跨组件通信。
3、发布与订阅模式
1)定义订阅者
//创建一个observer.js文件constobserver={list:[],subscribe(callback){this.list.push(callback)},dispatch(data){this.list.forEach(item=>{item(data)});}}exportdefaultobserver
2)定义子组件用来发送数据
importReact, {Component}from'react'import'./son.css'importobserverfrom'../observer'exportdefaultclassSonextendsComponent{render() {return(儿子
{observer.dispatch('兄弟姐妹们,我给每个人200元')}}>发送)}}
3)定义子组件用来接受数据
importReact, {Component}from'react'import'./daughter.css'importobserverfrom'../observer'exportdefaultclassDaughterextendsComponent{state={fromMsg:''}componentDidMount(){observer.subscribe(data=>{this.setState(()=>{return{fromMsg:data}})})}render() {return(女儿
{this.state.fromMsg})}}