这里写目录标题
前言
在文章开始之前,让我们来看看Raect脚手架为我们搭建的一个React项目中的一个基本的.js文件结构:
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = { }
}
render() {
return (
<div>hello jingda</div>
);
}
}
export default App;
从上面的代码可以看出,在React中class组件的重要性(这里暂时先不考虑hook哦)。因此本篇文章会花比较大的篇幅讲一下有关react中class组件,包括但不限于:
- ES6 class 类定义和继承
- 类组件中如何搞定this指向问题
- React中组件的生命周期
- React中组件通信
当然,在讲类组件的同时,必定会涉及到我们也熟知的function函数组件。以及其他一些知识点。
本人也是在慢慢学习react的过程中,写文章是总结,是加深印象。如果看完这篇文章的你,能对react 组件化,或者只是class类组件有了一些理解,我将十分荣幸。
(文章很多部分都是作者本人直接码字,如果表述不当,麻烦评论区指正,多谢阅读。)
好了,文章马上开始,小二上脑图:
1、React 中的组件思想
1.1 基本概念
把一个很大很难的工程分成很多小部分由不同的人来完成。这是我对组件化的理解。
当然这里给出React官方的对组件的表述,如下
组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。
1.2 react中组件的分类
- 根据组件的定义:函数组件和类组件
- 根据组件内部是否有状态需要维护:无状态和有状态
- 组件的不同职责:展示型组件和容器组件
这些概念有很多重叠,但是他们最主要的关注数据逻辑
和ui展示的分离
:
- 函数组件,无状态组件和展示组件主要
关注UI的展示
- 类组件,有状态组件和容器组件主要
关注数据逻辑
;
简单来说就是在类组件中有数据逻辑发生改变,而函数组件中是没有自己的内部状态(state)。
2、React 函数组件
2.1、函数式组件定义和特点
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
上面给出的代码就是官方给出的函数式组件,一看这个代码,觉得这就是一句传递了一个参数props对象的javascript函数代码,但是它返回的一段JSX代码。这句JSX代码,最终会被我们的Babel编译成 一个React.createElement() 函数调用。
(不熟悉这部分知识点的同学,可以看一下我上次的文章 从JSX到渲染页面,中间发生了什么😯)。
函数式组件看起来还是比较简单的,很像我们传统的js函数,但是这个组件有一些特点,可能并不适合开发中的很多情况:
函数式组件特点:
- 没有生命周期,也会被更新加载,但是没有生命周期函数
(为什么没有??)
- 没有this(组件实例);
(为什么)?
- 没有内部状态(state),只接收唯一带有数据的 “props”(代表属性)对象;
不懂就问,在总结function函数式组件的特点的过程中,我在当中有这两个疑问:
结合谷歌,得到以下的解答(有更好解答的大佬,麻烦评论区告知哦)
1、为什么函数组件中没有生命周期呢?
我们知道函数式组件的写法很简单,在函数式组件没有像class组件中这样的代码:
class App extends Component {...}
因此它没有继承React.Component,由于生命周期函数是React.Component类的方法实现的,所以没继承这个类,自然就没法使用生命周期函数了
2、为什么函数式组件中没有this呢?
当我试图在网上寻找这个答案时,并没有得到预期的结果,当然我也尝试了一下,打印这个this
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="1" />
<Welcome name="2" />
<Welcome name="3" />
</div>
);
}
结果undefined
,但是我还是很不解
当我在react官网上找到:
所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
让我又把解决方案找到了纯函数这里,现在我在网上可以找到的答案是,纯函数:
- 函数的返回结果只依赖于它的参数。
- 函数执行过程里面没有副作用:一个函数执行过程对产生了外部可观察的变化那么就说这个函数是有副作用的。
因此在这里我的理解是this作为一个执行上下文对象,它现在指向的是undefined,然后又由于纯函数的第二个特点,因此它的this不能进行改变。所以它没有存在的必要。
当然,这里只是我个人的看法。可能并不是这么理解的(唉,有答案请评论区告诉我,救救孩子。)
2.2、函数式组件使用
- 只有一个组件
function App() {
return (
<div>
这个一个函数式组件
</div>
);
}
export default App;
只渲染组件内部的代码
- 结合组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
父组件是App,子组件是Welcome,props对象存储了来自父子组件的name属性,并且传给了子组件Welcome,进行渲染。
3、React 类组件
3.1类的定义和继承 【包含es5 es6继承对比,建议详读】
3.1.1、类的定义
我们都知道在javascript中是没有"类"的,在javascript中只有一些类型于类的语法元素(new和instanceof)不过,在ES6中新增了一些如class的关键字。但是这并不代表js中有类。
类只是一种设计模式,你可以用一些方法实现类的功能:比如生成一个实例对象
- 用类来实现:this关键字就是代表实例对象
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
- 用方法来实现:p 是通过new产生的实例
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
3.1.2、类的继承
通过上面的展示,我们知道Javascript程序中是可以通过方法去模拟类行为的,但是如果没有继承机制的话,那么Javascript中的类就只是一个空架子。
在实现“类继承”之前,让我们先来了解一下ES5的原型继承
:
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function () {
console.log(this.name,this.age,'running')
}
var p = new Person('why',18);
console.log(p.name,p.age);
p.running()
构造函数Person的原型对象上有running()这个方法,当使用new方法,p = new Person('why',18)
,此时就创建了一个p实例对象,它可以访问到原型对象Person上的方法
当我们尝试用class去实现这个“继承”:
class Person {
constructor(name,age) {
this.name = name;
this.age = age;
}
running() {
console.log("running")
}
}
class P extends Person {
constructor(name,age,sno) {
super(name,age);
this.sno = sno;
}
}
const p1 = new P("why",10,110)
console.log(stu.name,stu.age,stu.sno)
stu.running()
这里定义了两个类,Person和P,其中Person是父类,而P作为Person的子类。Person中有两个方法 constructor() 在里面的this就是实例对象。
在子类P的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例
。
3.2 React class组件
3.2.1 class组件定义和使用
让我们先把前面的函数式组件转成类组件来分析:
函数式组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="1" />
<Welcome name="2" />
<Welcome name="3" />
</div>
);
}
转成class组件:在这里我做了如下操作:
- 创建一个同名的 ES6 class ,并且继承于 React.Component。
- 添加一个空的 render() 方法。
- 将函数体移动到 render() 方法之中。
- 在 render() 方法中使用 this.props 替换 props。
- 删除剩余的空函数声明。
(还有更清楚的解释,可以查看官网哦)
export class Welcome extends Component{
constructor(props){
super(props)
}
render(){
return <h1>Hello, {this.props.name}</h1>;
}
}
class App extends Component{
constructor(props){
super(props);
}
render(){
return (
<div>
<Welcome name="1" />
<Welcome name="2" />
<Welcome name="3" />
</div>
);
}
}
export default App;
在class组件中我们同时也需要注意一些东西:
类组件的定义有如下要求:
- 组件的名称是大写开头的(无论类组件还是函数组件)
- 类组件需要继承自React.Component
- 类组件必须实现render函数
使用class定义一个组件:
constructor
是可选的,我们通常在constructor中初始化一些数据;this.state
中维护的就是我们组件内部的数据;render()
方法就是class组件中唯一必须实现的方法
当render被调用,它会检测this.props
和this.state
的变化并返回以下类型之一- react元素:
通常通过jsx创建</div>
会被react渲染为DOM节点,
<myComponent />
会被react渲染为自定义组件
无论是<div/>
还是<myComponent />
均为react元素
这就是我们上面写的代码中返回的那种方式 - 数组或Fragments: 使得render方法可以返回多个元素
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
- 字符串或数值类型:它们在DOM中会被渲染为文本节点
render() {
return 1;
}
- 布尔类型或null:什么都不渲染
render() {
// return null;
return false;
}
3.2.2 class组件 - 事件处理中的绑定this问题
来看一段代码:
class App extends React.Component{
constructor() {
super();
this.state = {
}
}
render() {
return (
<div>
<button onClick={this.btnClick}>按钮</button>
</div>
)
}
btnClick() {
console.log('按钮被点击this打印结果为:',this)
}
}
这是我一个很简单的例子,我希望在点击按钮时,控制台打印出'按钮被点击this打印结果为:'
,和这个点击事件中的this
。让我们来尝试一下:
很明显,我们这里的到的this为undefined。
当我们需要点击按钮,实现一些功能时:比如点击按钮实现数字的减一:
class App extends React.Component{
constructor() {
super();
this.state = {
couter:100
}
}
render() {
return (
<div>
<div>{this.state.couter}</div>
<button onClick={this.decrement}>-1</button>
</div>
)
}
decrement() {
console.log(this) //undefined
this.setState({
count:this.state.couter-1
})
}
}
代码直接报错了,因为在decrement()里面的this为undefined,而我们调用this.setState,this.state,本应该拿到的数据应该是constructor中的this,也就是我们的实例对象。但是这里并没有,拿到,为什么this会丢失呢?
按道理类的方法内部如果含有this,它默认指向类的实例,但是如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到decrement()方法而报错。
那么如何来解决这个问题呢?下面有四种方法(具体四种,实质两种)
- 在绑定事件的时候加bind
<button onClick={this.decrement.bind(this)}>-1</button>
- 在构造函数中使用bind
this.decrement = this.decrement.bind(this);
- 在绑定事件时使用箭头函数(
这个方法比较好用哦,简单,传递参数也比较方便
)
<button onClick={() => {this.decrement()}}>-1</button>
- 使用箭头函数写这个点击事件
decrement = () => {
}
4、React组件生命周期
4.1 生命周期
从创建到销毁的整个过程:叫做生命周期
react组件也有它的生命周期,这里注意是类组件的生命周期,因为函数式组件时没有生命周期的。这是官方的一张生命周期图谱,点此查看,当然这是一张简单的生命周期图,包括组件的:
- 装载阶段:组件第一次在DOM树上被渲染的过程
- 更新过程:组件状态发生改变,重新更新渲染的过程
- 卸载过程: 组件从DOM树中移除的过程
下面围绕这个生命周期图,来介绍react的生命周期过程:
首先有几个生命周期回调函数
需要了解: - 装载:componentDidMount函数:组件已经挂载到DOM树,就会回调
- 更新:componentDidUpdate函数:组件已经发生了更新时,就会回调
- 卸载:componentWillUnmount函数:组件即将被移除时,就会回调
我们可以在这些回调函数编写自己的逻辑代码,来完成自己的需求功能
4.2 在生命周期方法
- constructor
如果不初始化state或不进行绑定,则不需要为react组件实现构造函数
constructor中通常做两件事:
1、 通常给this.state赋值对象来初始化内部的this
2、为事件绑定实例 this - render (前面已经说了)
- componentDidMount
1、componentDidMount()在组件挂载后(插入到DOM树)立即调用
2、 componentDidMount中通常进行哪些操作:?
3、依赖于DOM的操作可以在这里执行
4、在此处发送网络请求
5、 在这里添加一些订阅 (会在componentWillUnmount取消订阅); - componentDidUpdate
1、 componentDidUpdate 会在更新时立即调用,首次渲染不会执行此方法
2、 当组件更新后,可以在此处对DOM进行操作
3、 如果对更新前后的props进行了比较,也可以选择在此处进行网络请求(当props未发生变化时,则不会执行网络请求); - componentWillUnmount
1、componentWillUnmount() 会在组件卸载及销毁之前直接调用
2、在此方法中执行必要的清理操作
3、取消网络请求或清除在componentDidMount()中创建的订阅等
4.3 装载,更新,卸载
- 装载:从上面的图我们可以看出一直到componentDidMount()执行,经历了
例子:
class App extends Component {
constructor(props) {
super(props);
console.log("1、执行了constructor()")
this.state = {
counter:1,
}
}
render() {
console.log("2、执行了render()")
return (
<div>
<div>{this.state.counter}</div>
</div>
);
}
componentDidMount() {
console.log("3、调用了componentDidMount()")
}
}
一个很简单的例子,我们把构造函数定义的state:counter显示在页面上,运行代码:
- 更新,在进行更新的过程中,主要经历的生命周期方法:(这里只是说主要的哦),可以看出更新过程中constructor方法不要用到。
例子:还是上面的例子,当我们需要点击按钮counter+1:
class App extends Component {
constructor(props) {
super(props);
console.log("1、执行了constructor()")
this.state = {
counter:1,
}
}
render() {
console.log("2、执行了render()")
return (
<div>
<div>{this.state.counter}</div>
<button onClick={() => this.btnClick()}>点击</button>
</div>
);
}
componentDidMount() {
console.log("3、调用了componentDidMount()")
}
componentDidUpdate() {
console.log("4、调用了componentDidUpdate()")
}
btnClick() {
this.setState({
counter:this.state.counter+1
})
}
}
当点击按钮时,运行结果为:
constructor(),和componentDidMount()不会在执行。
- 卸载 :组件卸载,那么我们就再定义一个组件,刚开始是显示的,当点击按钮时,让这个组件不渲染:
class Jing extends Component {
render() {
return <h2>这里是婧大</h2>
}
componentWillUnmount() {
console.log("调用了Jing组件 componentWillUnmount() 卸载方法")
}
}
class App extends Component {
constructor(props) {
super(props);
console.log("执行了constructor()")
this.state = {
counter:1,
isShow:true
}
}
render() {
console.log("执行了render()")
return (
<div>
<button onClick={() => this.btnShow()}>999</button>
{this.state.isShow && <Jing/>}
</div>
);
}
componentDidMount() {
console.log("调用了componentDidMount()")
}
btnShow() {
this.setState({
isShow:!this.state.isShow
})
}
}
最开始打开页面时:
当点击按钮:
它执行了一个render方法和一个componentWillUnmount()方法。
5、组件通信
5.1 父传子
父组件在展示子组件时,可能会传递一些数据给子组件:
- 父组件通过
属性 = 值
的形式来传递子组件数据 - 子组件通过props参数获取父组件传递过来的数据
class Child extends Component {
constructor(props) {
super(props);
this.props = props;
}
render() {
const {name,age,height} = this.props
return (
<div>
子组件展示数据:{name+""+age+""+height}
</div>
)
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = { }
}
render() {
return (
<div>
<Child name="why" age="18" height="1.88" />
</div>
);
}
}
- 子传父
子传父比父传子复杂一点,因为React是单向数据流,在react中父组件中,我们可以通过props方便的向子组件传递数据,但是子组件怎么向父组件传递值呢?
先定义一个这样的需求:在父组件中有一个counter=0,在子组件里有个按钮,我希望点击子组件的按钮去实现counter加1的效果。
- 1、在父组件这里定义一个函数,
btnClick()
- 2、让父组件传给子组件,
<Child btnClick={() => this.btnClick()} />
,就是给子组件加上一个属性 - 3、子组件对父组件有个引用,依然是通过props传数据,只不过现在是把回调函数作为参数props
<button onClick={this.props.btnClick}>+1 </button>
- 4、当子组件触发事件的时候,实际上是由父组件来执行
看完整代码:
export class Child extends Component {
constructor(props) {
super(props);
this.state = { }
}
render() {
return (
<button onClick={this.props.btnClick}>+1 这是子组件按钮</button>
)
}
}
class App extends Component {
constructor(props) {
super(props);
this.state = {
counter:0
}
}
render() {
return (
<div>
<h2>这里是父组件:{this.state.counter}</h2>
<Child btnClick={() => this.btnClick()} />
</div>
);
}
btnClick() {
this.setState({
counter:this.state.counter+1
})
}
}
效果:
- 跨组件 (先不考虑context)
在上面这个图中,我们可以实现A->B ,B->A这种相邻组件的通信,那么当我们如果需要把A的数据传给C呢,直接传是不能的,我们需要先传给B,再由B传给A。
这种方法,如果只是实现这个隔代组件传值问题,可以解决。但是,当你把A的props传给B的时候,如果B并不需要A的props数据,那不是浪费了吗。而且在开发中,多的是复杂的组件传值。
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props,目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言.
有关这个Context,容我再花些时间理解理解,学完会再来总结一波。今天就到这里啦。
总结
这篇文章到这就结束啦,再来回顾文章开头说到的知识点,不知道你有没有多一些理解呢。
- ES6 class 类定义和继承
- 类组件中如何搞定this指向问题
- React中组件的生命周期
- React中组件通信
知识是无止境的,我知道还有很多东西我都还没有理解地很到位,还是慢慢来吧。感谢你阅读完这篇文章。在我花时间总结完后,希望对你也有所帮助。
还有有关组件内部自身参数state,传递参数的props对象属性,setState()
以及,在知道function函数式组件中没有状态,和class组件中复杂的this指向问题和代码编写冗余问题,还没好好学的hook。
都值得我花时间去学。
就先到这吧