【React系列】父子组件通信—props属性传值

本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)

一. 认识组件的嵌套

组件之间存在嵌套关系:

  • 在之前的案例中,我们只是创建了一个组件App;
  • 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
  • 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
  • 再将这些组件组合嵌套在一起,最终形成我们的应用程序;

我们来分析一下下面代码的嵌套逻辑:

javascript 复制代码
import React, { Component } from 'react';

function Header() {
  return <h2>Header</h2>
}

function Main() {
  return (
    <div>
      <Banner/>
      <ProductList/>
    </div>
  )
}

function Banner() {
  return <div>Banner</div>
}

function ProductList() {
  return (
    <ul>
      <li>商品1</li>
      <li>商品2</li>
      <li>商品3</li>
      <li>商品4</li>
      <li>商品5</li>
    </ul>
  )
}

function Footer() {
  return <h2>Footer</h2>
}

export default class App extends Component {
  render() {
    return (
      <div>
        <Header/>
        <Main/>
        <Footer/>
      </div>
    )
  }
}

上面的嵌套逻辑如下,它们存在如下关系:

  • App组件是Header、Main、Footer组件的父组件;
  • Main组件是Banner、ProductList组件的父组件;

在开发过程中,我们会经常遇到需要组件之间相互进行通信:

  • 比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;
  • 又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给他们来进行展示;
  • 也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件;

总之,在一个React项目中,组件之间的通信是非常重要的环节;

父组件在展示子组件,可能会传递一些数据给子组件:

  • 父组件通过 属性=值 的形式来传递给子组件数据;
  • 子组件通过 props 参数获取父组件传递过来的数据;

二. 父组件传递子组件

2.1. 子组件是class组件

我们这里先演示子组件是class组件:

javascript 复制代码
import React, { Component } from 'react';

// 1.类子组件
class ChildCpn1 extends Component {
  constructor(props) {
    super();
    this.props = props;
  }

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

    return (
      <div>
        <h2>我是class的组件</h2>
        <p>展示父组件传递过来的数据: {name + " " + age + " " + height}</p>
      </div>
    )
  }
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn1 name="why" age="18" height="1.88" />
      </div>
    )
  }
}

按照上面的结构,我们每一个子组件都需要写构造器来完成:this.props = props;

其实呢,大可不必,因为我们可以调用super(props),我们来看一下Component的源码:

javascript 复制代码
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}
  • 这里我们先不关心contextupdater
  • 我们发现传入的props会被Component设置到this中(父类的对象),那么子类就可以继承过来;

所以我们的构造方法可以换成下面的写法:

javascript 复制代码
constructor(props) {
  super(props);
}

甚至我们可以省略,为什么可以省略呢?

如果不指定构造方法,则使用默认构造函数。对于基类,默认构造函数是:

javascript 复制代码
constructor() {}

对于派生类,默认构造函数是:

javascript 复制代码
constructor(...args) {
  super(...args);
}

为什么 constructor 中不是必须传入 props 也能使用

在进行React开发中,有一个很奇怪的现象:

  • 在调用super的时候,我没有传入props,但是在下面的render函数中我依然可以使用;
  • 如果你自己编写一个基础的类,可以尝试一下:这种情况props应该是undefined的;
javascript 复制代码
class ChildCpn extends Component {
  constructor(props) {
    super();
  }

  render() {
    const {name, age, height} = this.props;
    return (
      <h2>子组件展示数据: {name + " " + age + " " + height}</h2>
    )
  }
}

为什么这么神奇呢?

  • 我一直喜欢说:计算机中没有黑魔法;
  • 之所以可以,恰恰是因为React担心你的代码会出现上面这种写法而进行了一些骚操作
  • React不管你有没有通过superprops设置到当前的对象中,它都会重新给你设置一遍;

如何验证呢?

  • 这就需要通过源码来验证了;
  • React的源码packages中有提供一个Test Rendererpackage
  • 这个 package 提供了一个 React 渲染器,用于将 React 组件渲染成纯 JavaScript 对象,不需要依赖 DOM 或原生移动环境;

查看源码:

我们来看一下这个组件是怎么被创建出来的:

  • 我们找到其中的render函数;
  • render函数中有这样的一段代码;
  • 这个_instance实例就是组件对象;
  • 我们再看一下,它在哪里重新赋值的:
  • 这里还包括通过this._instance的方式回调生命周期函数;
  • 这里可以看到是在组件挂载之前(componentWillMount方法调用之前)就进行了props的保存,因此可以在rendercomponentDidMount生命周期方法中获取到props的值。

结论不管你写不写构造函数,甚至不传super(props)的情况下,React源码中都会主动帮你进行props属性的绑定骚操作,会绑定到当前调用的对象实例的this上。

2.2. 子组件是function组件

我们再来演练一下,如果子组件是一个function组件:

javascript 复制代码
function ChildCpn2(props) {
  const {name, age, height} = props;

  return (
    <div>
      <h2>我是function的组件</h2>
      <p>展示父组件传递过来的数据: {name + " " + age + " " + height}</p>
    </div>
  )
}

export default class App extends Component {
  render() {
    return (
      <div>
        <ChildCpn1 name="why" age="18" height="1.88"/>
        <ChildCpn2 name="kobe" age="30" height="1.98"/>
      </div>
    )
  }
}

functional组件相对来说比较简单,因为不需要有构造方法,也不需要有this的问题。

其实本质还是通过 props 传递事件点击函数而已

2.3. 参数验证 propTypes

对于传递给子组件的数据,有时候我们可能希望进行验证,特别是对于大型项目来说:

  • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证;
  • 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-types 库来进行参数验证;

从 React v15.5 开始,React.PropTypes 已移入另一个包中:prop-types

我们对之前的class组件进行验证:

javascript 复制代码
ChildCpn1.propTypes = {
  name: PropTypes.string,
  age: PropTypes.number,
  height: PropTypes.number
}

这个时候,控制台就会报警告:

javascript 复制代码
<ChildCpn1 name="why" age={18} height={1.88}/>

更多的验证方式,可以参考官网:https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html

  • 比如验证数组,并且数组中包含哪些元素;
  • 比如验证对象,并且对象中包含哪些key以及value是什么类型;
  • 比如某个原生是必须的,使用 requiredFunc: PropTypes.func.isRequired

如果没有传递,我们希望有默认值呢?

  • 我们使用defaultProps就可以了
javascript 复制代码
ChildCpn1.defaultProps = {
  name: "王小波",
  age: 40,
  height: 1.92
}

三. 子组件传递父组件

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

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

我们这里来完成一个案例:

  • 将计数器案例进行拆解;
  • 将按钮封装到子组件中:CounterButton
  • CounterButton发生点击事件,将内容传递到父组件中,修改counter的值;

案例代码如下:

javascript 复制代码
import React, { Component } from 'react';

function CounterButton(props) {
  const { operator, btnClick } = props;
  return <button onClick={btnClick}>{operator}</button>
}

export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  changeCounter(count) {
    this.setState({
      counter: this.state.counter + count
    })
  }

  render() {
    return (
      <div>
        <h2>当前计数: {this.state.counter}</h2>
        <CounterButton operator="+1" btnClick={e => this.changeCounter(1)} />
        <CounterButton operator="-1" btnClick={e => this.changeCounter(-1)} />
      </div>
    )
  }
}

四. 组件通信案例练习

我们来做一个相对综合的练习:

index.js代码:

javascript 复制代码
import React from "react";
import ReactDOM from 'react-dom';
import "./style.css";

import App from './App';

ReactDOM.render(<App/>, document.getElementById("root"));

App.js

javascript 复制代码
import React, { Component } from 'react';

import TabControl from './TabControl';

export default class App extends Component {
  constructor(props) {
    super(props);

    this.titles = ["流行", "新款", "精选"];

    this.state = {
      currentTitle: "流行"
    }
  }

  itemClick(index) {
    this.setState({
      currentTitle: this.titles[index]
    })
  }

  render() {
    return (
      <div>
        <TabControl titles={this.titles} itemClick={index => this.itemClick(index)} />
        <h2>{this.state.currentTitle}</h2>
      </div>
    )
  }
}

TabControl.js

javascript 复制代码
import React, { Component } from 'react'

export default class TabControl extends Component {
  constructor(props) {
    super(props);

    this.state = {
      currentIndex: 0
    }
  }

  render() {
    const {titles} = this.props;
    const {currentIndex} = this.state;

    return (
      <div className="tab-control">
        {
          titles.map((item, index) => {
            return (
              <div className="tab-item" onClick={e => this.itemClick(index)}>
                <span className={"title " + (index === currentIndex ? "active": "")}>{item}</span>
              </div>
            )
          })
        }
      </div>
    )
  }

  itemClick(index) {
    this.setState({
      currentIndex: index
    });
    this.props.itemClick(index);
  }
}

style.css

css 复制代码
.tab-control {
  height: 40px;
  line-height: 40px;
  display: flex;
}

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

.tab-control .title {
  padding: 3px 5px;
}

.tab-control .title.active {
  color: red;
  border-bottom: 3px solid red;
}

五. React插槽实现

5.1. 为什么使用插槽?

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

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

举个栗子:假如我们定制一个通用的导航组件 - NavBar

  • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
  • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
  • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
  • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;

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

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

我这里先提前给出NavBar的样式:

css 复制代码
.nav-bar {
  display: flex;
  height: 44px;
  line-height: 44px;
  text-align: center;
}

.nav-bar .left, .nav-bar .right {
  width: 80px;
  background: red;
}

.nav-bar .center {
  flex: 1;
  background: blue;
}

5.2. children实现

每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。

比如:

html 复制代码
<Welcome>Hello world!</Welcome>

Welcome 组件中获取 props.children,就可以得到字符串 Hello world!

javascript 复制代码
function Welcome(props) {
  return <p>{props.children}</p>;
}

当然,我们之前看过props.children的源码:

  • 如果只有一个元素,那么children指向该元素;
  • 如果有多个元素,那么children指向的是数组,数组中包含多个元素;

那么,我们的NavBar可以进行如下的实现:

javascript 复制代码
import React, { Component } from 'react';

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

export default class App extends Component {
  render() {
    return (
      <div>
        <NavBar>
          <div>返回</div>
          <div>购物街</div>
          <div>更多</div>
        </NavBar>
      </div>
    )
  }
}

5.3. props实现

通过children实现的方案虽然可行,但是有一个弊端:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;

另外一个种方案就是使用 props 实现:

  • 通过具体的属性名,可以让我们在传入和获取时更加的精准;
javascript 复制代码
import React, { Component } from 'react';

class NavBar extends Component {
  render() {
    const { leftSlot, centerSlot, rightSlot } = this.props;

    return (
      <div className="nav-bar">
        <div className="item left">{leftSlot}</div>
        <div className="item center">{centerSlot}</div>
        <div className="item right">{rightSlot}</div>
      </div>
    )
  }
}

export default class App extends Component {
  render() {
    const navLeft = <div>返回</div>;
    const navCenter = <div>购物街</div>;
    const navRight = <div>更多</div>;

    return (
      <div>
        <NavBar leftSlot={navLeft} centerSlot={navCenter} rightSlot={navRight} />
      </div>
    )
  }
}
相关推荐
crary,记忆15 小时前
微前端 - Module Federation使用完整示例
前端·react·angular
aiguangyuan1 天前
浅谈 React Hooks
react·前端开发
whatever who cares3 天前
React hook之userReducer
react.js·react
aiguangyuan3 天前
React Hooks 基础指南
react·前端开发
aiguangyuan3 天前
React 项目初始化与搭建指南
react·前端开发
aiguangyuan4 天前
React 组件异常捕获机制详解
react·前端开发
aiguangyuan4 天前
深入理解 JSX:React 的核心语法
react·前端开发
aiguangyuan5 天前
React 基础语法
react·前端开发
aiguangyuan6 天前
React 核心概念与生态系统
react·前端开发
aiguangyuan6 天前
React 18 生命周期详解与并发模式下的变化
react·前端开发