本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
一. 认识组件的嵌套
组件之间存在嵌套关系:
- 在之前的案例中,我们只是创建了一个组件App;
- 如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;
- 所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;
- 再将这些组件组合嵌套在一起,最终形成我们的应用程序;
我们来分析一下下面代码的嵌套逻辑:
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
组件:
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
的源码:
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;
}
- 这里我们先不关心
context
、updater
; - 我们发现传入的
props
会被Component
设置到this
中(父类的对象),那么子类就可以继承过来;
所以我们的构造方法可以换成下面的写法:
constructor(props) {super(props);
}
甚至我们可以省略,为什么可以省略呢?
如果不指定构造方法,则使用默认构造函数。对于基类,默认构造函数是:
constructor() {}
对于派生类,默认构造函数是:
constructor(...args) {super(...args);
}
为什么 constructor 中不是必须传入 props 也能使用
在进行React开发中,有一个很奇怪的现象:
- 在调用
super
的时候,我没有传入props
,但是在下面的render
函数中我依然可以使用; - 如果你自己编写一个基础的类,可以尝试一下:这种情况
props
应该是undefined
的;
class ChildCpn extends Component {constructor(props) {super();}render() {const {name, age, height} = this.props;return (<h2>子组件展示数据: {name + " " + age + " " + height}</h2>)}
}
为什么这么神奇呢?
- 我一直喜欢说:计算机中没有黑魔法;
- 之所以可以,恰恰是因为React担心你的代码会出现上面这种写法而进行了一些骚操作;
- React不管你有没有通过
super
将props
设置到当前的对象中,它都会重新给你设置一遍;
如何验证呢?
- 这就需要通过源码来验证了;
- React的源码
packages
中有提供一个Test Renderer
的package
; - 这个
package
提供了一个 React 渲染器,用于将 React 组件渲染成纯 JavaScript 对象,不需要依赖 DOM 或原生移动环境;
查看源码:
我们来看一下这个组件是怎么被创建出来的:
- 我们找到其中的
render
函数;
render
函数中有这样的一段代码;
- 这个
_instance
实例就是组件对象; - 我们再看一下,它在哪里重新赋值的:
- 这里还包括通过
this._instance
的方式回调生命周期函数; - 这里可以看到是在组件挂载之前(
componentWillMount
方法调用之前)就进行了props
的保存,因此可以在render
和componentDidMount
生命周期方法中获取到props
的值。
结论:不管你写不写构造函数,甚至不传super(props)
的情况下,React源码中都会主动帮你进行props
属性的绑定骚操作,会绑定到当前调用的对象实例的this
上。
2.2. 子组件是function组件
我们再来演练一下,如果子组件是一个function
组件:
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
组件进行验证:
ChildCpn1.propTypes = {name: PropTypes.string,age: PropTypes.number,height: PropTypes.number
}
这个时候,控制台就会报警告:
<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
就可以了
ChildCpn1.defaultProps = {name: "王小波",age: 40,height: 1.92
}
三. 子组件传递父组件
某些情况,我们也需要子组件向父组件传递消息:
- 在vue中是通过自定义事件来完成的;
- 在React中同样是通过
props
传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可;
我们这里来完成一个案例:
- 将计数器案例进行拆解;
- 将按钮封装到子组件中:
CounterButton
; CounterButton
发生点击事件,将内容传递到父组件中,修改counter
的值;
案例代码如下:
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代码:
import React from "react";
import ReactDOM from 'react-dom';
import "./style.css";import App from './App';ReactDOM.render(<App/>, document.getElementById("root"));
App.js
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
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
.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对于这种需要插槽的情况非常灵活;
- 有两种方案可以实现:
children
和props
;
我这里先提前给出NavBar
的样式:
.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
:它包含组件的开始标签和结束标签之间的内容。
比如:
<Welcome>Hello world!</Welcome>
在 Welcome
组件中获取 props.children
,就可以得到字符串 Hello world!
:
function Welcome(props) {return <p>{props.children}</p>;
}
当然,我们之前看过props.children
的源码:
- 如果只有一个元素,那么
children
指向该元素; - 如果有多个元素,那么
children
指向的是数组,数组中包含多个元素;
那么,我们的NavBar
可以进行如下的实现:
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
实现:
- 通过具体的属性名,可以让我们在传入和获取时更加的精准;
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>)}
}