【React】组件进阶(5)

一、组件通讯介绍

  1. 每个组件都是独立的,默认情况下,只能使用组件自己的数据
  2. 组件化过程中,将完整功能的组件拆分成多个组件,更好的完成整个应用的功能,多个组件不可避免要共享数据
  3. 为了实现该功能,就需要打破组件的独立封闭性,让其与外界沟通,这个过程就是 组件通讯

二、组件的 props

1. 基础
  1. 组件是封闭的,需接收外部数据应该通过 props 来实现
  2. props 的作用:接收传递给组件的数据
  3. 传递数据:给组件标签添加属性
  4. 接收数据:函数组件通过 形参props 接收数据,类组件通过 this.props 接收数据
  5. props 是一个对象,对象里面存储传递的多个属性
  6. 特点
    1. props 是只读对象,值不能被修改
    2. 可以给组件传递任意的类型的数据
// 类组件
class Hello extends Component {
  render() {
    // 接收数据
    return <h1>接收到的数据:{this.props.age}</h1>
  }
}

// 传递数据
root.render(<Hello age={20}  />)


// 函数组件
function Hello(props) {
  return <h1>接收到的数据:{props.age}</h1>
}

// 传递数据
root.render(<Hello age={20} colors={['red', 'blue']} />)

注意:使用类组件,写了构造函数,应该将 props 传递给 super(),否则,无法在构造函数中获取 props!

class Hello extends Component {
  // 接收传递的值
  constructor(props) {
    // 将值传递的父组件
    super(props)
    console.log(props) // {age:20}
  }
  render() {
    // 接收数据
    return <h1>接收到的数据:{this.props.age}</h1>
  }
}

// 错误写法
class Hello extends Component {
  constructor() {
    super()
    console.log(this.props) // undefined
  }
  render() {
    // 接收数据
    return <h1>接收到的数据:{this.props.age}</h1>
  }
}

root.render(<Hello age={20}  />)
2. children 属性
  1. 表示组件标签的子节点,当组件标签中有子节点时,props 就会有该属性
  2. 跟 props 一样,值可以是任意值(文本、React元素、组件、甚至函数)
function Hello() {
  return <Child>{<h1>通过 children 方式传数据</h1>}</Child>
}

function Child(props) {
  //
  console.log(props)
  return (
    <div>
      // 渲染 h1
      {props.children}
    </div>
  )
}
3. props 校验
  1. 为什么要对 props 进行校验

    1. 对于组件来说 props 是外来的,无法保证组件使用者传入什么格式的数据
    2. 如果传入的数据格式不对,可能导致组件内部错误
    3. 关键问题:组件的使用者不知道明确的错误信息
  2. 作用

    1. 允许在创建组件的时候,指定 props 的类型、格式等
    2. 捕获使用组件时因 props 导致的错误,给出明确的错误提示,增加组件的健壮性
  3. 使用

    1. 安装 prop-types (yarn add prop-types / npm i props-types)
    2. 导入 prop-types 包
    3. **组件名.propTypes = {} **来给组件的 props 添加校验规则
    4. 校验规则由 PropType 对象来指定
    import PropTypes from 'prop-types'
    
    function Hello() {
      return <Child age={19} />
    }
    
    function Child(props) {
      return (
        <div>
          <h1>number类型:{props.age}</h1>
          <h1>指定对象属性的类型:{props.obj.name}</h1>
        </div>
      )
    }
    
    // 设置 props 默认值
    Child.defaultProps = {
      // obj 的默认值,值的类型要和约束的一致
      obj: { name: '张三' },
    }
    
    // 给 props 添加约束
    Child.propTypes = {
      // 约定 age 属性为 number 类型
      // 类型不对,报出明确的错误,便于分析错误原因
      age: PropTypes.number,
      // 这个对象里面的 name 属性必须是 string 类型
      obj: PropTypes.shape({
        name: PropTypes.string,
      }),
    }
    

三、组件通讯的四种方式

1. 父传子
  1. 父组件提供需要传递的 state 数据
  2. 给子组件标签添加属性,值为 state 中的数据
  3. 子组件通过 props 接收父组件传递的值
class Hello extends Component {
  state = {
    age: 19,
  }
  render() {
    // 接收数据
    return (
      <div>
        <h1>传递值给子组件的值:{this.state.age}</h1>
	    // 子组件
        <Child age={this.state.age} />
      </div>
    )
  }
}

function Child(props) {
  return <h1>接收父组件的值:{props.age}</h1>
}
2. 子传父
  1. 利用回调函数,父组件提供回调,子组件调用,将传递的数据作为回调函数的参数
  2. 父组件提供一个回调函数(用于接收数据)
  3. 将函数作为属性的值,传递给子组件
  4. 子组件通过 props 调用回调函数
  5. 将子组件的数据作为参数传递给回调函数
class Hello extends Component {
  state = {
    age: 0,
  }

  // 提供回调
  getChildMeg = (res) => {
    // 接收子组件的值 res,将 state 中的值进行替换
    this.setState({ age: res })
  }

  render() {
    // 接收数据
    return (
      <div>
        <h1>接收子组件的值:{this.state.age}</h1>
	    // 将父组件提供的回调传递给子组件
        <Child getMsg={this.getChildMeg} />
      </div>
    )
  }
}

class Child extends Component {
  state = {
    age: 20,
  }

  // 调用父组件传递的方法,将子组件的数据当作函数的参数传递过去
  sendData = () => {
    this.props.getMsg(this.state.age)
  }

  render() {
    return (
      <div>
        <h1>子组件数据:{this.state.age}</h1>
        <button onClick={this.sendData}>传递给父组件</button>
      </div>
    )
  }
}
3. 兄弟组件(状态提升)
  1. 共享状态 提升到最近的公共父组件中,由 公共父组件 管理这个状态
  2. 公共父组件职责:
    1. 提供共享状态
    2. 提供操作共享状态的方法
  3. 通讯的子组件只需通过 props 接收状态或操作状态的方法
// 共享数据的组件
class Hello extends Component {
  // 共享状态
  state = {
    count: 0,
  }

  // 提供修改状态的方法
  updata = () => {
    this.setState({ count: this.state.count + 1 })
  }

  render() {
    return (
      <div>
        {/* 当数据发生变化后,数据会自动流向该组件 */}
        <Child1 count={this.state.count} />
        <Child2 updata={this.updata} />
      </div>
    )
  }
}

// 展示数据
function Child1(props) {
  // 接收状态
  return <h1>计数器:{props.count}</h1>
}

// 操作数据
function Child2(props) {
  // 接收修改状态的方法
  return <button onClick={props.updata}>+1</button>
}
4. 跨组件传递
  1. 传统方式:使用 props 一层层组件往下传递(繁琐)
  2. Context:通过 Consumer 不管隔多远,都能往上找离它最近的 Provider 提供的数据 (推荐)
  3. 如果两个组件嵌套多层,可以使用 Context 实现组件通讯
  4. 步骤
    1. React.createContext 创建 **Provider(提供数据)**和 **Consumer(消费数据)**组件
    2. 使用 Provide 组件作为父节点
    3. 通过 value 属性,表示要传递的值
    4. 使用 Consumer 组件接收数据,回调函数中的 data 就是传递的值
  5. 注意:Consumer 组件中,写结构的话只能写在回调函数中
const { Provider, Consumer } = React.createContext()

class Hello extends Component {
  render() {
    return (
      <Provider value="blue">
        <div>
          <h1>我是祖宗</h1>
          <Node />
        </div>
      </Provider>
    )
  }
}

function Node(e) {
  return <Child1 />
}

function Child1(e) {
  return <Child2 />
}
function Child2(e) {
  return <Child3 />
}
function Child3(e) {
  return <Consumer>{(data) => <span>{data}</span>}</Consumer>

四、生命周期

1. 概述
  1. 组件的生命周期有利于理解组件的运行方式,完成复杂的组件功能、分析组件错误的原因等
  2. 生命周期:组件从创建到挂载到页面中运行,再到组件不用时卸载的过程
  3. 生命周期的每个阶段都有对应的方法调用,这些方法就是生命周期的 钩子函数
  4. 钩子函数的作用:让开发人员可以在不同阶段操作组件
  5. 只有 类组件 才有生命周期
2. 创建(挂载阶段)
  1. constructor(1)
    1. 创建组件时,最先执行
    2. 初始化 state
    3. 为事件处理程序绑定 this
  2. render(2)
    1. 每次组件渲染都会触发
    2. 渲染 UI
    3. 注意:不能调用 setState(),因为使用该方法能会改变状态和视图,发生改变后,又会触发 render,造成递归更新,react 限制了更新的次数,防止无限循环
  3. componentDitMount(3)
    1. 组件挂载(完成 DOM 渲染)后触发
    2. 发送网络请求
    3. DOM 操作
3. 更新(更新阶段)
  1. setState()、forceUpdate、组件接收到新的 props 值,组件就会重新渲染

  2. render(1)

  3. componentDitUpdate(2)

    1. 当状态发生变化完成 DOM 渲染后立即触发,首次渲染不触发

    2. 可以对更新前后的数据进行比较,从而执行对应的操作

    3. 发送网络请求

    4. DOM 操作

    5. 使用 setState() 必须放在条件里,不能直接调用 setState(),因为使用该方法能会改变状态和视图,

    6. 发生改变后,又会触发 render,造成递归更新,react 限制了更新的次数,防止无限循环

4. 卸载(卸载阶段)
  1. 组件从页面消失时触发
  2. componentWillUnmount
    1. 执行清理工作(定时器)
    2. 当组件中执行了一些不是 react 自身的操作的话,就需要在当前钩子函数中进行清理,否则可能造成内存泄漏

五、render-props 模式

1. 思路
  1. 将复用的 state 和操作 state 的方法封装到一个组件中
  2. 如何拿到该组件中复用的 state
    1. 添加一个值为 函数的prop,通过 函数参数 来获取(组件内部实现)
  3. 如何渲染任意的 UI?
    1. 使用 该函数的返回值 作为渲染的 UI 结构(组件内部实现)
Mouse
    // state 复用的状态
    render={(state) => (
      // 需要渲染的 UI
      <h1>
        鼠标的坐标:{state.x},{state.y}
      </h1>
    )}
  />,
2. props
  1. 创建 Mouse 组件,在组件中提供复用的 状态逻辑 代码(状态、操作状态的方法)
  2. 复用的状态 作为 props.render(state) 方法的参数,暴露到组件外部
  3. 使用 props.render() 的 返回值 作为渲染的内容(UI、state)
  4. 即可以通过函数的参数,拿到组件内部的 state,也可以通过函数的返回值指定渲染的内容,做到组件状态逻辑复用
class Mouse extends React.Componen	t {
  state = {
    x: 0,
    y: 0,
  }

  // 鼠标移动事件处理程序
  handleMouse = (e) => {
    console.log(e)
    this.setState({
      x: e.clientX,
      y: e.clientY,
    })
  }

  // 监听鼠标事件
  componentDidMount() {
    document.addEventListener('mousemove', this.handleMouse)
  }
    
  // 组件消失时移除事件绑定,因为它不是 react 自身的操作,不会自动清理
  componentWillUnmount() {
    document.removeEventListener('mouseover', this.handleMouse)
  }

  render() {
    // 将当前状态传递给 props 传递的方法,渲染方法返回的 UI
    return this.props.render(this.state)
  }
}

// 添加校验
Mouse.propTypes = {
  children: PropTypes.func.isRequired,
}

// 传递数据
root.render(
  <div>
  <Mouse
    // state 复用的状态
    render={(state) => (
      // 需要渲染的 UI
      <h1>
        鼠标的坐标:{state.x},{state.y}
      </h1>
    )}
  />,

  // 使用共同的数据,展示不同的 UI
  <Mouse
      // 接收传递的参数,返回需要展示的 UI 和 state
      render={(state) => (
        <div>
          {state.x}
          <img
            width={100}
            src={img}
            style={{
              position: 'absolute',
              top: state.y - 35,
              left: state.x - 50,
            }}
            alt="react"
          />
        </div>
      )}
  />
 </div>
)
3. children(推荐)
 	render() {
        // 将当前状态传递给 props 传递的方法,渲染方法返回的 UI
        return this.props.children(this.state)
 	}

 	<Mouse>
      {(state) => (
        <h1>
          鼠标的坐标:{state.x},{state.y}
        </h1>
      )}
    </Mouse>

	<Mouse>
      {(state) => (
        <img
          width={100}
          src={img}
          style={{
            position: 'absolute',
            top: state.y - 35,
            left: state.x - 50,
          }}
          alt="react"
        />
      )}
    </Mouse>
4. 总结
  1. 当组件之间有共同的 state 时,但展示的 UI 不同时,可以使用该模式,为了实现 状态逻辑复用
  2. 并不是该模式叫 render props 就必须使用名为 render 的 prop,可以使用任意名称的 prop
  3. prop 是一个函数并且告诉组件要渲染什么内容的技术叫:render props 模式

六、高阶组件

1. 概述
  1. 目的:实现 状态逻辑复用
  2. 采用:包装(装饰)模式
    1. 比如说:手机壳
    2. 手机:获取保护功能
    3. 手机壳:提供保护功能
  3. 高阶组件相当于手机壳,通过包装组件,增加组件功能
2. 思路分析
  1. 高阶组件(HOC,Higher-Order Component)是 一个函数 ,接收要包装的组件,返回增强后的组件
  2. 高阶组件内部 创建一个类组件,在这个类组件中 提供复用的状态逻辑 代码,通过 prop 将复用的状态传递给被包装的组件
3. 使用步骤
  1. 创建一个函数,名称约定 with 开头
  2. 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
  3. 函数内部创建一个类组件,提供复用的状态逻辑代码,并返回该组件
  4. 在该组件中,渲染参数组件,同时将值通过 prop 传递给参数组件
  5. 调用该高阶组件函数,传入要增强的组件,通过返回值拿到增强后的组件,并渲染到页面中
// 展示数值
const Test = (props) => {
  return (
    <h1>
      鼠标的坐标:{props.x},{props.y}
    </h1>
  )
}

// 图标跟随鼠标移动
const Test1 = (props) => {
  return (
    <img
      width={100}
      src={img}
      style={{
        position: 'absolute',
        top: props.y - 35,
        left: props.x - 50,
      }}
      alt="react"
    />
  )
}

// 接收包装好的组件,进行渲染
const WrapedComponent = withMouse(Test)
const WrapedComponent1 = withMouse(Test1)

// withMouse 包装组件的函数
// WrapedComponent 需要包装的组件
function withMouse(WrapedComponent) {
  // Mouse 提供复用的状态逻辑
  class Mouse extends React.Component {
    // 鼠标状态
    state = {
      x: 0,
      y: 0,
    }

    // 鼠标移动事件处理程序
    handlerMouseMove = (e) => {
      this.setState({
        x: e.clientX,
        y: e.clientY,
      })
    }

    // 控制鼠标状态逻辑
    componentDidMount() {
      document.addEventListener('mousemove', this.handlerMouseMove)
    }

    // 组件消失时移除事件绑定,因为它不是 react 自身的操作,不会自动清理
    componentWillUnmount() {
      document.removeEventListener('mouseover', this.handlerMouseMove)
    }

    render() {
      // 将复用状态传递给被包装的组件
      return <WrapedComponent {...this.state} />
    }
  }

  // 返回提供复用状态的组件
  return Mouse
}

root.render(
  <div>
    <WrapedComponent />
    <WrapedComponent1 />
  </div>,
)
4. displayName
  1. 使用高阶组件的问题:得到的两个组件名称相同
  2. 原因:默认情况下,react 使用 组件名称 作为 displayName
  3. 解决:为高阶组件设置 displayName 便于调试时区分不同的组件
  4. displayName 的作用:用于设置组件的名字,使用 react 调试工具好区分组件
  5. 设置方式:
// 返回的组件名字:当前高阶组件的名字和被包装的组件名字进行拼接
Mouse.displayName = `withMouse${getDisplayName(WrapedComponent)}`
  

function getDisplayName(componentName) {
  // 被包装的组件有 displayName 就返回该值,没有就把组件的 name 值返回或 component
  return componentName.displayName || componentName.name || 'component'
}
5. props 丢失
  1. 原因:高阶组件函数返回的一个增强后的组件,传递 props 相当于给高阶组件函数中的组件传递,并没有继续往下传递
  2. 解决:使用高阶组件的时候避免渲染包装组件时,需要将 stateprops 一起传递组件
<WrapedComponent {...this.state} {...this.props} />

七、总结

  1. 组件通讯是构建 react 应用必不可少的一环
  2. props 的灵活性让组件更加强大
  3. 状态提升是 react 组件的常用模式
  4. 组件的生命周期有助于理解组件的运行过程
  5. 钩子函数可以让开发者在特定的时机执行某些功能
  6. render props 模式和告诫组件都可实现组件的状态逻辑复用
  7. 组件极简模型:(state,props) => UI
  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值