React组件化开发

认识组件化

1.组件化思想

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

    • 将复杂的问题进行拆解, 拆分成很多个可以处理的小问题
    • 将其放在整体当中,你会发现大的问题也会迎刃而解

  • 其实上面的思想就是分而治之的思想:

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

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

  • 组件化也是类似的思想:

    • 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展
    • 但如果我们将一个页面拆分成一个个小的功能模块, 每个功能完成自己这部分独立功能, 那么整个页面的管理和维护变得非常容易

React组件化开发

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

    • 我们将一个完整的页面分成很多个组件
    • 每个组件都用于实现页面的一个功能块

3.React的组件化

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

    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
    • 任何的应用都会被抽象成一颗组件树

  • 组件化思想的应用:

    • 尽可能的将页面拆分成一个个小的、可复用的组件
    • 这样让我们的代码更加方便组织和管理,并且扩展性也更强

4.React组件分类

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

    • 根据组件的定义方式,可以分为:函数组件(Functional Component)和类组件(Class Component)
    • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)
    • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
  • 这些概念有很多重叠,但是它们最主要是关注数据逻辑UI展示的分离:

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

React 创建组件

1.类组件

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

    • 组件的名称是大写字符开头 (无论类组件还是函数组件)
    • 类组件需要继承自: React.Component
    • 类组件必须实现 render 函数
  • 使用 class 定义一个组件:

    • constructor是可选的,我们通常在 constructor 中初始化一些数据
    • this.state中维护的就是我们组件内部的数据
    • render() 方法是 class 组件中唯一必须实现的方法

2.render函数的返回值

render函数被调用时, 它会检查 this.propsthis.state 的变化并返回 以下类型之一
  • React元素

    • 通常通过 JSX 创建
    • 例如:
      会被 React 渲染为 DOM 节点, \会被 React 渲染为自定义组件
    • 无论是
      还是 均为 React 元素
  • 数组或 fragments: 使得 render 方法可以返回多个元素
  • Portals: 可以渲染子节点到不同的 DOM 子树中
  • 字符串或数值类型: 他们在 DOM 中会被渲染为文本节点
  • 布尔类型或null: 什么都不渲染

3.函数组件

函数组件是使用 function 来进行定义的函数, 只是这个函数会返回和类组件中 render 函数一样的内容
  • 函数组件的特点 (后面会讲hooks, 就不一样了)

    • 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数
    • 没有 this (组件实例)
    • 没有内部状态 (state)

React 生命周期

1.认识生命周期

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

React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能

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

    • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 比如更新阶段(Update),组件状态发生变化,重新更新渲染的过程
    • 比如卸载过程(Unmount),组件从DOM树中被移除的过程
  • React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的预定函数进行回调这些函数就是生命周期函数

    • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
    • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
    • 比如实现componentWillUnmount函数:组件卸载及销毁之前,就会回调
  • 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数

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

2.生命周期解析

  • 我们先来学习一下最基础、最常用的生命周期函数:

react生命周期

3.生命周期函数应用场景

Constructor

  • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
  • constructor 中通常只做两件事情:

    • 通过给 this.state 赋值对象来初始化内部 state
    • 为事件绑定this

componentDidMount

  • componentDidMount() 会在组件挂载后 ( 插入DOM树中 ) 立即调用
  • componentDidMount()中通常进行哪些操作?

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

componentDidUpdate

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

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

componentWillUnmount

  • componentWillUnmount 会在组件卸载及销毁之前调用

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

3.不常用的生命周期函数

除了上面介绍的生命周期函数之外,还有一些 不常用的生命周期函数
  • getDerivedStateFromProps

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

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

React 组件的嵌套

1.认识组件的嵌套

  • 组件之间存在嵌套关系:

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

module

  • 上面的嵌套逻辑如下

    • App组件是Header、Main、Footer组件的父组件
    • Main组件是Banner、ProductList组件的父组件

2.认识组件间的通信

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

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

React 父子组件间通信

1.父传子组件-props

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

  • 父组件通过 属性=值 的形式给子组件传递数据
  • 子组件通过 props 参数获取父组件传递过来的数据


函数组件传递Props

2.属性验证-propTypes

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

    • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证
    • 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-type 库来进行参数验证
  • 使用 propTypes 来进行对 props 的验证, 首先: 导入 prop-types
  • 我们这里只做的 props 类型的校验props 的默认值 (更多的验证方式可以参考官网)

    • 比如某个 props 属性是必须传递的使用: propTypes.string.isRequired
  • 如果没有传递props, 我们希望有默认值使用: 类名.defaultProps = {}
// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'

// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
  // name属性是必传的
  name: propTypes.string.isRequired,
  age: propTypes.number,
  height: propTypes.number,
}

// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
  name: 'hali',
  age: 21,
  height: 1.77,
}

3.子组件传递父组件-函数传递

  • 当子组件需要向父组件传递消息:

    • vue 中是通过自定义事件来完成的
    • React 中同样还是通过 props 传递消息
    • 只是让父组件给子组件传递一个回调函数(callback),在子组件调用这个函数

      • 注意 this 绑定问题
// 父组件
render() {
  return (
    

当前计数: {this.state.counter}

{/* 子传父: 让子组件来调用父组件中的方法 */} {/* 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 */} this.increment()} />
) } increment() { this.setState({ counter: this.state.counter + 1, }) } // 子组件 class Counter extends Component { render() {// 调用父组件传递的函数 return } }

React 非父子组件通信

1.Context介绍

非父子组件数据的共享:

在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递

但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)

如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

  • 如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

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

2.Context使用

React.createContext

  • 作用: 创建一个需要全局共享对象的数据 (需要跨组件间通信的数据)
  • 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider那么就使用默认值

Context.Provider

  • 介绍:每个 Context 对象都会返回一个Provider React 组件,它允许消费组件订阅 context 的变化
  • 传递value:Provider 接收一个 value 属性,传递给消费组件
  • 一个 Provider 可以和多个消费组件有对应关系
  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
  • Providervalue 值发生变化时,它内部的所有消费组件都会重新渲

Class.contextType

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

Context.Consumer

  • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件完成订阅 context
  • 这里需要 函数作为子元素(function as child)这种做法
  • 这个函数接收当前的 context 值,返回一个 React 节点

3.组件通信图示

react组件通信

React 通信补充

1.React slot

slot翻译为插槽: 在 React 中没有插槽概念, 因为 React 太灵活了, 那在React如何实现插槽功能呢?

使用过 Vue 的朋友知道, 如果我们向一个组件中插入内容时, 可以在子组件中预留插槽, 内容由父组件决定

Children 实现插槽功能

父组件在使用子组件时, 将需要展示的内容用子组件包裹起来

子组件中通过: props.children[0] 来取得父组件要展示的内容


理解

前面我们讲过:


  • render 函数中 return 的JSX 代码最终会转换成 React.createElement('tabName', 'config', children)

  • 而第三个参数 children 就是: 咱们在元素中插入的子元素, 在React源码中将 ChildrenArr放到了props属性中的children中, 所以说子组件可以通过props.children来接收到父组件插入的子元素

  • props 实现具名插槽

    在插入插槽内容时, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)

    在React中可以通过 属性(props) 来指定你要插入的内容, 然后在子组件中使用 props 取出指定的JSX元素插入到指定位置

    总结

    • Children使用场景: 当只有一个默认内容时, 直接插入到子元素即可
    • props指定slot使用场景: 当有多个插槽内容时, 使用 props 形式传递

    2.属性展开

    如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象
    function Profile(props) {
      return (
        
    {/* 在 JSX 中传递整个 props 对象。以下两个组件是等价的 */}
    ) }

    events 事件总线

    events

    • 前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?

      • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作
      • 在React中,我们可以依赖一个使用较多的 events 来完成对应的操作
    • 我们可以通过npm或者yarn来安装events:yarn add events
    • events常用的API:

      • 创建 EventEmitter 对象:eventBus对象;
      • 发出事件:eventBus.emit("事件名称", 参数列表);
      • 监听事件:eventBus.addListener("事件名称", 监听函数);
      • 移除事件:eventBus.removeListener("事件名称", 监听函数);
    // 1.创建全局事件总线
    const eventBus = new EventEmitter()
    
    // 2.发射事件
    emitHomeEvent() {
      eventBus.emit('sayHello', 'hello home', 123)
    }
    
    // 3.监听事件
    componentDidMount() {
      eventBus.addListener('sayHello', this.handleSayHelloListener)
    }
    
    // 4.卸载事件
    componentWillUnmount() {
      eventBus.removeListener('sayHello', this.handleSayHelloListener)
    }

    refs

    1.如何使用ref

    React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;
    • 如何创建refs来获取对应的DOM呢?目前有三种方式:
    • 方式一:传入字符串

      • 使用时通过 this.refs.传入的字符串格式获取对应的元素
    • 方式二:传入一个对象

      • 对象是通过 React.createRef() 方式创建出来的;
      • 使用时获取到创建的对象其中有一个current属性就是对应的元素
    • 方式三:传入一个函数

      • 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
      • 使用时,直接拿到之前保存的元素对象即可
    import React, { PureComponent, createRef } from 'react'
    // ...
    constructor(props) {
      super(props)
      this.titleRef = createRef()
      this.titleEl = null
    }
    render() {
      return (
        
    {/*

    hello react

    */}

    hello react

    hello react

    (this.titleEl = arg)}>hello react

    ) } changeText() { // 1.通过refs来操作DOM,有三种方式 // 方式一: 字符串 this.refs.titleRef.innerHTML = 'hello jean' // 方式二: 对象 this.titleRef.current.innerHTML = 'hello JavaScript' // 方式三: 函数 this.titleEl.innerHTML = 'hello TypeScript' }

    2.ref的类型

    • ref 的值根据节点的类型而有所不同:
    • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
    • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
    • 你不能在函数组件上使用 ref 属性,因为他们没有实例
    constructor(props) {
      this.counterRef = createRef()
    }
    
    
    render() {
      return (
        
    ) } // 通过ref来获取类组件对象 appIncrementCount() { // 调用子组件方法 this.counterRef.current.increment() }
    函数式组件是没有实例的,所以无法通过ref获取他们的实例:
    但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
    这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;

    受控组件与非受控组件

    1.认识受控组件

    • 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state
    • 比如下面的HTML表单元素:

      • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
      • 在React中,并没有禁止这个行为,它依然是有效的;
      • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
      • 实现这种效果的标准方式是使用“受控组件”;

    2.受控组件基本演练

    • 在 HTML 中,表单元素(如\、 \