React学习笔记 --- React组件化(下)

892 阅读9分钟

一、 父子组件间通信

1.1 子组件向父组件进行通信

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

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

父组件

import React, { Component } from 'react';

import Cpn from './Cpn'

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

        this.state = {
            count: 0
        }
    }

    render() {
        return (
            <div>
                <h3>{ this.state.count }</h3>
                {/*
          如果需要子组件传递状态给父组件
          那么其就需要给子组件传递一个事件的引用,
          让子组件的事件可以指向父组件中对应的方法
          这样调用子组件对应的事件的callback的时候本质上调用的就是父组件中对应的事件


          注意: 这个事件是传递给子组件中交由react自定义事件去执行的,所以需要注意内部的this指向
              所以只要是react事件去进行的调用,那么就一定要注意内部this的指向
        */}
                <Cpn increment={ () => { this.increment() } } />
            </div>
        )
    }

    increment() {
        // setState 是一个function
        this.setState({
            count: this.state.count + 1
        })
    }
}

export default App

子组件

import React, { Component } from 'react'

class Cpn extends Component {
  render() {
    return (
      <div>
        {/*
          这里不要去调用函数(也就是不要去加()),
          这里需要的是一个引用,指向了父组件中对应的函数
          在点击之后本质上回调的是父组件中对应传递过来的方法
        */}
        <button onClick={ this.props.increment }>
          +1
        </button>
      </div>
    )
  }
}

export default Cpn

1.2 父子组件通信综合案例

dqS4MQ.gif

父组件

import React, { Component } from 'react';

import TabBar from './Cpn'

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

        // 对于后期不会发生修改的数据,不需要挂载到state中
        // 直接挂载到组件上即可
        this.items = ['流行', '新款', '精选']

        // 对于后期需要变化的数据,可以挂载到state中
        this.state = {
            currentItem: '流行'
        }
    }

    render() {
        return (
            <div>
                {/*
          change函数是子组件进行调用的
          子组件在调用的时候会传递过来一个index作为形参值
          虽然在调用父组件内部的change方法来更新界面
        */}
                <TabBar items={ this.items } change={ index => this.change(index) } />
                <h3>{ this.state.currentItem }</h3>
            </div>
        )
    }

    change(index) {
        this.setState({
            currentItem: this.items[index]
        })
    }
}

export default App;

子组件

import React, { Component } from 'react';

// 在子组件中引入样式文件
import './style.css'

class Cpn extends Component {
    constructor(props) {
        super(props)

        this.state = {
            currentIndex: 0
        }
    }

    render() {
        const {
            items
        } = this.props

        const {
            currentIndex
        } = this.state

        return (
            <div className="tab-container">
                {
                    items.map((item, index) => (
                        <div key={item}
                            className={'tab-item ' + (index === currentIndex ? 'active' : '') }
                            onClick={e => this.changeIndex(index) }
                            >
                            {/*
                1.  如果属性值比较多的时候,其可以采用上述的方法进行换行显示

                2, 如果需要动态绑定样式类的时候,需要使用上述的这种字符串拼接的方式
                    注意多个样式之间使用空格进行划分,所以tab-item后面需要加上空格

                3.  onClick是react中回调的事件,其只有一个形参,那就是event对象

                4. changeIndex事件中的index是循环中的index
              */}
                            <span> { item } </span>
                        </div>
                    ))
                }
            </div>
        )
    }

    changeIndex(index) {
        // 修改界面中高亮的元素
        this.setState({
            currentIndex: index
        })

        // 调用父组件的方法,修改父组件中的状态
        this.props.change(index)
    }
}

export default Cpn;

样式文件

.tab-container {
    display: flex;
    height: 45px;
    line-height: 45px;
}

.tab-item {
    flex: 1;
    text-align: center;
}

.active {
    color: red;
}

.tab-item span {
    padding: 5px 8px;
}

.active span {
    border-bottom: 3px solid red;
}

二、 React模拟插槽

方式1

父组件

import React, { Component } from 'react'

import Cpn from './Cpn'

export default class componentName extends Component {
  render() {
    return (
      <div>
        {/*
          主要把需要展示的内容作为子组件的子组件去进行传递就可以了
        */}
        <Cpn>
          <span>Left</span>
          <span>Center</span>
          <span>Right</span>
        </Cpn>
      </div>
    )
  }
}

子组件

import React, { Component } from 'react'

import './style.css'

export default class componentName extends Component {
  render() {
    return (
      <div className="container" >
        {/*
          此时 Cpn中的所有插槽都作为了props的children给传递过来了
          this.props.children 是一个数组,所以我们可以通过索引来获取到每一个插槽值
        */}
        <div className="left">
          { this.props.children[0] }
        </div>
        <div className="center">
          { this.props.children[1] }
        </div>
        <div className="right">
          { this.props.children[2] }
        </div>
      </div>
    )
  }
}

效果图

dqkL7Q.png

问题

  1. 这里获取插槽中的元素的值的时候,使用的是索引的方式,所以对于传入的插槽的顺序是有要求的

    如果传入的顺序不对,那么插槽最终展示效果也会出现问题

  2. 所以 一般使用这种方式去模拟插槽的时候,适合于那些只有一个插槽项的情况,因为其只有一个子组件

方式2

父组件

import React, { Component } from 'react'

import Cpn from './Cpn'

export default class componentName extends Component {
  render() {
    return (
      <div>
        {/*
          因为JSX最后会被解析为ReactElement对象
          所以可以认为JSX就是一个对象类型的数据
          所以其可以作为大括号中的值传递给子组件
           
          注意: 这里的插槽中的值可以是类似于<NavBar/>之类的组件,所以可以在这里传递入一个组件(组件最后返回的也是JSX对象)
          		也可以将这里的JSX对象提取出来形参render函数的变量,随后在Cpn中使用变量即可
        */}
        <Cpn
          leftSlot={ <span>Left</span> }
          centerSlot={ <span>Center</span> }
          rightSlot={ <span>Right</span> }
        />
      </div>
    )
  }
}

子组件

import React, { Component } from 'react'

import './style.css'

export default class componentName extends Component {
  render() {
    // 从props中取出对应的属性值(这里就是插槽值)
    let {
      leftSlot,
      centerSlot,
      rightSlot,
     } = this.props


    //  可以对插槽值进行校验和设置默认值
    leftSlot = leftSlot || <span>Slot不存在</span>
    centerSlot = centerSlot || <span>Slot不存在</span>
    rightSlot = rightSlot || <span>Slot不存在</span>

    return (
      <div className="container">
        <div className="left">
          { leftSlot }
        </div>

        <div className="center">
          { centerSlot }
        </div>

        <div className="right">
          { rightSlot }
        </div>
      </div>
    )
  }
}

三、跨组件通信

​ 非父子组件数据的共享:

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

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

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

3.1 Context相关API

React.createContext

创建一个需要共享的Context对象:

  • 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值

dq2QsJ.png

Context.Provider

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

dqWECV.png

Class.contextType

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

dqWG8K.png

Context.Consumer

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

dqWrPP.png

3.2 示例代码

1. 使用props一层层嵌套

import React, { Component } from 'react';

// 子组件 === 消费组件
class ProfileHeader extends Component {
  render() {
    return (
      <div>
        <h3>UserName: { this.props.name }</h3>
        <h3>Age: { this.props.age }</h3>
      </div>
    )
  }
}

// 中间组件
class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader {...this.props} />

        <div>profile-item1</div>
        <div>profile-item2</div>
        <div>profile-item3</div>
        <div>profile-item4</div>
        <div>profile-item5</div>
      </div>
    )
  }
}

// 父组件 === 生产组件
class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <div>
        {/*
          Spred Atrtribute 对象展开语法
          具体文档可以参考: https://zh-hans.reactjs.org/docs/jsx-in-depth.html
          {... this.state} === name='Klaus' age=23
        */}
        <Profile {...this.state} />
      </div>
    );
  }
}

export default App;

2. 使用上下文(context)进行传递

类组件

import React, { Component } from 'react';

// 1. 创建执行上下问(命名时推荐首字母大写)
const UserContext = React.createContext({
  // 这里书写默认值
  name: 'Steven',
  age: 18
})

class ProfileHeader extends Component {
  // 每一个类组件内部都会有一个叫做context变量, 默认值是{}
  // 需要将其和对应的ctx进行关联,以便于对context进行赋值操作
  static contextType = UserContext

  render() {
    return (
      <div>
        {/* 关联完毕以后就可以使用this.context来进行使用 */}
        <h3>UserName: { this.context.name }</h3>
        <h3>Age: { this.context.age }</h3>
      </div>
    )
  }
}


class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader />

        <div>profile-item1</div>
        <div>profile-item2</div>
        <div>profile-item3</div>
        <div>profile-item4</div>
        <div>profile-item5</div>
      </div>
    )
  }
}

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

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <UserContext.Provider value={ this.state }>
        {/*
           Provider和Consumer是CTX中提供的组件,直接使用即可

           value 是你实际需要传递的共享的数据,类型为Object
        */}

        {/*
          将消费组件所在的组件树(也就是其在父辈组件)
          放置到CTX的生成者中
        */}
        <Profile />
      </UserContext.Provider>
    )
  }
}

export default App;
render() {
 return (
   <div>
     <UserContext.Provider value={ this.state }>
     </UserContext.Provider>
     {/* 如果放在生成者内部,那么其值就会自动取value中的值,如果没有给value中的值就会报错 */}
     {/* 但是不在生成者内部其就会使用默认值,也就是Steven和18 */}
     <Profile />
   </div>
 )
}

函数组件

import React, { Component } from 'react';

const UserContext = React.createContext({
  name: 'Steven',
  age: 18
})

function ProfileHeader() {
  return (
    <UserContext.Consumer>
      {/*
        在这里使用消费者组件中传递一个callback
        在这个callback函数中返回一个JSX对象
      */}

      {
        value => {
          return (
            <div>
              {/* 这里的value值就是存储了数据的context */}
              <h3>name: {value.name}</h3>
              <h3>age: {value.age}</h3>
            </div>
          )
        }
      }
    </UserContext.Consumer>
  )
}


class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader />

        <div>profile-item1</div>
        <div>profile-item2</div>
        <div>profile-item3</div>
        <div>profile-item4</div>
        <div>profile-item5</div>
      </div>
    )
  }
}

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

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <div>
        <UserContext.Provider value={ this.state }>
          <Profile />
        </UserContext.Provider>
      </div>
    )
  }
}

export default App;

3.多层Context进行消费

子组件消费多个context只能使用函数组件是不可以使用类组件

import React, { Component } from 'react';

const UserContext = React.createContext({
  name: 'Steven',
  age: 18
})


// 这是另外一个需要共享的数据
const ThemeContext = React.createContext({
  color: 'black'
})

function ProfileHeader() {
  return (
    <UserContext.Consumer>
      {/* 这里是第一个消费者 */}
      {
        value => {
         return  (
           <ThemeContext.Consumer>
             {/* 这里是第二个消费者 */}
             {
               theme => {
                return (
                  <div>
                    <h3>name: {value.name}</h3>
                    <h3>age: {value.age}</h3>
                    <h3>color: { theme.color }</h3>
                  </div>
                )
              }
             }
           </ThemeContext.Consumer>
         )
        }
      }
    </UserContext.Consumer>
  )
}


class Profile extends Component {
  render() {
    return (
      <div>
        <ProfileHeader />

        <div>profile-item1</div>
        <div>profile-item2</div>
        <div>profile-item3</div>
        <div>profile-item4</div>
        <div>profile-item5</div>
      </div>
    )
  }
}

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

    this.state = {
      name: 'Klaus',
      age: 23
    }
  }

  render() {
    return (
      <div>
        <UserContext.Provider value={ this.state }>
          <ThemeContext.Provider value={ {color: 'red'} }>
            <Profile />
          </ThemeContext.Provider>
        </UserContext.Provider>
      </div>
    )
  }
}

export default App;

备注:

  1. 多数情况下,会把定义的context的代码抽取到一个独立的文件中,例如context.js
  2. 如果需要传递的数据比较多的情况下推荐使用redux,以免出现context多层嵌套的问题
  3. 什么时候使用Context.Consumer呢?
    1. 当使用value的组件是一个函数式组件时;
    2. 当组件中需要使用多个Context时;

上一篇 React组件化(上) 下一篇 setState的简单使用