Skip to content

一. 组件化开发


1.什么是组件化开发呢?

  • 组件化是一种分而治之的思想:
    • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展
    • 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
  • 我们需要通过组件化的思想来思考整个应用程序:
    • 我们将一个完整的页面分成很多个组件
    • 每个组件都用于实现页面的一个功能块
    • 而每一个组件又可以进行细分
    • 而组件本身又可以在多个地方进行复用
image-20220905192227005

2. React的组件化

  • 组件化是React的核心思想,前面我们封装的App本身就是一个组件:
    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
    • 任何的应用都会被抽象成一颗组件树
image-20220905192327837
  • 组件化思想的应用:
    • 有了组件化的思想,我们在之后的开发中就要充分的利用它
    • 尽可能的将页面拆分成一个个小的、可复用的组件
    • 这样让我们的代码更加方便组织和管理,并且扩展性也更强
  • React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:
    • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component)
    • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component)
    • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
  • 这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:
    • 函数组件、无状态组件、展示型组件主要关注UI的展示
    • 类组件、有状态组件、容器型组件主要关注数据逻辑
  • 当然组件还有很多的其他概念:比如异步组件、高阶组件等

3.类组件

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

    • 组件名称是 大写字符开头(无论类组件还是函数组件)
    • class组件需 继承自React.Component
    • class组件需 实现render函数
  • ES6之前,可以通过create-react-class 模块来定义类组件,但是目前官网建议我们使用ES6class类定义

  • 使用class定义一个组件:

    • constructor是可选的,我们通常在constructor中初始化一些数据
    • this.state中维护的就是我们组件内部的数据
    • render() 方法是class组件中唯一必须实现的方法
    jsx
    // 写法一: 内部有对Component单独导出
    import { Component } from 'react'
    
    class App extends Component {
      render() {
        return <h2></h2>
      }
    }
    
    // 写法二
    import React from 'react'
    
    class App extends React.Component {
      render() {
        return <h2></h2>
      }
    }

4.render函数

  • render()方法执行时机:
    • 组件第一次渲染的时候
    • this.props变化时
    • this.state变化时
  • render()方法的返回值
    • render被调用时,它会检查this.propsthis.state的变化并返回以下类型之一:
    • React 元素
      • 通常通过jsx创建出来的,都叫做React元素,因为一般jsx的代码最终都会转换为React.createElement的代码,而React.createElement就是在创建React元素
      • 例如,<div /> 会被React渲染为DOM节点,<MyComponent /> 会被React渲染为自定义组件
      • 无论是<div />还是<MyComponent />均为React元素
    • Array、fragments:使得render方法可以返回多个元素
    • Portals:可以渲染子节点到不同的DOM子树中
    • String、Number:它们在DOM中会被渲染为文本节点
    • **Boolean、null、undefined **:什么都不渲染

5.函数组件

  • 函数组件是使用function来进行定义的函数,不需要继承自React.Component,只是这个函数会返回和类组件中render函数返回一样的内容

  • 函数组件有自己的特点(当然,后面用hooks,就不一样了):

    • 没有生命周期函数,也会被更新并挂载
    • this关键字不能指向组件实例(因为没有组件实例)
    • 没有内部状态state
  • 定义一个函数组件:

    jsx
    function App() {
      const state = { // 没有内部state,每次执行函数的时候都会重新初始化state,所以这个state是无法维护下去的
        msg: 'hhh'
      }
      return <h2></h2>
    }

二. 组件生命周期


1.认识生命周期

  • 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期
  • React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能
  • 生命周期和生命周期函数的关系:
  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段
    • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程
    • 比如卸载过程(Unmount),组件从DOM树中被移除的过程
  • React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
    • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
    • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
    • 比如实现componentWillUnmount函数:组件即将被移除时,就会回调
    • 我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能
  • 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的(函数式组件也可以通过hooks来模拟一些生命周期的回调)

2.生命周期解析

  • 最基础、最常用的生命周期函数:
image-20220905204448218
  • Constructor

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

    • constructor中通常只做两件事情:

      • 通过给this.state赋值对象来初始化内部的state

      • 为事件绑定实例(this

        image-20220905210048909
  • componentDidMount

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

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

      • 依赖于DOM的操作可以在这里进行

      • 在此处发送网络请求就最好的地方(官方建议)

      • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)比如事件总线

        image-20220905210109190
  • componentDidUpdate

    • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法
    • 当组件更新后,可以在此处对DOM进行操作
    • 如果你对更新前后的props 进行了比较,也可以选择在此处进行网络请求(例如,当props未发生变化时,则不会执行网络请求)
  • componentWillUnmount

    • componentWillUnmount() 会在组件卸载及销毁之前直接调用
      • 在此方法中执行必要的清理操作
      • 例如,清除timer,取消网络请求或清除在componentDidMount()中创建的订阅等
  • 除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:

    • getDerivedStateFromPropsstate 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state

    • getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置)

      jsx
      getSnapshotBeforeUpdate() {
        return { scrollPosition: 100 }
      }
      
      componentDidUpdate(preProps, preState, snapshot) {
        // 可以在 componentDidUpdate 中的 snapshot 中接收到 getSnapshotBeforeUpdate 的返回值
      }
    • shouldComponentUpdate:该生命周期函数很常用,可以用来做性能优化

      jsx
      /*
        返回 false 的时候,当组件状态变化时,不会重新执行 render 函数,
        在执行 render 函数前,会判断 shouldComponentUpdate 的返回值,默认是 true
      */
      shouldComponentUpdate() {
        return false 
      }
  • 另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用

  • 更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html

image-20220905211013999

三. 组件间通信


1.认识组件的嵌套

  • 组件之间存在嵌套关系:
    • 在之前的案例中,我们只是创建了一个组件App
    • 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护
    • 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件
    • 再将这些组件组合嵌套在一起,最终形成我们的应用程序
image-20220905211309666
  • 上面的嵌套逻辑如下,它们存在如下关系:
    • App组件是Header、Main、Footer组件的父组件
    • Main组件是Banner、ProductList组件的父组件

2.认识组件间的通信

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

    • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示
    • 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示
    • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件
  • 总之,在一个React项目中,组件之间的通信是非常重要的环节

  • 父组件在展示子组件,可能会传递一些数据给子组件:

    • 父组件通过属性=值的形式来传递给子组件数据
    • 子组件通过props参数获取父组件传递过来的数据
      • 内部自动把props保存到当前子组件实例中,等价于this.props = props
    jsx
    class App extends Component {
      constructor(props) {
        console.log(props) // { msg: 'aaa' }
        super(props) 
        // 内部有做一个操作:this.props = props
      }
    
      render() {
        const { msg } = this.props // 省略constructor不写,this.props也是可以获取到传入进来的数据
        console.log(msg) // aaa
        // ... 
      }
    }
    
    <div>
      <App msg="aaa"></App>
    </div>
  • 函数式

    jsx
    function App(props) {
      console.log(props) // { msg: 'aaa' }
      const { msg } = props
      console.log(msg) // 'aaa'
    
      return (
        // ...
      )
    }
    
    <div>
      <App msg="aaa"></App>
    </div>

3.参数propTypes

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

    • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证
    • 但是,即使我们没有使用Flow或者TypeScript,也可以通过prop-types库来进行参数验证
  • React v15.5开始,React.PropTypes 已移入另一个包中:prop-types 库,方便进行tree-shaking,因为你不引用的话,就会被删除掉这部分

    jsx
    import PropTypes from 'prop-types'
    
    Cpn.PropTypes = {
      name: PropTypes.string,
      age: PropType.number
    }
  • 更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

    • 比如验证数组,并且数组中包含哪些元素
    • 比如验证对象,并且对象中包含哪些key以及value是什么类型
    • 比如某个属性是必须的,使用requiredFunc: PropTypes.func.isRequired
  • 如果没有传递,我们希望有默认值呢?

    • 我们使用defaultProps就可以了
    jsx
    Cpn.defaultProps = {
      name: 'later',
      age: 18
    }

4.子组件传递父组件

  • 某些情况,我们也需要子组件向父组件传递消息:

    • vue中是通过自定义事件来完成的
    • React中同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
  • 我们这里来完成一个案例:

    image-20220906170020478
    • 将计数器案例进行拆解
    • 将按钮封装到子组件中:AddCount、SubtractCount
      • 子组件发生点击事件,将内容传递到父组件中,修改父组件中的counter的值
image-20220906165622919

5.组件通信案例练习

  • 效果图

    image-20220906184451056
  • 代码

image-20220906184737433

四. 组件插槽用法


1.React中的插槽

  • 在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素
  • 我们应该让使用者可以决定某一块区域到底存放什么内容
  • 这种需求在Vue当中有一个固定的做法是通过slot来完成的,React呢?
  • React对于这种需要插槽的情况非常灵活,有两种方案可以实现:
    • 组件的children子元素
    • props属性传递React元素

2.children实现插槽

  • 每个组件都可以获取到** props.children:它包含组件的开始标签和结束标签之间的内容**
  • 组件内部多个子元素的话,props.children获取到的是一个数组,里面包含的是组件内部的子元素
image-20220906190239710
  • 如果组件内部只有一个子元素的话,props.children获取到的就是那一个子元素(源码中的处理)

    jsx
    	class NavBar extends Component {
            render() {
                const { children } = this.props
                console.log(children) // 不是一个数组,而是类名为aaa的元素
                return (
                	
                )
            }
        }
    
    	<NavBar>
    		<div className="aaa">
        		<div className="bbb"></div>
        	</div>
    	</NavBar>
  • 可以通过propTypes限制组件内部传入的元素个数,如果别人传入的类型不符合要求,就会报错

    jsx
    	import PropTypes from 'prop-types'
    
    	NavBar.propTypes = {
            children: PropTypes.element, // 限制组件内部children值的类型是React元素,传一个子元素的话children就是传入的单个元素
            children: PropTypes.array // 限制组件内部children值的类型是数组,传多个子元素的话children才是数组
        }

3.props实现插槽

  • 通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生
  • 另外一个种方案就是使用props实现:
    • 通过具体的属性名,可以让我们在传入和获取时更加的精准
image-20220906195613940

4.通过props实现作用域插槽

  • 通过子组件调用父组件传入的属性中的回调函数,父组件中通过该回调函数调用所传入的参数item,来决定返回什么内容在子组件中显示
  • 组件内部引用的样式是全局的,而不是局部的
image-20220906200232451

五. 非父子间组件通信


1.Context应用场景

  • 非父子组件数据的共享

    • 在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递
    • 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)
    • 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作
  • 我们实现一个一层层传递的案例:

    • 顺便补充一个小的知识点:Spread Attributes
    jsx
    	class App extends Component {
            constructor() {
                super()
                this.state = {
                    info: { name: 'later', age: 18}
                }
            }
            render() {
                return (
                    <TabBar name={ this.state.info.name} age={ this.state.info.age } ></TabBar>
                    { /* 等价于上面写法 */ }
                	<TabBar { ...info }></TabBar>
                )
            }
        }
    
    	class TabBar extends Component {
            render() {
                return <TabBarItem { ...this.props }></TabBarItem>
            }
        }
    
        class TabBar extends Component {
            render() {
                return <div { ...this.props }></div>
            }
        }
  • 但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

    • React提供了一个APIContext
    • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
    • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

2.Context相关API

  • React.createContext

    • 创建一个需要共享的Context对象
    • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的context
    • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
    jsx
    	const MyContext = React.createContext(defaultValue)
  • Context.Provider

    • 每个Context对象都会返回一个Provider React组件,它允许消费组件订阅context的变化
    • Provider 接收一个value属性,传递给消费组件
    • 一个Provider可以和多个消费组件有对应关系
    • 多个Provider也可以嵌套使用,里层的会覆盖外层的数据
    • Providervalue值发生变化时,它内部的所有消费组件都会重新渲染
    jsx
    	<MyContext.Provider value={ /* 某个值*/}
  • Class.contextType

    • 挂载在class上的contextType属性会被重赋值为一个由React.createContext()创建的Context对象:
    • 这能让你使用**this.context来消费离组件最近Context上的那个值**
    • 你可以在任何生命周期中访问到它,包括render函数中
    • 函数式组件中没有contextType
    jsx
    	MyClass.contextType = MyContext
  • Context.Consumer

    • 这里,React 组件也可以订阅到context变更。这能让你在 函数式组件 中完成订阅 context
    • 这里需要 函数作为子元素(function as child)这种做法
    • 这个函数接收当前的context值,返回一个React节点
    jsx
    <MyContext.Consumer>
    {
      value => /* 基于 context 值进行渲染 */
    }
    </MyContext.Consumer>

3.Context代码演练

  • Context的基本使用

    • 什么时候使用默认值defaultValue呢?
      • Home组件不被UserContext所包裹,却想访问UserContext中的数据时,可以访问到UserContext初始化传入的defaultValue中的数据(有传的话)
    jsx
    	<div>
    		<Home/>
        	<UserContext.Provider value={ {name: 'later', age: 18} }></UserContext.Provider>
    	</div>
  • 什么时候使用Context.Consumer呢?

    • 1.当使用value的组件是一个函数式组件时
    • 2.当组件中需要使用多个Context
image-20220906214113419

六. setState的使用详解


1.为什么使用setState

  • 开发中我们并不能直接通过修改state的值来让界面发生更新:
    • 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化
    • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化
    • 我们必须通过setState来告知React数据已经发生了变化
  • 疑惑:在组件中并没有实现setState的方法,为什么可以调用呢?
    • 原因很简单,setState方法是从Component中继承过来的
image-20220907161051992
  • setState用法

    jsx
        changeText() {
           // 1.基本使用
           this.setState({
              message: "你好啊, 李银河"
           })
    
           // 2.setState可以传入一个回调函数
           // 好处一: 可以在回调函数中编写新的state的逻辑
           // 好处二: 当前的回调函数会将之前的state和props传递进来
           this.setState((state, props) => {
              // 1.编写一些对新的state处理逻辑
             // 2.可以获取之前的state和props值
              console.log(this.state.message, this.props)
              return {
                 message: "你好啊, 李银河"
              }
           })
    
           // 3.setState在React的事件处理中是一个异步调用
           // 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
           // 那么可以在setState中传入第二个参数: callback
           this.setState({ message: "你好啊, 李银河" }, () => {
              console.log("++++++:", this.state.message)
           })
           console.log("------:", this.state.message)
        }

2 .setState异步更新

jsx
	this.state = {
        message: 'hello world'
    }
	// ...
	changeText() {
        this.setState({
            message: '你好啊'
        })
        console.log(this.state.message) // hello world
    }
  • setState的更新是异步的?
    • 最终打印结果是Hello World
    • 可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果
  • 为什么setState设计为异步呢?
    • setState设计为异步其实之前在GitHub上也有很多的讨论
    • React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的同学可以参考一下
    • https://github.com/facebook/react/issues/11527#issuecomment-360199710
  • 其回答做一个简单的总结:
    • setState设计为异步,可以显著的提升性能
      • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的
      • 最好的办法应该是获取到多个更新,之后进行批量更新
    • 如果同步更新了state,紧跟着还有很多同步代码需要执行,这时还没有立即执行render函数,那么这时如果有父组件中的state作为子组件props的值,而刚好该state有更新的话,则父组件和子组件中的数据不能保持同步
      • stateprops不能保持一致性,会在开发中产生很多的问题,像父组件中state已经更新,这时传递给子组件的props属性是该state,而render函数却还没执行,就会产生数据不一致
  • 本次更新会加入react内部的一个事件队列queue
  • 两个优势:
    • 多个update放在同一次更新中,执行一次render函数,提高性能
    • 保证在state没有被更新的时候,state/props保持一致

3.如何获取异步的结果

  • 那么如何可以获取到更新后的值呢?

  • 方式一:setState的回调

    • setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行
    • 格式如下:setState(partialState, callback)
    jsx
    	changeText() {
            this.setState({
                message: '你好啊'
            }, () => {
                console.log(this.state.message) // 你好啊
            })
        }
  • 当然,我们也可以在生命周期函数中获取

    jsx
    	componentDidUpdate(preProps, preState, snapshot) {
            console.log(this.state.message)
        }

4.setState一定是异步吗?(React18之前)

  • 验证一:在setTimeout中的更新:

    jsx
    	changeText() {
            setTimeout(() => {
                this.setState({
                    message: '你好啊'
                })
                console.log(this.state.message) // 你好啊
            }, 0)
        }
  • 验证二:原生DOM事件:

    jsx
    	componentDidMount() {
            const btnEl = document.querySelector('.btn')
            btnEl.addEventListener('click', () => {
                this.setState({
                    message: '你好啊'
                })
                console.log(this.state.message) // 你好啊
            })
        }
  • React18之前,其实分成两种情况:

    • 在组件生命周期或React合成事件中,setState是异步(批处理)
    • setTimeoutPromise.then回调原生dom事件中,setState是同步

5.setState默认是异步的(React18之后)

  • React18之后,默认所有的操作都被放到了批处理中(异步处理)
image-20220907212203267
  • 如果希望代码可以同步拿到,则需要执行特殊的flushSync操作:

    jsx
    	import { flushSync } from 'react-dom'
    
         // 在react18之前, setTimeout中setState操作, 是同步操作
         // 在react18之后, setTimeout中setState异步操作(批处理)
    	
    	// 正确使用
    	flushSync(() => {
          this.setState({ message: "你好啊, 李银河" })
        })
    	console.log(this.state.message) // 你好啊,李银河
    
    	// 错误使用
    	flushSync(() => {
          this.setState({ message: "你好啊, 李银河" })
          console.log(this.state.message)
        })

Released under the MIT License.