03_React组件通信

132 阅读5分钟

组件通信的意义

组件是独立且封闭的单元,默认情况下组件只能使用自己的数据(state)

组件化开发的过程中,完整的功能会拆分为多个组件,在这个过程中不可避免的需要互相传递一些数据,为了能让各组件之间可以进行互相沟通,数据传递,这个过程就是组件通信。

  1. 父子关系 - 最重要的
  2. 兄弟关系 - 自定义事件模式产生技术方法eventBus / 通过共同的父组件通信
  3. 其它关系 - redux/mobox / zustand

父传子实现

实现步骤:

  1. 父组件提供要传递的数据 - state
  2. 给子组件标签添加属性值为state中的数据
  3. 子组件中通过 props 接收父组件中传过来的数据
    1. 类组件使用 this.props 获取 props 对象
    2. 函数组件直接在参数中获取 props 对象
import React from 'react'

// 函数式子组件
function FSon(props) {
  console.log(props)
  return (
    <div>
      子组件1
      {props.msg}
    </div>
  )
}
// 类子组件
class CSon extends React.Component {
  render() {
    return (
      <div>
        子组件2
        {this.props.msg}
      </div>
    )
  }
}
// 父组件
class App extends React.Component {
  state = {
    message: 'this is message'
  }
  render() {
    return (
      <div>
        <div>父组件</div>
        <FSon msg={this.state.message} />
        <CSon msg={this.state.message} />
      </div>
    )
  }
}

export default App

props说明

  1. props是只读对象(readonly): 根据单向数据流的要求,子组件只能读取props中的数据,不能进行修改
  2. props可以传递任意数据: 数字、字符串、布尔值、数组、对象、函数、JSX
class App extends React.Component {
  state = {
    message: 'this is message'
  }
  render() {
    return (
      <div>
        <div>父组件</div>
        <FSon 
          msg={this.state.message} 
          age={20} 
          isMan={true} 
          cb={() => { console.log(1) }} 
          child={<span>this is child</span>}
        />
        <CSon msg={this.state.message} />
      </div>
    )
  }
}

props校验

对于组件来说,props是由外部传入的,为了避免出现错误,在组件内部需要对传入props进行验证

  • 如果项目中集成了Flow或者TypeScript,可以直接进行类型校验
  • 如果没有,也可以使用 prop-types 库进行参数校验
  • prop-types官网:zh-hans.reactjs.org/docs/typech…

实现步骤:

  1. 安装属性校验包:npm i prop-types
  2. 导入prop-types
  3. 使用组件名.propTypes = {}给组件添加校验规则

常见结构:

  1. 常见类型:array、bool、func、number、object、string
  2. React元素类型:element
  3. 必填项:isRequired
  4. 特定的结构对象:shape({})
Comp.propTypes = {
	// 常见类型
	optionalFunc: PropTypes.func,
	// 必填 只需要在类型后面串联一个isRequired
	requiredFunc: PropTypes.func.isRequired,
	// 特定结构的对象
	optionalObjectWithShape: PropTypes.shape({
		color: PropTypes.string,
		fontSize: PropTypes.number
	})
}

props默认值

通过defaultProps可以给组件的props设置默认值,在未传入props的时候生效

1.函数组件

直接使用函数参数默认值 或者 定义defaultProps

function List({pageSize = 10}) {
  return (
    <div>
      此处展示props的默认值:{ pageSize }
    </div>
  )
}

List.defaultProps = {
  pageSize: 10
}

2.类组件

使用类静态属性声明默认值,static defaultProps = {}

class List extends Component {
  // 方法一
  // static defaultProps = {
  //   pageSize: 10
  // }
  render() {
    return (
      <div>
        此处展示props的默认值:{this.props.pageSize}
      </div>
    )
  }
}
// 方法二
List.defaultProps = {
  pageSize: 10
}

子传父实现

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

  • 在vue中通过自定义事件完成
  • 在React中同样使用props,将父组件的方法作为props传递给子组件,在子组件中调用这个方法即可
import React from 'react'

// 子组件
function Son(props) {
  const {changeMsg} = props
  function handleClick() {
    // 调用父组件传递过来的回调函数 并注入参数
    changeMsg('this is newMessage')
  }
  return (
    <div>
      {props.msg}
      <button onClick={handleClick}>change</button>
    </div>
  )
}


class App extends React.Component {
  state = {
    message: 'this is message'
  }
  // 提供回调函数
  changeMessage = (newMsg) => {
    console.log('子组件传过来的数据:',newMsg)
    this.setState({
      message: newMsg
    })
  }
  render() {
    return (
      <div>
        <div>父组件</div>
        <Son
          msg={this.state.message}
          changeMsg={this.changeMessage}
        />
      </div>
    )
  }
}

export default App

React中的插槽(slot)

在开发中有时需要让使用者决定组件的某一块区域到底存放什么内容

这种需求在Vue中使用的是 插槽(slot)

在React中对于这种需要插槽的情况非常灵活,有两种方式

  1. props.children
    1. children属性表示该组件的子节点,只要组件内部有子节点,props中就有该属性
    2. children属性可以是:普通文本、普通标签元素、函数/对象、JSX
  2. props 属性传递 React 元素
  3. 作用域插槽实现
import React, { Component } from 'react'

class NavBar extends Component {
  render() {
    const { children } = this.props
    console.log(children)
    return (
      <div className='nav-bar'>
        <div className="left">{children[0]}</div>
        <div className="center">{children[1]}</div>
        <div className="right">{children[2]}</div>
      </div>
    )
  }
}

class NavBarTwo extends Component {
  render() {
    const { leftSlot, centerSlot, rightSlot } = this.props
    return (
      <div className='nav-bar'>
        <div className="left">{leftSlot}</div>
        <div className="center">{centerSlot}</div>
        <div className="right">{rightSlot}</div>
      </div>
    )
  }
}

class TabControl extends Component {
  constructor() {
    super()
    this.state = {
      currentIndex: 0
    }
  }

  itemClick(index) {
    // 1.自己保存最新的index
    this.setState({ currentIndex: index })
    // 2.让父组件执行对应的函数
    this.props.tabClick(index)
  }

  render() {
    const { tabList, itemType } = this.props
    const { currentIndex } = this.state

    return (
      <div className='tab-control'>
        {
          tabList.map((item, index) => {
            return (
              <div
                className={`item ${index === currentIndex ? 'active' : ''}`}
                key={item}
                onClick={e => this.itemClick(index)}
              >
                {/* <span className='text'>{item}</span> */}
                {itemType(item)}
              </div>
            )
          })
        }
      </div>
    )
  }
}

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

    this.state = {
      tabList: ["推荐", "音乐馆", "视频"],
      tabIndex: 0
    }
  }

  getTabItem(item) {
    if (item === "推荐") {
      return <span>{item}</span>
    } else if (item === "音乐馆") {
      return <button>{item}</button>
    } else {
      return <i>{item}</i>
    }
  }

  render() {
    const { tabList, tabIndex } = this.state
    const btn = <button>按钮2</button>

    return (
      <div>
        {/* 1.使用children实现插槽 */}
        <NavBar>
          <button>按钮</button>
          <h2>哈哈哈</h2>
          <i>斜体文本</i>
        </NavBar>
        <hr />
        {/* 2.使用props实现插槽 */}
        <NavBarTwo
          leftSlot={btn}
          centerSlot={<h2>呵呵呵</h2>}
          rightSlot={<i>斜体2</i>}
        />
        <hr />
        {/* 3.作用域插槽 */}
        <TabControl
          tabList={tabList}
          tabClick={i => this.setState({ tabIndex: i })}
          itemType={item => this.getTabItem(item)}
        />
        <h1>{tabList[tabIndex]}</h1>
      </div>
    )
  }
}

export default App

跨组件通信Context

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

但是有些数据需要在多个组件中进行共享(UI主题、登录状态、用户信息等)

如果从App组件向下一层一层的props往下传,显然很繁琐

React提供了一个API:Context

Context 提供了一个不必显式的通过组件树逐层传递props,就可以在在组件之间共享数据的方式,

Context 设计目的是为了 共享那些对于一个组件树而言是“全局”的数据, 例如当前用户的信息

1. React.createContext()

  1. 创建一个需要共享的Context对象
  2. 如果一个组件订阅了Context,那个这个组件会从最近的那个匹配的Provier中读取到当前的context值
  3. defaultValue:如果组件在查找过程中,没有找到对应Provider,那么就是用默认值
import React from 'React'

const UserContext = React.createContext({name: 'tom'})
const ThemeContext = React.createContext({color: '#409eff'})

2. Context.provider

  1. Context对象会返回一个 Provider 组件,它允许消费者订阅context的变化
  2. Provider 接受一个value属性,传递给消费组件
  3. 一个Provider可以包含多个消费组件
  4. 多个Provider组件可以嵌套使用,里层的会覆盖外层的数据
  5. 当Provider的value发生了变化,它所有后代组件都会重新渲染
<UserContext.Provider value={{nickname: "sam", age: 30}}>
  <ThemeContext.Provider value={{color: "red", size: "30px"}}>
    <Home {...info}/>
  </ThemeContext.Provider>
</UserContext.Provider>

3. Class.contextType

  1. 将一个由React.createContext()创建的Context对象赋值给class的contextType属性
  2. 通过使用this.context来获取最近的Context提供的值
  3. 可以在任何生命周期中访问this.context,包括render函数中

注意: 这总方式只能订阅单一 context

import React, {Component} from 'react

class UserInfo extends Component {
  // 方法二 class fields 语法
  // static contextType = UserContext;
  
  render() {
    <div>用户信息{{this.context}}</div>>
  }
}

// 方法一
UserInfo.contextType = UserContext

4. Context.Consumer

通过 Context 对象返回的Consumer组件,也可以订阅context

<ThemeContext.Consumer>
    {
        themeVal => (
            <UserContext.Consumer>
                {
                    userVal => (
                            <ProfilePage user={userVal} theme={themeVal} />
                    )
                }
            </UserContext.Consumer>
        )
    }
</ThemeContext.Consumer>