react 组件通讯

组件通讯

  • 组件是独立且封闭的单元,默认情况下,只能使用组件自己的数据。
  • 在组件化过程中,我们将一个完整的功能拆分成多个组件,以更好的完成整个应用的功能。
  • 而在这个过程中,多个组件之间不可避免的要共享某些数据。
  • 为了实现这些功能,就需要打破组件的独立封闭性,让其与外界沟通。这个过程就是组件通讯
  • 在react中,数据来源主要有两个,一个是state内部数据,另一个是props外部数据,这两个数据发生改变后都会引起组件的更新渲染。
  • 父组件在展示子组件,可能会传递一些数据给子组件:
  • 父组件通过 属性=值 的形式来传递给子组件数据;
  • 子组件通过 props 参数获取父组件传递过来的数据

 

props

  • 组件是封闭的,要接收外部数据应该通过props来实现
  • props的作用:接收传递给组件的数据
  • 传递数据:给组件标签添加属性
  • 接收数据:函数组件通过参数props接收数据,类组件通过this.props接收数据

函数组件通讯

基本数据类型传递

子组件

import React, { memo } from 'react'
// import PropTypes from 'prop-types'

function demoFuncClassComponent(props) {
  return (
    <div>
      <h4>函数组件</h4>
      <p>{props.title}</p>
    </div>
  )
}

// 定义props的类型
// demoFuncClassComponent.propTypes = {}

export default memo(demoFuncClassComponent)

父组件

import './App.css'

import DemoFuncClassComponent from './components/demoFuncClassComponent'

function App() {
  return (
    <div className="App">
      <DemoFuncClassComponent title="我是父组件传递给子组件的数据" />
    </div>
  )
}

export default App

引用数据类型传递

如果子组件接收的数据是一个一个的字段,在对象中有很多数据需要传入,那么你可以使用es6中的扩展语法将对象的数据全部扩展进去

子组件Props.jsx

import React, { memo } from 'react'
// import PropTypes from 'prop-types'

function demoFuncClassComponent(props) {
  return (
    <div>
      <h4>函数组件</h4>
      <p>{props.title}</p>
      <p>{props.name}</p>
      <p>{props.age}</p>
      <p>{props.address}</p>
    </div>
  )
}

// 定义props的类型
// demoFuncClassComponent.propTypes = {}

export default memo(demoFuncClassComponent)

父组件App.jsx

import './App.css'

import DemoFuncClassComponent from './components/demoFuncClassComponent'

let user = {
  name: '张三',
  age: 20,
  address: '渝北区',
}

function App() {
  return (
    <div className="App">
      <DemoFuncClassComponent title="我是父组件传递给子组件的数据" {...user} />
    </div>
  )
}

export default App

如果子组件接收的是整个对象,那么你可以直接将对象传入

子组件Props.jsx

import React from 'react'

export default function Props(props) {

    console.log(props);

  return (
    <div>
        标题:{props.title}
        <br />
        姓名:{props.user.name}
        <br />
        年龄:{props.user.age}
        <br />
        地址:{props.user.address}
    </div>
  )
}

父组件App.jsx

import Props from "./components/Props";


let user = {
  name: '张三',
  age: 20,
  address: '渝北区'
}

function App() {
  return (
    <div>
      <Props title='标题' user={user} />
    </div>
  );
}

export default App;

类组件通讯

类组件的外部数据通过this.props来访问

import React, { Component } from 'react'

export default class PropsClass extends Component {
    constructor(props) {
        super(props);
    }
  render() {
    return (
      <div>
        标题:{this.props.title}
      </div>
    )
  }
}

父组件:App.jsx传入数据

import Props from "./components/Props";

function App() {
    return (
        <div>
        <Props title='标题' />
        </div>
    );
}

export default App;

props的特点

  • 可以给组件传递任意类型的数据
  • props是只读的,不允许修改props的数据
  • 注意:在类组件中使用的时候,需要把props传递给super(),否则构造函数无法获取到props
class Hello extends React.Component {
    constructor(props) {
        // 推荐将props传递给父类构造函数
        super(props)
    }
    render() {
    	return <div>接收到的数据:{this.props.age}</div>
    }
}

组件通讯方式

  • 父传子
  • 子传父
  • 非父子

父传子

在vue中,父传子通过props传递,子传父通过$emit触发自定义事件进行传参。

在react中,父子组件通信,父传子通过props传递;子传父通过调用父组件传递的函数通知父组件进行修改数据:

  1. 父组件提供要传递的state数据
  1. 给子组件标签添加属性,值为 state 中的数据
  1. 子组件中通过 props 接收父组件中传递的数据

App.js

import './App.css';
import React from 'react';
import Parent from './components/Parent'

function App() {
  return (
    <div>
    <Parent/>
    </div>
  );
}

export default App;

父组件:提供数据并且传递给子组件--components/Parent.jsx

import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
  state = {
    title: '星期一'
  }
  render() {
    return (
      <div>
        <Child title={this.state.title}></Child>
      </div>
    )
  }
}

子组件:接收数据

import React, { Component } from 'react'

export default class Child extends Component {
  render() {
    return (
      <div>
        <p>接收父组件传递过来的参数--{this.props.title}</p>
      </div>
    )
  }
}

子传父

思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数。

  1. 父组件提供一个回调函数(用于接收数据)
  1. 并将该函数作为属性值,传递给子组件
  1. 子组件通过 props 调用回调函数
  1. 将子组件的数据作为参数传递给回调函数

App.js

import './App.css';
import React from 'react';
import Parent from './components/Parent'

function App() {
  return (
    <div>
    <Parent/>
    </div>
  );
}

export default App;

父组件提供函数--components/Parent.jsx

类似于js里的将函数当做参数传递给子组件

import React, { Component } from 'react';
import ClassSub from './classSub';
class classHello extends Component {
  constructor(){
    super()
    this.state = {
      counter: 0
    }
  }
  // 父组件提供一个回调函数(用于接收数据)
  changeCounter = (count) => {
    this.setState({
      counter: this.state.counter + count
    })
  }
  render() {
    const {counter} = this.state
    return (
      <div>
        <p>当前值:{counter}</p>
        <ClassSub changeCounter = {(count) => this.changeCounter(count)} />
      </div>
    );
  }
}

export default classHello;

子组件接收函数并且调用--components/Child.jsx

import React, { Component } from 'react'

export default class classSub extends Component {
  
  handlerClick = (count) => {
    this.props.changeCounter(count)
  }
  render() {
    return (
      <div>
        <button onClick={() => this.handlerClick(1)}>子传父+1</button>
        <button onClick={() => this.handlerClick(5)}>子传父+5</button>
        <button onClick={() => this.handlerClick(10)}>子传父+10</button>
      </div>
    )
  }
}

 

父子通讯示例

tab栏

利用父传子,子传父

父组件:

import React, { Component } from 'react'
import TabControl from './TabControl'
export default class classHello extends Component {
  constructor(){
    super()
    this.state = {
      titles: ["流行","新款", "精选"],
      tabIndex: 0
    }
  }

  tabClick = (index) => {
    this.setState({
      tabIndex: index
    })
  }
  render() {
    const {titles, tabIndex} = this.state
    return (
      <div>
        <TabControl titles={titles} tabClick={(index) => this.tabClick(index)} />
        <div>{titles[tabIndex]}</div>
      </div>
    )
  }
}

子组件:

import React, { Component } from 'react';
import './TabControl.css'
class TabControl extends Component {

  constructor(){
    super()
    this.state = {
      currentIndex: 0,
    }
  }

  itemClick = (index) => {
    this.setState({
      currentIndex: index
    })
    this.props.tabClick(index)
  }
  
  render() {
    const {titles} = this.props
    const {currentIndex} = this.state
    return (
      <div className='tab-control'>
        {
          titles.map((item, index) => {
            return (
              <div 
                className={`item ${index === currentIndex ? 'active' : ''}`} 
                key={item}
                onClick={() => this.itemClick(index)}
              >
                <span className='text'>{item}</span>
              </div>
            )
          })
        }
      </div>
    );
  }
}

export default TabControl;

样式:

.tab-control {
  display: flex;
  align-items: center;
  text-align: center;
  height: 40px;
}

.tab-control .item {
  flex: 1;
}

.tab-control .item.active {
  color: red;
}

.tab-control .item.active .text {
  border-bottom: 3px solid red;
  padding: 3px;
}

props 和 callback 一起使用
// 子组件
function Son(props) {
  const { fatherSay, sayFather } = props;

  return (
    <div className="son">
      我是子组件
      <div>父组件对我说:{ fatherSay }</div>
      <input placeholder="我对父组件说" onChange={ e =>  sayFather(e.target.value)}></input>
    </div>
  )
}

// 父组件
function Father() {
  const [ childSay, setChildSay ] = useState("");
  const [ fatherSay, setFatherSay ] = useState("");
  return(
    <div className="box father">
      我是父组件
      <div>子组件对我说:{ childSay }</div>
      <input placeholder="我对子组件说" onChange={ e =>  setFatherSay(e.target.value)}></input>
      <Son fatherSay={fatherSay} sayFather={setChildSay}></Son>
    </div>
  )
}

 

组件插槽实现

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

我们应该让使用者可以决定某一块区域到底存放什么内容。

这种需求在Vue当中有一个固定的做法是通过slot来完成的,React呢?

React对于这种需要插槽的情况非常灵活,有两种方案可以实现:

  1. 组件的children子元素
  1. props属性传递React元素

children实现插槽
  • 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容
  • 通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的元素

父组件:

import React, { Component } from 'react'
import NavBar from './NavBar'
export default class classHello extends Component {
  render() {
    return (
      <div>
        <NavBar>
          <button>按钮</button>
          <h2>标题</h2>
          <i>斜体文字</i>
        </NavBar>
      </div>
    )
  }
}

注意:如果在父组件里只传递一个元素,那么在子组件里这一个就是children,并不是数组了

例如:

父:
<NavBar>
  <i>斜体文字</i>
</NavBar>

子:
<div className="left">{children}</div>

子组件:

import React, { Component } from 'react';
import './navBar.css'
class NavBar extends Component {
  render() {
    // 注意:放置多个的时候拿到的children就是一个数组
    // 放置一个的时候children就是那一个元素/内容
    const {children} = this.props
    return (
      <div className='nav-bar'>
        <div className="left">{children[0]}</div>
        <div className="center">{children[1]}</div>
        <div className="right">{children[2]}</div>
      </div>
    );
  }
}

export default NavBar;

样式:

*{
  padding: 0;
  margin: 0;
}
.nav-bar {
  display: flex;
  height: 40px;
  text-align: center;
  line-height: 40px;
}

.left, .right {
  width: 80px;
  background-color: green;
}

.center {
  flex: 1;
  background-color: aqua;
}

props实现插槽
  • 通过具体的属性名,可以让我们在传入和获取时更加的精准

父组件:

import React, { Component } from 'react'
import NavBar from './NavBar'
export default class classHello extends Component {
  render() {
    const lefeSlot = <button>按钮</button>
    const centerSlot = <h2>标题</h2>
    const rightSlot = <i>斜体文字</i>
    return (
      <div>
        <NavBar lefeSlot={lefeSlot} centerSlot={centerSlot} rightSlot={rightSlot} />
      </div>
    )
  }
}

子组件:

import React, { Component } from 'react';
import './navBar.css'
class NavBar extends Component {
  render() {
    const {lefeSlot, centerSlot, rightSlot} = this.props
    return (
      <div className='nav-bar'>
        <div className="left">{lefeSlot}</div>
        <div className="center">{centerSlot}</div>
        <div className="right">{rightSlot}</div>
      </div>
    );
  }
}

export default NavBar;

样式:

*{
  padding: 0;
  margin: 0;
}
.nav-bar {
  display: flex;
  height: 40px;
  text-align: center;
  line-height: 40px;
}

.left, .right {
  width: 80px;
  background-color: green;
}

.center {
  flex: 1;
  background-color: aqua;
}

 

作用域插槽
  • 根据回调函数可以返回不同的内容来实现
  • 组件的标签由父组件决定,但是渲染的内容由子组件决定

父组件:

import React, { Component } from 'react'
import TabControl from './TabControl'
export default class classHello extends Component {
  constructor(){
    super()
    this.state = {
      titles: ["流行","新款", "精选"],
      tabIndex: 0
    }
  }

  tabClick = (index) => {
    this.setState({
      tabIndex: index
    })
  }
  getTabItem = (item) => {
    if(item === '流行'){
      return <span>{item}</span>
    }else if(item === '新款'){
      return <button>{item}</button>
    } else {
      return <i>{item}</i>
    }
  }
  render() {
    const {titles, tabIndex} = this.state
    return (
      <div>
        <TabControl 
          titles={titles} 
          tabClick={(index) => this.tabClick(index)}
          /**itemType={(item) => <span>{item}</span>} */
          itemType={item => this.getTabItem(item)}
        />
        <div>{titles[tabIndex]}</div>
      </div>
    )
  }
}

子组件:

import React, { Component } from 'react';
import './TabControl.css'
class TabControl extends Component {

  constructor(){
    super()
    this.state = {
      currentIndex: 0,
    }
  }

  itemClick = (index) => {
    this.setState({
      currentIndex: index
    })
    this.props.tabClick(index)
  }
  
  render() {
    const {titles, itemType} = this.props
    const {currentIndex} = this.state
    return (
      <div className='tab-control'>
        {
          titles.map((item, index) => {
            return (
              <div 
                className={`item ${index === currentIndex ? 'active' : ''}`} 
                key={item}
                onClick={() => this.itemClick(index)}
              >
               {/* <span className='text'>{item}</span>*/}
               {itemType(item)}
              </div>
            )
          })
        }
      </div>
    );
  }
}

export default TabControl;

样式:

.tab-control {
  display: flex;
  align-items: center;
  text-align: center;
  height: 40px;
}

.tab-control .item {
  flex: 1;
}

.tab-control .item.active {
  color: red;
}

.tab-control .item.active .text {
  border-bottom: 3px solid red;
  padding: 3px;
}

 

兄弟组件

  • 将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态
  • 思想:状态提升
  • 公共父组件职责:
    • 提供共享状态
    • 提供操作共享状态的方法
  • 要通讯的子组件只需通过 props 接收状态或操作状态的方法

状态提升前

状态提升之后

 

利用状态提升

其实就是利用子传父、父传子实现

App.js

import React, { Component } from 'react'
import Jack from './components/Jack'
import Rose from './components/Rose'

export default class App extends Component {
  // 1. 状态提升到父组件
  state = {
    info: ''
  }
  changeInfo = (info) => {
    this.setState({
      info
    })
  }
  render() {
    return (
      <div>
      {/*4. 更改属性值*/}
      <Jack say={this.changeInfo} />
      {/* 2. 将状态给子组件进行显示 */}
      <Rose info={this.state.info} />
      </div>
    )
  }
}

components/Jack.jsx

import React, { Component } from 'react'

export default class Jack extends Component {
  // 3. 子传父
  say = () => {
    this.props.say('you jump i look')
  }
  render() {
    return (
      <div>
      <h3>Jack:</h3>
      <button onClick={this.say}>say</button>
      </div>
    )
  }
}

components/Rose.jsx

import React, { Component } from 'react'

export default class Rose extends Component {
  render() {
    return (
      <div>
        <h3>Rose</h3>
        <p>Jack say:{this.props.info}</p>
      </div>
    )
  }
}

 

利用 ref 实现

在 React 的开发模式中,通常情况下不需要,也不建议直接操作DOM,但是某些特殊的情况需要获取到DOM进行某些操作:

  • 管理焦点,文本选择或媒体播放
  • 触发强制动画
  • 集成第三方DOM库

App.js

import './App.css';
import React from 'react';
import Parent from './components/Parent'

function App() {
  return (
    <div>
    <Parent/>
    </div>
  );
}

export default App;

components/Parent.jsx

通过父组件作为中转站去获取子组件实例操作子组件的方法或数据

import React, { Component } from "react";
import Child from "./Child";
import Child2 from "./Child2";
export default class Parent extends Component {
  constructor(props) {
    super(props);
    this.childRef = React.createRef();
  }

  // 在父组件里通过 ref 去调用子组件里的那个更改自己属性值的方法,并将参数传递过去
  changeTitle = () => {
    this.childRef.current.changeTitle("马上国庆节");
  };

  render() {
    return (
      <div>
        <Child
          ref={this.childRef}
        />
        <Child2 onChangeTitle={this.changeTitle} />
      </div>
    );
  }
}

components/Child.jsx

定义内部state的title

import React, { Component } from 'react'

export default class Child extends Component {
  state = {
    title: '今天星期一'
}

// 在子组件内自定义一个方法去更改自己的属性值
changeTitle(title) {
    this.setState({
        title
    })
}

render() {
    return (
        <div>
            title: {this.state.title}
        </div>
    );
}
}

components/Child2.jsx

import React, { Component } from 'react'

export default class Child2 extends Component {
  updateTitle = () => {
    this.props.onChangeTitle();
}
  render() {
    return (
      <div>
        <button onClick={this.updateTitle}>修改title</button>
      </div>
    )
  }
}

总结:

这种方式稍微感觉有点麻烦,如果组件嵌套过深,那么通信显得非常繁琐,会一层一层往上传递,然后又一层一层往下传递。

基于事件总线

事件总线的方式,其实就是一种设计模式,被称为订阅和发布模式,设计模式一前辈们总结出来的思想,设计模式有23三种,比如还有单例模式,策略模式,适配模式等等

使用 events 插件

当然利用 eventBus 也可以实现组件通信,但是在 React 中并不提倡用这种方式,我还是更提倡用 props 方式通信。如果说非要用 eventBus,我觉得它更适合用 React 做基础构建的小程序,比如 Taro。

在react没有内置事件总线的方式,我们需要下载第三方插件events

  • 触发事件:emit('事件名称', 参数)
  • 监听事件:addListener('事件名称', 监听函数)
  • 移除事件:removeLIstener('事件名称',  监听函数)

  1. 安装插件
yarn add events
# or
npm i events

  1. 创建eventBus文件
    src/utils/eventBus/eventBus.js
import { EventEmitter } from 'events';

export default new EventEmitter(); // 生成一个eventBus实例

  1. App.js
import './App.css';
import React from 'react';
import Parent from './components/Parent'

function App() {
  return (
    <div>
    <Parent/>
    </div>
  );
}

export default App;


components/Parent.jsx

import React, { Component } from "react";
import Child from "./Child";
import Child2 from "./Child2";
export default class Parent extends Component {
  

  render() {
    return (
      <div>
        <Child/>
        <Child2 />
      </div>
    );
  }
}

  1. 在组件中订阅和发布事件
    订阅事件:Child.jsx
    components/Child.jsx
import React, { Component } from "react";
import eventBus from "../utils/eventBus/eventBus.js";

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

  //子组件自定义更改年龄方法
  changeAge = (age) => {
    this.setState({
      age,
    });
  };

  componentDidMount() {
    // 订阅事件
    eventBus.addListener("updateAge", this.changeAge);
  }
  componentWillUnmount() {
    // 移除事件
    eventBus.removeListener('updateAge', this.changeAge)
	}
  render() {
    return (
      <div>
      age: {this.state.age}
      </div>
    )
  }
}


发布事件:Child2.jsx
components/Child2.jsx

import React, { Component } from 'react'
import eventBus from "../utils/eventBus/eventBus.js";

export default class Child2 extends Component {
  changeAge = () => {
    eventBus.emit('updateAge', 18)
}
  render() {
    return (
      <div>
      <button onClick={this.changeAge}>修改age</button>
      </div>
    )
  }
}

使用 hy-event-store 插件

安装:

yarn add hy-event-store
# or
npm i hy-event-store

App.jsx

import React, { Component } from 'react'
import Home from './Home'
import eventBus from './utils/event-bus'

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

    this.state = {
      name: "",
      age: 0,
      height: 0
    }
  }

  componentDidMount() {
    // eventBus.on("bannerPrev", (name, age, height) => {
    //   console.log("app中监听到bannerPrev", name, age, height)
    //   this.setState({ name, age, height })
    // })

    eventBus.on("bannerPrev", this.bannerPrevClick, this)
    eventBus.on("bannerNext", this.bannerNextClick, this)
  }

  bannerPrevClick(name, age, height) {
    console.log("app中监听到bannerPrev", name, age, height)
    this.setState({ name, age, height })
  }

  bannerNextClick(info) {
    console.log("app中监听到bannerNext", info)
  }

  componentWillUnmount() {
    eventBus.off("bannerPrev", this.bannerPrevClick)
  }

  render() {
    const { name, age, height } = this.state

    return (
      <div>
        <h2>App Component: {name}-{age}-{height}</h2>
        <Home/>
      </div>
    )
  }
}

export default App

Home.jsx

import React, { Component } from 'react'
import HomeBanner from './HomeBanner'

export class Home extends Component {
  render() {
    return (
      <div>
        <h2>Home Component</h2>
        <HomeBanner/>
      </div>
    )
  }
}

export default Home

HomeBanner.jsx

import React, { Component } from 'react'
import eventBus from "./utils/event-bus"

export class HomeBanner extends Component {
  prevClick() {
    console.log("上一个")

    eventBus.emit("bannerPrev", "why", 18, 1.88)
  }

  nextClick() {
    console.log("下一个")
    eventBus.emit("bannerNext", {nickname: "kobe", level: 99})
  }

  render() {
    return (
      <div>
        <h2>HomeBanner</h2>
        <button onClick={e => this.prevClick()}>上一个</button>
        <button onClick={e => this.nextClick()}>下一个</button>
      </div>
    )
  }
}

export default HomeBanner

utils/event-bus.js

import { HYEventBus } from "hy-event-store"

const eventBus = new HYEventBus()

export default eventBus

总结:

eventBus订阅发布的方式给组件之间直接通信仍然存在一些缺陷,订阅和发布事件绑定太多的话,会非常的分散,对于以后的维护来说不太好维护,你就不知道哪里在监听,哪里在触发,哪里要移除事件,不太好管理

我们可以使用其他的方式,例如redux,进行数据的管理

跨级通讯-context

上下文

在 vue 里是通过 provide inject ,但是在 react 中可以通过 context 实现类似的

基本概念

思考:App 组件要传递数据给 Child 组件,该如何处理?

处理方式:使用 props 一层层组件往下传递(繁琐)

更好的用法:使用 Context

作用:跨组件传递数据(比如:主题、语言等)

Context相当于一个公共的存储空间:

  • 我们可以将多个组件中需要访问的数据统一存储到一个Context中
  • 这样我们无需通过props逐层传递,即可使组件中访问到这些数据

相关API

React.createContext:

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

Context.Provider:

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

Class.contextType:

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

Context.Consumer:

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

什么时候会用到Context.Consumer?当使用value的组件是一个函数组件时、当组件中需要用到多个Context时

使用方式一

类组件中:

context/them-context.js

import React from 'react';
// 创建一个上下文
const ThemContext = React.createContext()

export default ThemContext

祖先组件:classHello

import React, { Component } from 'react';
import Home from './Home';
import ThemContext from './them-context/them-context'
class classHello extends Component {
  render() {
    return (
      <div>
        <ThemContext.Provider value={{color: 'red', size: '30'}}>
          <Home />
        </ThemContext.Provider>
      </div>
    );
  }
}

export default classHello;

父组件:Home

import React, { Component } from 'react'
import HomeInfo from './HomeInfo'
export class Home extends Component {
  render() {
    return (
      <div>
        Home
        <hr />
        <HomeInfo />
      </div>
    )
  }
}

export default Home

孙组件:HomeInfo

import React, { Component } from 'react'
import ThemContext from './them-context/them-context'
export class HomeInfo extends Component {
  
  render() {
    console.log(this.context);
    return (
      <div>HomeInfo:{this.context.color}</div>
    )
  }
}
HomeInfo.contextType = ThemContext
export default HomeInfo

函数组件中:

context/them-context.js

import React from 'react';
// 创建一个上下文
const ThemContext = React.createContext()

export default ThemContext

祖先组件:

import React, { Component } from 'react';
import Home from './Home';
import ThemContext from './them-context/them-context'
class classHello extends Component {
  render() {
    return (
      <div>
        <ThemContext.Provider value={{color: 'red', size: '30'}}>
          <Home />
        </ThemContext.Provider>
      </div>
    );
  }
}

export default classHello;

父组件:

import React, { Component } from 'react'
import HomeInfo from './HomeInfo'
export class Home extends Component {
  render() {
    return (
      <div>
        Home
        <hr />
        <HomeInfo />
      </div>
    )
  }
}

export default Home

孙组件:

import React from 'react'
import ThemContext from './them-context/them-context'
export default function HomeBanner() {
  return (
    <div>
      HomeBanner(函数组件):
      {
        <ThemContext.Consumer>
          {
            value => {
              return <span>{value.color}</span>
            }
          }
        </ThemContext.Consumer>
      }
    </div>
  )
}

多个context:

context/them-context.js

import React from 'react';
// 创建一个上下文
const ThemContext = React.createContext()

export default ThemContext

context/user-context.js

import React from 'react';
// 创建一个上下文
const UserContext = React.createContext()

export default UserContext

祖先组件:

import React, { Component } from 'react';
import Home from './Home';
import ThemContext from './context/them-context'
import UserContext from './context/user-context'
class classHello extends Component {
  render() {
    return (
      <div>
      <UserContext.Provider value={{nickName: 'zs'}}>
        <ThemContext.Provider value={{color: 'red', size: '30'}}>
          <Home />
        </ThemContext.Provider>
      </UserContext.Provider>
      </div>
    );
  }
}

export default classHello;

父组件:

import React, { Component } from 'react'
import HomeInfo from './HomeInfo'
import HomeBanner from './HomeBanner'
export class Home extends Component {
  render() {
    return (
      <div>
        Home
        <hr />
        <HomeInfo />
        <HomeBanner />
      </div>
    )
  }
}

export default Home

孙组件:

import React, { Component } from 'react'
import ThemContext from './context/them-context'
import UserContext from './context/user-context'
export class HomeInfo extends Component {
  
  render() {
    console.log(this.context);
    return (
      <div>
        <p>HomeInfo(类组件):{this.context.color}</p>
        <UserContext.Consumer>
          {
            value => {
              return <h2>Info User:{value.nickName}</h2>
            }
          }
        </UserContext.Consumer>
      </div>
    )
  }
}
HomeInfo.contextType = ThemContext
export default HomeInfo

孙组件:

import React from 'react'
import ThemContext from './context/them-context'
export default function HomeBanner() {
  return (
    <div>
      HomeBanner(函数组件):
      {
        <ThemContext.Consumer>
          {
            value => {
              return <span>{value.color}</span>
            }
          }
        </ThemContext.Consumer>
      }
    </div>
  )
}

使用方式二

步骤:

1.引入context组件
   import TestContext from '../store/testContext';
 2.使用 Xxx(模块名).Consumer 组件来创建元素
   Consumer的标签体需要一个回调函数
   它会将context设置为回调函数的参数,通过参数就可以访问到context对象

- 回调函数中,形参ctx就是Context中的对象

store/testContext.jsx

import React from "react";

const TestContext = React.createContext({
  name: "孙悟空",
  age: 18,
});

export default TestContext;

App.js

import React, { Component } from 'react'
import TestContext from './store/testContext';
export default class App extends Component {
  render() {
    return <TestContext.Consumer>
        {
            (ctx) => {
                return <div>
                    {ctx.name} - {ctx.age}
                </div>
            }
        }
    </TestContext.Consumer>
  }
}

使用方式三

App.js

import React, { Component } from 'react'
import Father from './components/Father'
// 1. createContext
// 2. Provider 包裹根元素,Provider组件就是最大的根元素
const {Provider, Consumer}= React.createContext()
export { Consumer }

export default class App extends Component {
  state = {
    color: 'green'
  }
  
  render() {
    return (
      <Provider value={this.state.color}>
        <div>
          <Father />
        </div>
      </Provider>
    )
  }
}

components/Father.jsx

import React, { Component } from 'react'
import Son from './Son'

export default class Father extends Component {
  render() {
    return (
      <div>
        <h3>Father</h3>
        <Son />
      </div>
    )
  }
}

components/Son.jsx

import React, { Component } from 'react'
import Sun from './Sun'
import {Consumer} from '../App'

export default class Son extends Component {
  render() {
    return (
      <div>
        <Consumer>
          {
            (value) => <h3>Son--{value}</h3>
          }
        </Consumer>
        <Sun />
      </div>
    )
  }
}

components/Sun.jsx

import React, { Component } from 'react'
import {Consumer} from '../App'
export default class Sun extends Component {
  render() {
    return (
      <div>
        <h3>Sun</h3>
        <Consumer>
            {
              (value) => <div style={{color: value}}>孙--{value}</div>
            }
        </Consumer>
      </div>
    )
  }
}

高级:

App.js

import React, { Component } from 'react'
import Father from './components/Father'
// 1. createContext
// 2. Provider 包裹根元素,Provider组件就是最大的根元素
const {Provider, Consumer}= React.createContext()
export { Consumer }

export default class App extends Component {
  state = {
    color: 'green',
    bgColor: 'pink'
  }
  changeColor = (color) => {
    this.setState({
      color
    })
  }
  
  render() {
    const {color, bgColor} = this.state
    return (
      <Provider value={{color, bgColor, changeColor:this.changeColor}}>
        <div>
          <Father />
        </div>
      </Provider>
    )
  }
}

components/Sun.jsx

import React, { Component } from 'react'
import {Consumer} from '../App'
export default class Sun extends Component {
  render() {
    return (
      <div>
        <h3>Sun</h3>
        <Consumer>
            {
              ({color, bgColor, changeColor}) => 
              <div>
              <div style={{color, backgroundColor: bgColor}}>孙--</div>
              <button onClick={() => changeColor('skyblue')}>修改</button>
              </div>
            }
        </Consumer>
      </div>
    )
  }
}

总结:

  1. 如果两个组件是远方亲戚(比如,嵌套多层)可以使用Context实现组件通讯
  1. Context提供了两个组件:Provider 和 Consumer
  1. Provider组件:用来提供数据
  1. Consumer组件:用来消费数据

对于复杂的组件通讯,将来会用  redux

状态管理方式

React-redux 或 React-mobx 状态管理方式。

对于这种方式在后面会有专门的章节去讲,这里就不去叙述了。

props深入

children属性

  • children属性:表示该组件的子节点,只要组件有子节点,props就有该属性
  • children 属性与普通的props一样,值可以是任意值(文本、React元素、组件,甚至是函数)
function Hello(props) {
  return (
    <div>
      该组件的子节点:{props.children}
    </div>
  )
}

<Hello>我是子节点</Hello>

props校验

  • 目的:校验接收的props的数据类型,增加组件的健壮性
  • 对于组件来说,props是外来的,无法保证组件使用者传入什么格式的数据
  • 如果传入的数据格式不对,可能会导致组件内部报错。组件的使用者不能很明确的知道错误的原因
  • 更多校验知识请参考官网:https://zh-hans.reactjs.org/docs/typechecking-width-proptypes.html

  • props校验允许在创建组件的时候,就约定props的格式、类型等

  • 作用:规定接收的props的类型必须为数组,如果不是数组就会报错,增加组件的健壮性。

通过静态属性设置propTypes属性去校验外部数据的类型

propTypes是固定的单词,不能写错了,写错了校验不生效。

如果是老版本,你需要手动下载prop-types插件

yarn add prop-types
  1. 导入 prop-types 包
  1. 使用组件名.propTypes = {} 来给组件的props添加校验规则
  1. 校验规则通过 PropTypes 对象来指定
import PropTypes from "prop-types"

MainBanner.propTypes = {
  banners: PropTypes.array.isRequired,
  title: PropTypes.string
}

类组件中

可以在类外部设置静态属性propTypes

import React, { Component } from 'react'
import PropTypes from 'prop-types';

class PropsClass extends Component {
    constructor(props) {
        super(props);
    }
  render() {
    return (
      <div>
        标题:{this.props.title}
      </div>
    )
  }
}

PropsClass.propTypes = {
    title: PropTypes.string.isRequired,
    isShow: PropTypes.bool
}

export default PropsClass;

也可以在类内部设置static静态属性propTypes

import React, { Component } from 'react'
import PropTypes from 'prop-types';

class PropsClass extends Component {

    static propTypes = {
        title: PropTypes.string,
        isShow: PropTypes.bool
    }

    constructor(props) {
        super(props);
    }
  render() {
    return (
      <div>
        标题:{this.props.title}
      </div>
    )
  }
}

export default PropsClass;

函数组件

import React from 'react'
import PropTypes from 'prop-types';

function Props(props) {

    console.log(props);

  return (
    <div>
        标题:{props.title}
        <br />
        姓名:{props.user.name}
        <br />
        年龄:{props.user.age}
        <br />
        地址:{props.user.address}
    </div>
  )
}

Props.propTypes = {
    title: PropTypes.string.isRequired
}

export default Props;

约束规则

  1. 常见类型:array、bool、func、number、object、string
  1. React元素类型:element
  1. 必填项:isRequired
  1. 特定结构的对象:shape({})
// 常见类型
optionalFunc: PropTypes.func,
// 必选
requiredFunc: PropTypes.func.isRequired,
// 特定结构的对象
optionalObjectWithShape: PropTypes.shape({
	color: PropTypes.string,
	fontSize: PropTypes.number
})

props默认值

场景:分页组件  每页显示条数
作用:给 props 设置默认值,在未传入 props 时生效

类组件中使用props默认值

  • 通过静态属性defaultProps设置外部数据的默认值
  • defaultProps这个单词是固定的,不能写错,写错了默认值不生效。

1)在类里面通过static设置静态属性

import React, { Component } from 'react'

class PropsClass extends Component {

     static defaultProps = {
         title: '标题'
     }

    constructor(props) {
        super(props);
    }
  render() {
    return (
      <div>
        标题:{this.props.title}
      </div>
    )
  }
}
export default PropsClass;

2)可以在类外部设置静态属性--推荐

import React, { Component } from 'react'

class PropsClass extends Component {

    constructor(props) {
        super(props);
    }
  render() {
    return (
      <div>
        标题:{this.props.title}
      </div>
    )
  }
}

PropsClass.defaultProps = {
    title: '标题'
}

export default PropsClass;

这两种方式是等价的,都是静态属性。

函数组件中使用props默认值

  • 外部数据props的默认值,需要在组件上添加静态属性defaultProps
  • defaultProps这个单词是固定的,不能写错,写错了默认值不生效。

import React from 'react'

function Props(props) {
  return (
    <div>
        标题:{props.title}
        <br />
        姓名:{props.user.name}
        <br />
        年龄:{props.user.age}
        <br />
        地址:{props.user.address}
    </div>
  )
}

Props.defaultProps = {
    title: '标题'
}
export default Props;

组件性能优化

  1. 功能第一
  1. 性能优化

减轻state

  • 减轻 state:只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等)
  • 注意:不用做渲染的数据不要放在 state 中,比如定时器 id等
  • 对于这种需要在多个方法中用到的数据,应该直接放在 this 中
    • this.xxx = 'bbb'
    • this.xxx

class Hello extends Component {
    componentDidMount() {
        // timerId存储到this中,而不是state中
        this.timerId = setInterval(() => {}, 2000)
    }
    componentWillUnmount() {
    	clearInterval(this.timerId)
    }
    render() { … }
}

vue中不要把和渲染无关的数据放到data中

避免不必要的重新渲染

  • 组件更新机制:父组件更新会引起子组件也被更新,这种思路很清晰
  • 问题:子组件没有任何变化时也会重新渲染 (接收到的props没有发生任何的改变)
  • 如何避免不必要的重新渲染呢?
  • 解决方式:使用钩子函数 shouldComponentUpdate(nextProps, nextState)
  • 作用:通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染
  • 触发时机:更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate => render)

class Hello extends Component {
    shouldComponentUpdate() {
        // 根据条件,决定是否重新渲染组件
        return false
    }
    render() {…}
}

案例:随机数

纯组件

  • 纯组件:React.PureComponentReact.Component功能相似
  • 区别:PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较
  • 原理:纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件

class Hello extends React.PureComponent {
    render() {
        return (
        	<div>纯组件</div>
        )
    }
}

只有在性能优化的时候可能会用到纯组件,不要所有的组件都使用纯组件,因为纯组件需要消耗性能进行对比

纯组件比较-值类型

  • 说明:纯组件内部的对比是 shallow compare(浅层对比)
  • 对于值类型来说:比较两个值是否相同(直接赋值即可,没有坑)

let number = 0
let newNumber = number
newNumber = 2
console.log(number === newNumber) // false

state = { number: 0 }
setState({
  number: Math.floor(Math.random() * 3)
})
// PureComponent内部对比:
最新的state.number === 上一次的state.number // false,重新渲染组件

纯组件比较-引用类型

  • 说明:纯组件内部的对比是 shallow compare(浅层对比)
  • 对于引用类型来说:只比较对象的引用(地址)是否相同

const obj = { number: 0 }
const newObj = obj
newObj.number = 2
console.log(newObj === obj) // true

state = { obj: { number: 0 } }
// 错误做法
state.obj.number = 2
setState({ obj: state.obj })
// PureComponent内部比较:
最新的state.obj === 上一次的state.obj // true,不重新渲染组件

纯组件的最佳实践:

注意:state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!

// 正确!创建新数据
const newObj = {...state.obj, number: 2}
setState({ obj: newObj })
// 正确!创建新数据
// 不要用数组的push / unshift 等直接修改当前数组的的方法
// 而应该用 concat 或 slice 等这些返回新数组的方法
this.setState({
	list: [...this.state.list, {新数据}]
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白小白从不日白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值