React学习笔记 --- 组件化开发(上)

2,686 阅读18分钟

一、 为什么需要组件

人面对复杂问题的处理方式:

​ 任何一个人处理信息的逻辑能力都是有限的 ,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。但是,我们人有一种天生的能力,就是将问题进行拆解。 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解

组件化拆分

总结:

  1. 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石;
  2. 而前端目前的模块化和组件化都是基于分而治之的思想;

二、 什么是组件化开发

​ 如果我们将一个页面中所有的处理逻辑全 部放在一起,处理起来就会变得非常复杂, 而且不利于后续的管理以及扩展。

​ 但如果,我们讲一个页面拆分成一个个小 的功能块,每个功能块完成属于自己这部 分独立的功能,那么之后整个页面的管理 和维护就变得非常容易了。

所以我们需要通过组件化的思想来思考整个应用程 序:

	1. 我们将一个完整的页面分成很多个组件; 

 		2. 每个组件都用于实现页面的一个功能块; 
             		3. 每一个组件又可以进行细分; 
            		4. 组件本身又可以在多个地方进行复用;

d5wsZn.png

总结:

  1. 对一个页面按照功能进行一层层的划分,直到划分到最小位置,随后在将拆分开来的组件依据他们之间的相互关系使他们有机的整合在一起

    1. 一般情况下,react中的根组件App组件,其余组件都是App组件子组件

三、React中的组件化

组件化是React的核心思想, 前面我们封装的App本身就是一个组件

	1. 组件化提供了一种`抽象(描述)`,让我们可以开发出一个个·独立可复用·的小组件来构造我们的应用。 

 		2. 任何的应用都会被`抽象`成一颗`组件树`

d5Be1O.png

组件化思想的应用:

  1. 有了组件化的思想,我们在之后的开发中就要充分的利用它。
  2. 尽可能的将页面拆分成一个个小的、可复用的组件。
  3. 这样让我们的代码更加方便组织和管理,并且扩展性也更强。

React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:

  1. 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);

  2. 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);

  3. 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(逻辑组件)(Container Component);

这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:

  • 函数组件、无状态组件、展示型组件主要关注UI的展示
  • 类组件、有状态组件、容器型组件主要关注数据逻辑

React中组件的注意事项:

  1. html中是不严格区分大小写的,也就是说h2写成H2, div写成DIV, 在html中都是可以被正常解析的

    但是在JSX中,标签是严格区分大小写的, 因为在JSX中首字母大写的标签会被认为是组件去进行解析,

    所以H2JSX中是不会被作为h2去进行解析的,而是会作为组件去进行解析,但是我们并没有名其H2的组件,

    所以此时就会报H2 is not defined的错误

  2. 无论是函数式组件还是类组件,其返回的jsx只能有一个根组件如果有多个根组件,react就不知道具体需要渲染哪一个组件为主

    如果的确需要返回多个组件,可以使用数组作为render函数返回值

3.1 类组件

类组件的定义有如下要求:

  1. 组件的名称是大写字符开头(无论类组件还是函数组件
  2. 类组件需要继承React.Component
  3. 类组件必须实现render函数, 以告诉react框架需要在页面上渲染什么结构和什么内容

在ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6的class类定义。

使用class定义一个组件:

  1. constructo是可选的,我们通常在constructor中初始化一些数据;
  2. this.state维护(管理)的就是我们组件内部的数据·;
  3. render() 方法是 class 组件唯一必须实现的方法;
// 注意 npm包的名字都是小写的,所以引入的是react,不是React
import React, { Component } from 'react'

export default class App extends Component {
    constructor() {
        super()

        this.state = {
            name: 'Klaus'
        }
    }

    // render函数是必须定义的
    // 以便于告诉react框架需要在界面上渲染什么内容
    render() {
        return (
            <div>
                <h2>这是类组件</h2>
                <p>My Name is { this.state.name }</p>
            </div>
        )
    }
}

3.2 函数组件

函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容

函数组件有自己的特点(暂时不使用Hooks):

  1. 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
  2. 没有this(组件实例);
  3. 没有内部状态(state);
import React from 'react'

// 函数组件本质上就是一个构造函数
// 函数组件的特点
//   1. 函数组件是没有状态的,因为每调用一次该组件,其都会开辟一个新的函数执行上下问
//      所以 其是没有办法来保留状态的,如果需要保留状态需要使用到 React Hook
//   2. 在函数组件中,因为不是类定义,所以其内部是无法使用this的,如果需要使用this,依旧需要借助 React Hook

//  所以 一般来说 函数组件可以被认为是无状态组件
export default function App() {
    // 函数组件也是返回JSX,告诉React需要在界面上显示什么,以及如何显示
    return (
        <h2>我是函数组件</h2>
    )
}

以下这么写就是错误的

import React from 'react'

export default function App() {
    const msg = 'Klaus'

    return (
        <div>
            <h2>我是函数组件</h2>
            <div>{msg}</div>
        </div>
    )
}

虽然这么写在界面上可以正常展示,且不会报错

但是因为每次重新渲染都会开启一个新的函数执行上下文,所以在函数组件中是没有办法保存上一次的状态

其次,因为react在使用babel进行转换的过程中开启了严格模式,所以在函数组件中是没有this的,this的值是undefined

3.3 render函数的返回值

当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:

  1. React 元素:

    1.1 通常通过 JSX 创建。

    ​ 例如 <div /> 会被 React 渲染为 DOM 节点

    <MyComponent /> 会被 React 渲染为自定义组件

    ​ 无论是 <div /> 还是 均为被渲染为 React 元素(ReactElement元素,也就是VDOM)

  2. 数组或 fragments:使得 render 方法可以返回多个元素。

    import React, { Component } from 'react';
    
    class App extends Component {
        render() {
            return (
                [
                    <div>我是第一个导出的数据</div>,
                    <div>我是第二个导出的数据</div>
                ]
            )
        }
    }
    
    export default App;
    
  3. Portals:可以渲染子节点到不同的 DOM 子树中。

  4. 字符串或数值类型:它们在 DOM 中会被渲染为文本节点

    // 返回字符串的时候,其就不需要进行JSX转换的了,所以就需要在引入React了
    import { Component } from 'react';
    
    class App extends Component {
        render() {
            return [
                {/*
                React在导出的时候,只能有一个根节点
                如果需要导出多个根节点的时候,需要在外层包一层数组
                数组中多个元素之间使用逗号进行分割
    
                在这里如果直接注释,其注释也会被解析为ReactElement元素
                所以在这种情况下市不可以直接写注释的,所以需要使用数组的方式进行返回
            */},
                'Klaus',
                {/* return 123 */}
            ]
        }
    }
    
    export default App;
    
  5. 布尔类型或 null:什么都不渲染

    import { Component } from 'react';
    
    class App extends Component {
        render() {
            return [
                {/*
          这里可以返回 null, true, false
          这些不会报错可以正常解析,但是界面上不会有任何渲染
    
          注意: 在这里是不可以导出undefined,返回undefined,渲染的时候是会报错的
        */},
                null
            ]
        }
    }
    
    export default App;
    

四、 生命周期

很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期

React组件也有自己的生命周期, 其会在执行到某一个特定的阶段的时候,去调用我们定义的callback

以便于我们在渲染组件的某一些关键的时刻进行自定义操作

React的生命周期示意图可以查看这里 React生命周期示意图

4.1 生命周期和生命周期函数的关系

生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;

  • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程; +
  • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
  • 比如卸载过程(Unmount),组件从DOM树中被移除的过程

React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:

  • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调;
  • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调;
  • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调; p 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;

我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;

(后面我们可以通过hooks来模拟一些生命周期的回调)

4.2 常用的生命周期函数

d52Hnf.png

mounting --- 挂载阶段

  1. 调用constructor进行组件实例初化

  2. 调用render方法来渲染界面 ---- 将内容更新到DOM树上

  3. 调用componentDidMount这个callback,表示组件已经挂载完毕后的自定操作

uodating --- 更新阶段

  1. 状态的改变或强制更新组件会触发render函数的调用

  2. 执行render函数来重新渲染界面

  3. 执行componentDidUpdate这个calkback,表示组件已经被更新完成

unmounting --- 卸载阶段

  1. 某些操作,触发组件卸载 (例如 thisShow为true的时候,显示 Category组件,如果为假,就什么也不显示)

    1. 调用render方法,来更新界面
    2. 执行componentWillUnmount这个回调函数,表示组件即将被移除
    3. 调用componentDidUpdate这个回调,表示界面已经被更新完毕,且组件已经被移除

生命周期中的主要职责

Constructor

严格意义上来说,这个不是生命周期函数,这个函数只是用来进行初始化组件实例对象的类似于 vue 的 created

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

其会调用默认的构造函数(默认构造函数会调用super初始化父类的构造器)

但是自定义了构造函数后,其是不会再去调用默认的构造函数的

(所以需要手动调用super方法以初始化父类实例,且super必须在构造器中使用this的语句之前被调用)

constructor中通常只做两件事情:

  1. 通过给 this.state 赋值对象来初始化内部的state;
  2. 为事件绑定实例(this)

componentDidMount

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。

类似于vue中的mounted

componentDidMount中通常进行哪里操作呢?

  1. 依赖于DOM的操作可以在这里进行;
  2. 在此处发送网络请求就最好的地方;(官方建议)
  3. 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)

componentDidUpdate

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执 行此方法

类似于vue的updated方法

componentDidUpdate中可以执行的操作?

  1. 当组件更新后,可以在此处对 DOM 进行操作;
  2. 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网 络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。

类似于vue的beforeDestory方法

componentWillUnmount可以执行的操作

  1. 在此方法中执行必要的清理操作; (例如,清除 timer,取消网络请求或清除 在 componentDidMount() 中创建的订阅等)

示例:

子组件 --- Cpn.js

import React, { Component } from 'react';

class Cpn extends Component {
    render() {
        return (
            <h2>我是子组件Cpn</h2>
        )
    }
}

export default Cpn;

父组件 --- App.js

import React, { Component } from 'react';

import Cpn from './Cpn'

class App extends Component {
    constructor() {
        super()

        this.state = {
            isShow: true
        }
    }

    render() {
        console.log('render')

        return (
            <div>
                <button onClick={ () => { this.handleClick() } }>切换子组件的显示和隐藏</button>
                {/*
            1. 如果isShow为真的时候,就显示Cpn组件,其就类似于vue中的v-if指令

            2. 和vue引入组件需要先注册后使用不一样, react组件只要引入后就可以直接使用
          */}
                { this.state.isShow && <Cpn /> }
            </div>
        )
    }

    handleClick() {
        this.setState({
            isShow: !this.state.isShow
        })
    }

    componentWillUnmount() {
        console.log('componentWillUnmount')
    }

    componentDidUpdate() {
        console.log('componentDidUpdate')
    }
}

export default App;

执行顺序

  1. 执行了setState方法,触发了render方法,更新VDOM
  2. 触发 componentWillUnmount方法,移除Cpn组件
  3. 组件移除,界面更新完成, 调用componentDidUpdate

4.2 不常用的生命周期

d7NYuQ.png

五、 组件间通信

组件之间存在嵌套关系:

  • 在之前的案例中,我们只是创建了一个组件App;
  • 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
  • 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
  • 再将这些组件组合嵌套在一起,最终形成我们的应用程序;

5.1 组件的嵌套

d7IE28.png

import React, { Component } from 'react';

function Header() {
    return <h2>Header</h2>
}

function Footer() {
    return <h2>Footer</h2>
}

function Banner() {
    return <h4>Banner</h4>
}

function ProductList() {
    return <h4>List</h4>
}

function Main() {
    return (
        <div>
            <Banner />
            <ProductList />
        </div>
    )
}

class App extends Component {
    render() {
        return (
            <div>
                <Header />
                <Main />
                <Footer />
            </div>
        );
    }
}

export default App;

5.2 组件间通信

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

  • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让 其进行展示;

  • 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;

  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;

    总之,在一个React项目中,组件之间的通信是非常重要的环节

    所谓通信其本质就是组件和组件之间进行的数据传递

父子组件之间进行传递

类组件父传子

父组件 --- App.js

import React, { Component } from 'react';

import Cpn from './Cpn'

class App extends Component {
    render() {
        return (
            <div>
                {/*
           在调用组件的时候,进行属性的传递
           把子组件需要的状态传递给子组件
        */}
                <Cpn name="Klaus" age="23" />
            </div>
        )
    }
}

export default App

子组件 --- Cpn.js

import React, { Component } from 'react';

// 子类 --- 派生类
class Cpn extends Component {
    // props 就是一个对象,上面挂载着父组件传递给子组件中的状态
    constructor(props) {
        super()
		
        // props 是 properties的简写
        this.props = props
    }

    render() {
        return (
            <div>
                <p>
                    name: { this.props.name }
                </p>
                <p>
                    age: { this.props.age }
                </p>
            </div>
        )
    }
}

export default Cpn;

但是props是每一个子组件中共有的属性,所以这个属性可以放置到其父组件上,通过继承来进行获取

import React, { Component } from 'react';

class Cpn extends Component {
    // 因为默认的构造器就是下面这种写法
    // 所以如果是以下这种写法的时候,我们一般是可以省略构造器
    // 使用默认构造器即可
    // constructor(props) {
    //   super(props)
    // }

    render() {
        return (
            <div>
                <p>
                    name: { this.props.name }
                </p>
                <p>
                    age: { this.props.age }
                </p>
            </div>
        )
    }
}

export default Cpn;
函数组件的父传子
import React, { Component } from 'react';

function Cpn(props) {
    // 函数组件主要直接使用props即可
    const {name, age} = props

    // 函数组件不需要实现render函数
    // 但是需要返回JSX对象
    return (
        <div>
            <h2>name: {name}</h2>
            <h3>age: {age}</h3>
        </div>
    )
}

class App extends Component {
    render() {
        return (
            <div>
                <Cpn name="Klaus" age="23" />
            </div>
        )
    }
}

export default App

5.3 类型校验和默认值

对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:

  • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证 ---- 复杂的验证和检验
  • 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证 --- 简单的校验和验证

React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types

也就是说原本的类型校验库是集成在React对象中的,但是自 React v15.5开始类型校验被抽取成为

一个独立的库, 叫做prop-types

prop-types可以校验的类型如下:

dbYqL4.png

需要注意的是 boolfunc是简写,不是全写

prop-types是可以自定义自己需要校验的规则的,但是如果需要进行复杂验证的话还是推荐使用Flow和TS来进行类型的规范

函数组件中的类型校验和默认值

import React, { Component } from 'react';
import PropTypes from 'prop-types'

function Cpn(props) {
    const {name, age} = props

    return (
        <div>
            <h2>name: {name}</h2>
            <h3>age: {age}</h3>
        </div>
    )
}

// 注意类型校验和默认值是写在类上边的,不是写在类的实例上的
// 注意这个propTypes的首字母是小写的
Cpn.propTypes = {
    // ProtoTypes对传入的数据类型进行校验,如果不符合校验会在控制台输出警告

    // 这里表示name必须传入一个字符串
    name: PropTypes.string,
    // age必须是数字类型,且必须传递
    age: PropTypes.number.isRequired
}

// 这里设置props的默认值
Cpn.defaultProps = {
    // 如果设置了默认值
    // 如果父组件没有传入的时候,其会被作为默认值进行传入
    // 所以此时在propTypes中对name使用isRequried就没有任何意义了
    // 因为去任何情况下都是有值的
    name: 'Klaus'
}

class App extends Component {
    render() {
        return (
            <div>
                <Cpn age={23} />
            </div>
        )
    }
}

export default App

类组件中的函数校验和默认值

父组件

import React, { Component } from 'react';

import Cpn from './Cpn'

class App extends Component {
    render() {
        return (
            <div>
                {/* 
          在jsx中如果传值必须使用字符串或者使用大括号
          因为大括号中可以传递数字和表达式
          所以如果传递数字可以使用{23}
          传递数组可以使用 {[ 'Klaus', 'Alice ]}
        */}
                <Cpn age={23} />
            </div>
        )
    }
}

export default App

子组件

import React, { Component } from 'react';
import PropTypes from 'prop-types'


class Cpn extends Component {
    // static表示的是静态属性(类属性)
    // 是class fields的一种写法
    static propTypes= {
        name: PropTypes.string,
        age: PropTypes.number.isRequired
    }

static defaultProps = {
    name: 'Klaus'
}

render() {
    return (
        <div>
            <p>
                name: { this.props.name }
            </p>
            <p>
                age: { this.props.age }
            </p>
        </div>
    )
}
}

export default Cpn;

补充

npx

  1. 避免一些临时使用,但是需要全局安装npm工具

  2. 默认情况下node_modules下的command工具是无法直接调用的,在没有被全局安装之前,我们无法在path环境变量下找到该命令,所以需要到node_modules对应的目录下使用对应命令,但是在该命令前加上npx就可以帮助我们去调用并执行对应的命令

    npx会先在path中进行查找,没有去当前node_modules中进行查找,如果依旧没有就会去npm上下载到一个临时的目录中,在使用完毕以后,这个目录会被移除

  3. 方法和函数的区别

    方法 --- 定义在类中的函数,内部存在this,执行调用的哪一个实例对象

    函数 --- 直接调用的函数,一般直接调用,不关心内部的this指向

    在JS中 函数方法,并没有本质的区别,因为在JS中定义的函数本质上也是window对象上的一个方法

  4. 类继承的本质

    class Person {
        constructor(name, age) {
            this.name = name
            this.age = age
        }
    }
    
    class Teacher extends Person {
        constructor(name, age, sno) {
            super(name, age)
            this.sno = sno
        }
    }
    
    
    const teacher = new Teacher('klaus', 23, 1810166)
    const teacher2 = new Teacher('AAAA', 23, 1010166)
    
    console.log(teacher)
    console.log(teacher2)
    

    d7HMOU.png

    可以看到的是nameage属性是定义在其父类上的,但是其却在每一个子类中出现了,这是为什么那,经过Babel编译后的代码中显示如下

    var Teacher = /*#__PURE__*/function (_Person) {
        _inherits(Teacher, _Person);
    
        var _super = _createSuper(Teacher);
    
        function Teacher(name, age, sno) {
            var _this;
    
            _classCallCheck(this, Teacher);
    		
            // 可以看到的是 虽然name和age是所有子类共有的属性,但是他们的值是不一样的
            // 所以其本质是在调用父类的构造方法的时候,使用call来改变this指向为子类
           // 也就是说 其虽然是调用了父类的构造方法,但是其this本质上是子类的实例对象
           // name和age属性,其本质上是挂载在Teacher的实例对象上的
            _this = _super.call(this, name, age);
            _this.sno = sno;
            return _this;
        }
    
        return Teacher;
    }(Person);
    

    上一篇 React脚手架 下一篇 React组件化开发(下)