React 组件化开发

无论是 vue、React 还是 Angular,主流框架都支持并提倡组件化开发,因为组件化开发不仅可以增强代码的能动性和复用性,还能够加快团队协作的速度。组件化开发就像搭积木,首先把一个个积木(组件)设计好,甚至将小积木(容器组件、展示组件)组装成具备一定功能的积木(比如一个房子),最终再将功能化的积木摞成最终的成品(比如一个社区)。
本文简单介绍 React 中组件的定义,以及容器组件、展示组件、高阶组件、复合组件等常见组件的应用,并介绍组件间的通信方式。

1. 如何定义一个组件

1.1 一般组件

React 中组件的定义有两种方式,一种是使用 Class 关键字以类的形式来定义组件,另一种是使用函数方式定义。比如定义一个网站的欢迎提示组件:

  • 类定义
class WelcomeTip extends React.Component {
  render() {
    return (
        
Welcome to this website!
) } }
  • 函数定义
function WelcomeTip(props) {
  return (
    
Welcome to this website!
) }

无论使用哪一种方式定义组件,组件的调用都是一致的


但是,组件内状态管理、生命周期却有着很大的不同,本文中主要采用类定义的方式来构建组件,关于函数定义组件的应用可以移步 “React Hook” 的介绍。

  • 组件状态
class Counter extends React.Component {
  // 写了 constructor 就要调用 super
  constructor(props) {
    super(props)
    // 状态声明
    this.state = {
      count: 0
    }
  }
  // state 的调用:this.state.xxx
  // state 的修改:this.setState({count: 1}) 
  // 或者 this.setState(state => ({count: 1}))
  // 支持同时设置多个 key 值,key 值相同时后者覆盖前者
  // setState 是一个异步函数
  render() {
    return (
        

Welcome, {this.props.name}! You have click {this.state.count} times!

) } }
  • 组件的生命周期
    • 初始化:constructor ,用于完成组件的初始化工作,如定义state 的初始内容、定义组件内部变量等
    • 组件的挂载:
      • componentWillMount,发生在组件挂载到 DOM 之前,此处修改 state 不会引起组件的重新渲染。该部分的功能也可以提前到 constructor 中,因此很少在项目中使用。
      • render,根据组件的 propsstate(两者的重传递和重赋值,无论值是否有变化,都可以引起组件重新 render),返回⼀个 React 元素(描述组件,即UI),不负责组件实际渲染⼯作,之后由 React ⾃身根据此元素去渲染出⻚⾯DOM。纯函数,返回结果只依赖于传入的参数,执行过程中没有副作用。不能在该阶段执行 setState,会造成死循环。
      • componentDidMount,组件挂载到 DOM 之后调用,且只会被调用一次。
    • 组件的更新:当 propsstate 被重新赋值时,无论值是否发生改变,都会触发组件的更新。因此有如下两种情况会触发组件的更新:1. 父组件重新 render,由于子组件的 props 被传值,触发子组件的更新;2. 组件本身调用 setState,无论 state 有没有改变,组件都会更新
      • componentWillReceiveProps(nextProps)props 重传时被调用,该函数中调用 setState 不会引起组件的二次更新,因此即便在该函数中执行 this.setState 更新了stateshouldComponentUpdate componentWillUpdate 中的 this.state 依旧是原来的值
      • shouldComponentUpdate(nextProps, nextState),此⽅法通过⽐较 nextPropsnextState及当前组件的 this.propsthis.state,返回 true时当前组件将继续执⾏更新过程,返回 false 则当前组件更新停⽌,以此可⽤来减少组件的不必要渲染,优化组件性能。
      • componentWillUpdate(nextProps, nextState),此⽅法在调⽤ render ⽅法前执⾏,在这边可执⾏⼀些组件更新发⽣前的⼯作,⼀般较少⽤。
      • render :同挂载时的 render。
      • componentDidUpdate(prevProps, prevState),此⽅法在组件更新后被调⽤,可以操作组件更新的 DOM ,prevPropsprevState 这两个参数指的是组件更新前的 propsstate
    • 组件的卸载:
      • componentWillUnmount:此⽅法在组件被卸载前调⽤,可以在这⾥执⾏⼀些清理⼯作,⽐如清除组件中使⽤的定时器, componentDidMount 中⼿动创建的 DOM 元素等,以避免引起内存泄漏。
    • 【注意】componentWillMount componentWillReceivePropscomponentWillUpdate 在 React 17.x 版本之后将不再支持,目前使用会提示 warning。在 16.3 之后,使用 getDerivedStateFromProps 代替上述三个函数
      • static getDerivedStateFromProps(props, state),在组件创建时和更新时的 render ⽅法之前调⽤, 它应该返回⼀个对象来更新状态,或者返回 null 来不更新任何内容。
      • getSnapshotBeforeUpdate,被调⽤于render之后,此时可以读取但还不能操作更新 DOM ,因此可以按需调整滚动条等。 返回值(必须有)将作为参数传递给 componentDidUpdate
        React 组件化开发_第1张图片
        引自https://github.com/aermin/blog/issues/55
1.2 组件拆分——容器组件&展示组件

在涉及复杂的数据预处理时,可以考虑将组件拆分成容器组件和展示组件。其中容器组件负责请求并处理数据,展示组件负责根据 Props 显示信息。如此可以减小组件的体积,使开发人员可以跟专注于某一功能开发,并提高组件的重用性和可用性,同时易于测试和提高系统性能。

// 容器组件
class CommentList extends React.Component {
    state = {
        list: []
    }

    componentDidMount() {
        setTimeout(() => {
            this.setState({
                list: [
                    {id: 1, text: '我喜欢苹果', author: '小A'},
                    {id: 2, text: '我喜欢橙子', author: '小B'},
                    {id: 3, text: '我喜欢西瓜', author: '小C'},
                ]
            })
        })
    }

    render() {
        return (
            
{this.state.list.map(l => { return })}
) } } // 展示组件 function Item({text, author}) { return (
{text} -- {author}
) }
1.3 PureComponent

在组件生命周期中组件更新过程中,提及只要发生重新挂载,无论 props state 是否变化,都会出发更新。纯组件就是定制了 shouldComponentUpdate 后的Component,仅有依赖的数据发生变化时才进行更新。 该比较过程数据浅比较,因此对象属性或数组中元素并不适用于该特性。

// 假设父组件有 count 和 name 两个状态
// 子组件仅依赖父组件的 count
// 如果子组件继承的是 React.Component,那么父组件 name 值发生变更时,子组件依旧会重新 render
// 继承的是 React.PureComponent 时,则仅有父组件的 count 值变化时,子组件才会重新调用 render 
class Child extends React.PureComponent {
  render() {
    return 
{this.props.count}
} }

React 16.6.0 之后,使用 React.memo 让函数式的组件也有 PureComponent 的功能

const Child = React.memo(() => {
  return 
{this.props.count}
})

2. 高阶组件是什么

2.1 高阶组件与一般组件有什么不同

高阶组件是 React 中重用组件逻辑的高级技术,它不是 React 的 api ,而是一种组件增强模式。高阶组件是一个函数,它返回另外一个组件,产生新的组件可以对被包装组件属性进行包装,也可以重写部分生命周期。

高阶组件可以为组件添加某一特殊功能,也可以多层嵌套,赋予被包装组件多个功能。比如打印日志功能、添加标题功能等。

// 包装后的组件具备日志打印功能
const withLog = Component => { 
    class newComponent extends React.Component {
        componentDidMount() {
            console.log(`${Date.now()}:组件已挂载`)
        }
        render() {
            return 
        }
    }
    return newComponent
}

// 包装后的组件都带有一个标题
const withTitle = Component => {
    const newComponent = props => {
        return (
            

这是一个标题


) } return newComponent }
2.2 高阶组件怎么使用
  1. 链式调用

高阶组件本质上就是一个函数,因此可以采用链式调用的形式,将待包装的组件作为参数传入,并 export 出去即可。同时也可以多个高阶组件嵌套,一层层包装单一组件。

export default withLog(withTitle(CommentList))
  1. 装饰者模式

ES7 中提供了装饰者模式的写法,可以使代码更加简洁,但需要进行相关配置:

  • 暴露项目的所有配置项:npm run eject

  • 安装:npm install -D @babel/plugin-proposal-decorators

  • 配置 package.json 文件中 babel 配置项

      "babel": {
        "presets": [
          "react-app"
        ],
        "plugins": [
          ["@babel/plugin-proposal-decorators", {"legacy": true}]
        ]
      }
    

如此,上述链式调用可以修改为:

export default 
@withLog
@withTitle
class CommentList extends React.Component {
  ...
}

3. 复合组件

复合组件可以让开发者以更便捷地创建组件的外观和行为,相比继承更加直观和安全。

// 容器不关心内容与逻辑
// 3. 容器中可以使用 children,但由于传入的是 vdom 数组,故而不能修改
function Dialog(props) {
  return (
{React.Children.map(props.children, child => child.type === 'p' ? child : null)} {props.footer}
) } // 通过复合提供内容 function HelloDialog(props) { // 1. 参数可以使用 props 传入 // 2. 可以传入任何表达式 return (版权归 road 所有

}>

你好啊,{props.name}

感谢访问本网站

) }

4. 组件间如何实现通信

4.1 父传子

通过 props 将参数传递给子组件,使用 class 关键字以类方式定义组件时,使用 this.props 即可以父组件传递的所有参数,函数方式定义时则需要在声明时添加 props 参数,或解构参数。

// 类方式定义
class Child extends React.Component {
  render() {
    return (
子组件:{this.props.name}
) } } // 函数方式定义 function Child(props) { return (
子组件:{props.name}
) } // 函数方式 function Child({name}) { return (
子组件:{name}
) }

父组件传参:


4.2 子传父

父组件中声明一个相关方法,并作为参数传递给子组件。子组件通过调用父组件传递过来的方法,修改父组件中的数据。

// 比如:父组件中有个计数值,子组件中的按钮点击之后计数值 +1
function Child({increase, step}) {
    return (
        
); } export class Parent extends Component { state = { count: 0 } add(step) { this.setState(state => ({count: state.count + step})) } render() { return (
计数值为 {this.state.count} {/* 注意方法传递过程中 this 的指向变更 */}
); } }
4.3 跨组件通信

跨组件通信有兄弟组件通信、父组件与孙组件的通信等,从上到下的数据传递可以通过 props 一层层传递,但从下到上的数据传递则十分麻烦。例如下图中【子组件1】相与【父组件B】通信时,就需要将信息一层层冒到祖先组件中,再通过祖先组件派发给【父组件B】。

React 组件化开发_第2张图片
多层组件结构

因此如果项目较为庞大时,可以引入 redux 进行全局状态管理(可参考 redux 使用实例)。当项目量级较小时,则使用 React 中的 Context 来进行公共状态的管理,该模式包括两个角色:

  • Provider:外层提供数据的组件,内部组件都可以访问到来自 provider 的数据

  • Consumer :内层获取数据的组件,沿上追溯到最近的 provider,消费其数据。接收一个函数作为子节点,返回 react 节点。

function Display(props) {
    // 6. props 重新赋值,组件更新
    return (
        

{props.title}

你的名字是:{props.name}

你的邮箱是:{props.email}

) } class FormItem extends Component { state = { val: '' } render() { const {keyName, label, type} = this.props // 3. consumer 内部接收一个函数,参数 value 来源于最近的 provider return ( {(value, _this) => { return (
{this.setState({val: e.target.value})}} onKeyDown = {e => { if( 13 === e.keyCode ) { // 4. 调用操作方法,也即 Survey 组件中的 changeState 方法,修改 provider 中的数据 value.change(keyName, this.state.val) } }} />
) }}
) } } // 2. 中间组件不需要传递数据和方法 class Form extends Component { render() { return (
); } } const SurveyContext = React.createContext() export default class Survey extends Component { state = { name: 'abc', email: '[email protected]' } changeState(key, val) { this.setState({[key]: val}) } // 5. setState 方法触发组件更新,重新 render render() { return (
{/* 1. provider 提供 value 给 consumer,可以将修改 state 的方法也作为 value 对象的方法传递*/}

) } }

效果:


跨组件通信实例

你可能感兴趣的:(React 组件化开发)