一. 認識元件的嵌套
元件之間存在嵌套關系:
- 在之前的案例中,我們隻是建立了一個元件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的源碼:
- 這裡我們先不關心context、updater;
- 我們發現傳入的props會被Component設定到this中(父類的對象),那麼人類就可以繼承過來;
- 補充一個思考題:為什麼子類可以繼承過來呢?
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;
}
是以我們的構造方法可以換成下面的寫法:
constructor(props) {
super(props);
}
甚至我們可以省略,為什麼可以省略呢?
如果不指定構造方法,則使用預設構造函數。對于基類,預設構造函數是
constructor() {}
對于派生類,預設構造函數是:
constructor(...args) {
super(...args);
}
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的問題。
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的值;
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>
)
}
}
四. 元件通信案例練習
我們來做一個相對綜合的練習:
TabControl案例練習
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>
)
}
}
我是 @半糖學前端 ,專注前端技術領域分享,關注我和志同道合的人在一起,更容易進步!
源自: coderwhy