Skip to content

组件化开发

组件化开发

组件化是一种分而治之的思想:

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

image-20221115154858249

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

image-20221115154931744

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

  • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component)
  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component)
  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)

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

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

类组件

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

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

使用 class 定义一个组件:

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

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

  • React 元素
    • 通常通过 JSX 创建
    • <div /> 会被 React 渲染为 DOM 节点,<MyComponent /> 会被 React 渲染为自定义组件
  • 数组或 fragments
    • 使得 render 方法可以返回多个元素
  • Portals
    • 可以渲染子节点到不同的 DOM 子树中
  • 字符串或数值类型
    • 在 DOM 中会被渲染为文本节点
  • 布尔类型或 null
    • 什么都不渲染
jsx
import React from 'react'
class App extends React.Component {
  constructor() {
    super()
    this.state = {
      message: 'App Component'
    }
  }

  render() {
    const { message } = this.state
    // 1.react元素: 通过jsx编写的代码就会被编译成React.createElement,
    return <h2>{message}</h2>

    // 2.组件或者fragments(后续学习)
    return ["abc", "cba", "nba"]

    // 3.字符串/数字类型
    return "Hello World"

    // 4。布尔类型或 null
    return true
  }
}
export default App

函数式组件

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

函数组件有自己的特点(前提:没有 hooks)

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数
  • this 关键字不能指向组件实例(因为没有组件实例)
  • 没有内部状态(state)
jsx
function App(props) {
  return <h1>App Functional Component</h1>
}

export default App

生命周期

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

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

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

  • 比如实现 componentDidMount 函数:组件已经挂载到 DOM 上时,就会回调
  • 比如实现 componentDidUpdate 函数:组件已经发生了更新时,就会回调
  • 比如实现 componentWillUnmount 函数:组件即将被移除时,就会回调

image-20221115171727724

Constructor

  • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
  • constructor中通常只做两件事情:
    • 通过给 this.state 赋值对象来初始化内部的state
    • 为事件绑定实例(this)

componentDidUpdate

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

componentWillUnmount

  • componentWillUnmount() 会在组件卸载及销毁之前直接调用
    • 在此方法中执行必要的清理操作
    • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等

还有一些不常用的生命周期函数:

image-20221115171621147

jsx
import React from 'react'

class HelloWorld extends React.Component {
  // 1.构造方法: constructor
  constructor() {
    console.log('constructor')
    super()
    this.state = {
      message: 'Hello World'
    }
  }
  changeText() {
    this.setState({ message: '你好啊, 李银河' })
  }

  // 2.执行render函数
  render() {
    console.log('render')
    const { message } = this.state

    return (
      <div>
        <h2>{message}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
      </div>
    )
  }

  // 3.组件被渲染到DOM: 被挂载到DOM
  componentDidMount() {
    console.log('componentDidMount')
  }

  // 4.组件的DOM被更新完成: DOM发生更新
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('componentDidUpdate:', prevProps, prevState, snapshot)
  }

  // 5.组件从DOM中卸载掉: 从DOM移除掉
  componentWillUnmount() {
    console.log('componentWillUnmount')
  }

  // 不常用的生命周期补充
  shouldComponentUpdate() {
    return true
  }

  getSnapshotBeforeUpdate() {
    console.log('getSnapshotBeforeUpdate')
    return {
      scrollPosition: 1000
    }
  }
}

export default HelloWorld

组件间通信

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

image-20221115172033726

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

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

image-20221115173801220

参数 propTypes

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

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

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

更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

  • 比如验证数组,并且数组中包含哪些元素
  • 比如验证对象,并且对象中包含哪些 key 以及 value 是什么类型
  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired

如果没有传递,我们希望有默认值,使用 defaultProps 即可

jsx
class MainBanner extends React.Component { }

// MainBanner 传入的 props 类型进行验证
MainBanner.propTypes = {
  banners: PropTypes.array,
  title: PropTypes.string
}
// MainBanner 传入的 props 的默认值
MainBanner.defaultProps = {
  banners: [],
  title: "默认标题"
}

从 ES2022 开始,你也可以在 React 类组件中将 defaultProps 声明为静态属性

jsx
class MainBanner extends React.Component {
  static defaultProps = {
    banners: [],
    title: "默认标题"
  }
}

子传父

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

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

image-20221115180526781

插槽

在开发中,我们抽取了一个组件,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的 div、span 等等这些元素

  • 这种需求在 Vue 当中有一个固定的做法是通过 slot 来完成的,React 呢?
  • React 对于这种需要插槽的情况非常灵活,有两种方案可以实现:
    • 组件的 children 子元素
    • props 属性传递 React 元素

每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容

  • 如果传入多个 children,props 里的 children 就是数组
  • 如果只传入一个 children,props 里的 children 就是那一个 children(是 React 里的一个 element)

image-20221116091004016

image-20221116091224653

通过 children 实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生

另外一个种方案就是使用 props 实现

image-20221116092207710

image-20221116092150864

Context

非父子组件数据的共享:

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

这里补充一个知识点 Spread Attributes

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

  • React 提供了一个 API:Context
  • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
  • Context 设计目的是为了共享那些对于一个组件树而言是全局的数据,例如当前认证的用户、主题或首选语言
jsx
// 1.创建一个Context
const ThemeContext = React.createContext({ color: 'blue', size: 10 })

// 2.通过 ThemeContext 中 Provider 中 value 属性为后代提供数据
export class App extends Component {
  render() {
    return (
      <ThemeContext.Provider value={{ color: 'red', size: '30' }}>
      	<Home {...info} />
    	</ThemeContext.Provider>
    )
  }
}

// 3.第三步操作: 设置组件的contextType为某一个Context
HomeInfo.contextType = ThemeContext

export class HomeInfo extends Component {
  render() {
    // 4.第四步操作: 获取数据, 并且使用数据
    console.log(this.context)
  }
}

函数式组件中使用 Context 共享的数据

  • 类组件多个数据共享,也可以使用 Consumer 来进行消费
jsx
function HomeBanner() {
  return (
    <div>
      <ThemeContext.Consumer>
        {value => {
          return <h2> Banner theme:{value.color}</h2>
        }}
      </ThemeContext.Consumer>
    </div>
  )
}

React.createContext

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

Context.Provider

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

Class.contextType

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

Context.Consumer

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

setState

用法

  • setState(updater)
  • setState((state, props) => updater)
  • setState(updater, callback)

image-20221117110046072

为什么使用 setState

开发中我们并不能直接通过修改 state 的值来让界面发生更新:

  • 因为我们修改了 state 之后,希望 React 根据最新的 State 来重新渲染界面,但是这种方式的修改 React 并不知道数据发生了变化
  • React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的 Proxy 的方式来监听数据的变化
  • 我们必须通过 setState 来告知 React 数据已经发生了变化

疑惑:在组件中并没有实现 setState 的方法,为什么可以调用呢?

  • 原因很简单,setState 方法是从 Component 中继承过来的

image-20221116111347098

为什么setState设计为异步呢?

setState 设计为异步,可以显著提升性能

  • 如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的
  • 最好的办法应该是获取到多个更新,之后进行批量更新

如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步

  • state 和 props 不能保持一致性,会在开发中产生很多的问题

优势:

  1. 多个 updater 放在一次更新中,执行一次 render 函数,提高性能
  2. 保证在 state 没有被更新的时候,state、props 保持一致

setState 一定是异步吗

https://zh-hans.reactjs.org/blog/2022/03/29/react-v18.html#whats-new-in-react-18

React 18 之前

  • 在 setTimeout 中的更新

    • 在 React18 之前,setTimeout 中 setState 操作,是同步操作
    jsx
    setTimeout(() => {
      this.setState({
        message: '你好,世界'
      })
      console.log(this.state.message)
    }, 0)
  • 原生 DOM 事件

    • 在组件生命周期或 React 合成事件中,setState 是异步的
    • 在 setTimeout、Promise.then 回调、原生 DOM 事件中,setState 是同步的

React18 之后

  • 在 React18 之后,默认所有的操作都放到了批处理中(异步操作)

  • 如果希望代码可以同步拿到,则需要执行特殊的 flushSync 操作

    jsx
    import { flushSync } from 'react-dom'
    flushSync(() => {
      this.setState({ message: '你好,世界' })
    })

常备不懈,才能有备无患