React數(shù)據(jù)流
在React中,數(shù)據(jù)是自頂向下單向流動的,即從父組件到子組件。這條原則讓組件之間的關(guān)系變得簡單且可預(yù)測。
state與props是React組件中最重要的概念,如果頂層組件初始化props,那么React會向下遍歷整棵組件樹,重新嘗試渲染所有相關(guān)的子組件。而state只關(guān)心每個組件自己內(nèi)部的狀態(tài),這些狀態(tài)只能在組件內(nèi)改變。把組件看成一個函數(shù),那么它接受了props作為參數(shù),內(nèi)部由state作為函數(shù)的內(nèi)部參數(shù),返回一個virtual DOM實現(xiàn)。
其中react有三個非常重要的概念:state、props與context。state其實應(yīng)該被稱為內(nèi)部狀態(tài)或是局部狀態(tài)?!皟?nèi)部”表示它很少"跑出"組件,狀態(tài)意味著它經(jīng)常發(fā)生改變。Props與context用于在組件中傳遞數(shù)據(jù),props僅僅支持逐層傳遞數(shù)據(jù),但是context則支持跨級傳遞。State、props與context都是react中的數(shù)據(jù)載體,他們都是各司其職,讓數(shù)據(jù)在組件中優(yōu)雅的變化和流動。
state
在使用React之前,常見的MVC框架也非常容易實現(xiàn)交互界面的狀態(tài)管理。在MVC框架將View中與界面交互的狀態(tài)解耦,一般將狀態(tài)放在Model中管理。但是React沒有結(jié)合Flux或是Redux框架前,React也同樣可以管理組件的內(nèi)部狀態(tài)。在React中,把這類狀態(tài)統(tǒng)一稱為state。
當組件內(nèi)部使用庫內(nèi)置的setState方法時,最大的表現(xiàn)行為就是該組件會嘗試重新渲染。這很好理解,因為我們改變了內(nèi)部的狀態(tài),組件需要更新了。我們編寫一個計數(shù)器組件:
import React, {Copmonent} from 'react';
class Counter extends Component{
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
this.state ={
count:0,
};
}
handleClick(e){
e.preventDefault();
this.setState({
count:this.state.count + 1,
});
}
render(){
return(
<div>
<p>{this.state.count}</p>
<a href='#' onClick = {this.handleClick}>更新</a>
</div>
);
}
}
在React中常常在事件處理方法中更新state,上面的例子中就是通過點擊“更新”按鈕不斷地更新內(nèi)部cout的值,這樣就可以把組件內(nèi)狀態(tài)封裝在實現(xiàn)中。
值得注意的是,setState是一個異步的方法,一個生命周期內(nèi)所有的setState方法會合并操作,有了這個特性,讓React變得充滿想象力,我們完全可以只用React來完成行為控制、數(shù)據(jù)的更新和界面的渲染。然而,隨著內(nèi)容的深入,我們發(fā)現(xiàn)官方并不推薦開發(fā)者濫用state。因為過多的內(nèi)部狀態(tài)會讓數(shù)據(jù)流混亂,數(shù)據(jù)變得難以維護。
我們再看一下Tabs組件的state,我們了解到應(yīng)該有兩個內(nèi)部狀態(tài)-- activeIndex和preIndex,這兩個狀態(tài)分別表示當前選中tab的索引和前一次選中的tab的索引,我們需要注意的是,當前選中的索引也是組件本身需要的參數(shù)之一。
我們針對activeIndex做為state,就有兩種不同的視角:
-
activeIndex在內(nèi)部更新:當我們切換tab標簽時,可以看做是組件內(nèi)部的交互行為,被選擇后通過回調(diào)函數(shù)返回具體選擇的索引。 -
activeIndex在外部更新:當我們切換tab標簽時,可以看做是組件外部在傳入具體的索引,而組件就像‘木偶’一樣被操作。
這兩種情況在React組件的設(shè)計非常的常見,我們形象的把第一種和第二種視角寫成的組件分別稱為智能組件(smart component)和木偶組件(dumb component)
實現(xiàn)組件的時候,可以同時考慮兼容這兩種。我們來看一下Tabs組件初始化時實現(xiàn)部分:
constructor (props){
super(props);
const currProps = this.props;
let activeIndex = 0;
if('activeIndex' in currProps){
activeIndex = currProps.activeIndex;
}else if('defaultActiveIndex' in currProps){
activeIndex = currProps.defaultActiveIndex;
}
this.state ={
activeIndex ,
preIndex:activeIndex ;
}
}
props
props是React中的另外的一個重要概念。props是React用來讓組件之間互相聯(lián)系的一種機制,通俗的說就像方法傳入?yún)?shù)一樣。
props的傳統(tǒng)過程,對于React組件來說是非常直觀的。React的單向數(shù)據(jù)流,主要的流動管道就是props。props本身是不可變的,當我們試圖改變props的原始值的時候,React會報出類型錯誤的警告,組件的props一定來自于 默認屬性或通過父組件傳遞而來。如果說要渲染一個對props加工后的值,最簡單的方法就是使用局部變量或直接在JSX中計算結(jié)果。
我們之前了解到Tabs組件的數(shù)據(jù)都是通過data prop傳入的,也就是<Tabs data = {data} />。那么Tabs組件的props還會有哪些,我們看一下下面的幾項:
-
className:根節(jié)點的class,為了方便覆蓋其原始樣式,我們都會在根節(jié)點上定義class。 -
classPrefix:class前綴,對于組件來說,定義一個統(tǒng)一的class前綴,對樣式與交互分離起了很重要的作用。 -
defaultActiveIndex和activeIndex:默認的激活索引。 -
onChange:回調(diào)函數(shù),當我們切換tab的時候,外組件需要知道組件的內(nèi)部信息,尤其是當前tab的索引號的信息,onChange一般與activeIndex搭配使用。
React為props同樣提供了默認配置,通過defaultProp靜態(tài)變量的方式來定義。當組件被調(diào)用的時候,默認值
保證渲染后始終有值。在render方法中,可以直接使用props的值來渲染。這里,我們只需要默認設(shè)置classPrefix和onChange即可。因為defaultActiveIndex和activeIndex,我們需要保持只去其中一個條件:
static defaultProps = {
classPrefix : 'tabs',
onchange:()=>{},
};
但是Tabs組件的信息全由一個對象傳進來的方式真的好嗎?對于React組件來說,我們考慮設(shè)計組件一定要滿足一大原則-- 直觀。把基本設(shè)置與數(shù)據(jù)一起定義成一個組件或?qū)ο笫浅鯇W(xué)者很容易犯的錯誤,對于React來說,如果組件是可以分解的,那么一定要將它進行分解,使用子組件的方式來進行處理。
我們仔細觀察一下Tabs組件在web界面的特征,一般來說,主要分成兩個區(qū)域:切換區(qū)域和內(nèi)容區(qū)域。那么我們根據(jù)上面說的,定義兩個區(qū)域:切換區(qū)域和內(nèi)容區(qū)域。TabNav組件對應(yīng)切換區(qū)域,TabContent組件對應(yīng)內(nèi)容區(qū)域。這兩個區(qū)域組件都存放一個有序的數(shù)組,都可以進行進一步的拆分, 具體的兩種組織方式如下:
- 在
Tabs組件的內(nèi)部把所有定義的子組件都顯示的展示出來。這么做的好處在于非常的易于理解,可以自定義的能力強,但是在調(diào)用的過程就會顯得笨重。React-Bootstrap和Material UI組件庫中的Tabs組件采用的就是這樣的方式,我們進行調(diào)用的方式如下:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0}>
<TabNav>
<TabHead>Tab 1</TabHead>
<TabHead>Tab 2</TabHead>
<TabHead>Tab 3</TabHead>
</TabNav>
<TabContent>第一個Tab里面的內(nèi)容</TabContent>
<TabContent>第二個Tab里面的內(nèi)容</TabContent>
<TabContent>第三個Tab里面的內(nèi)容</TabContent>
</Tabs>
- 在
Tabs組件內(nèi)置顯示定義內(nèi)容區(qū)域的子組件集合,頭部區(qū)域?qū)?yīng)內(nèi)部區(qū)域的每一個TabPane組件的props,讓其在TabNav組件內(nèi)拼裝。這種方式的調(diào)用寫法比較簡單,把復(fù)雜的邏輯留給了組件去實現(xiàn)。Ant Design組件庫中的Tabs組件采用的就是這種方式。調(diào)用方式如下形式:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0}>
<TabPane key ={0} tab = {'Tab 1'}>第一個Tab里面的內(nèi)容</TabPane>
<TabPane key ={1} tab = {'Tab 2'}>第二個Tab里面的內(nèi)容</TabPane>
<TabPane key ={2} tab = {'Tab 3'}>第三個Tab里面的內(nèi)容</TabPane>
</Tabs>
我們通過后面的一種方法進行具體的講述,當基本結(jié)構(gòu)確定后,我們需要看一下怎么渲染這個結(jié)構(gòu)的內(nèi)容。顯然,不能讓所以的參數(shù)都由Tabs組件來承載。只有兩個props放在了Tabs組件上面,而其他的參數(shù)直接放在了TabPane組件上面,由它的父組件TabContent隱式對TabPane組件的拼裝。
子組件prop
在React中有一個重要且內(nèi)置的prop-children,它代表了組件的子組件結(jié)合。children可以根據(jù)傳入子組件的數(shù)量來決定是否是數(shù)組類型。我們上面調(diào)用TabPane組件的過程,翻譯過來就是:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0} className = "tabs-bar"
children ={[
<TabPane key ={0} tab = {'Tab 1'}>第一個Tab里面的內(nèi)容</TabPane>
<TabPane key ={1} tab = {'Tab 2'}>第二個Tab里面的內(nèi)容</TabPane>
<TabPane key ={2} tab = {'Tab 3'}>第三個Tab里面的內(nèi)容</TabPane>
]}
>
</Tabs>
實現(xiàn)的基本思路就是以TabContent組件渲染TabPane子組件集合為例來講,其中渲染TabPane組件的方法如下:
getTabPanes(){
const {classPrefix, activeIndex, panels, isActive } = this.props;
return React.Children.map(panels, (child) =>{
if(!child){return;}
const order = parseInt(child.props.order, 10);
const isActive = activeIndex === order;
return React.cloneElement(child,{
classPrefix,
isActive,
children:child.props.children,
key:'tabPane - ${order}',
});
});
}
上面的代碼講述了子組件組合是怎么渲染的,通過React.Children.map方法遍歷子組件將order(渲染順序)、isActive(是否激活tab)、children(Tabs組件中傳下的children)和key利用React的cloneElement方法克隆到TabPane組件中,最后返回這個TabPane組件集合。這也是Tabs組件拼裝子組件的基本原理。
其中,React.children是React官方提供的一系列操作children的方法。它提供諸如map、forEach、count等實用函數(shù),可以為我們提供子組件提供便利。
最后,TabContent組件的render方法只需要調(diào)用getTabPanes方法就可以完成渲染:
render(){
return (<div>{this.getTabPanes()}</div>)
}
假如我們把render方法中的this.getTabPanes方法中對子組件的遍歷直接放進去,就會變成如下的形式
render(){
return (<div>{React.Children.map(this.props.children, (child) => {...})}</div>);
}
這種調(diào)用方式稱為Dynamic Children(動態(tài)子組件)。它指的是組件內(nèi)的子組件是通過動態(tài)計算得到的。就像上述對子組件的遍歷一樣,我們一樣可以對任何數(shù)據(jù)、字符串、數(shù)組或?qū)ο笞鲃討B(tài)計算。
用聲明式編程的方式來渲染數(shù)據(jù),這樣的做法和關(guān)心所有的細節(jié)的命令式編程相比,會讓我們輕松很多,當然,除了數(shù)據(jù)的map函數(shù)。還可以用其他使用的高階函數(shù),如reduce、filter等函數(shù)。值得注意的是,與map函數(shù)相似但不返回調(diào)用結(jié)果的forEach函數(shù)不能這么使用。
組件props
<TabPane key ={0} tab = {'Tab 1'}>第一個Tab里面的內(nèi)容</TabPane>
現(xiàn)在tab prop中傳入的是一個字符串。但是,如果我們傳入的是節(jié)點呢,是不是就可以自定義tab頭展示的形式了,這就是component props。對于子組件而言,我們不僅可以直接使用this.props.children定義,也可以將子組件以props的形式傳遞。一般我們會用這種方法來讓開發(fā)者定義組件的某一個prop,讓其具備多種類型,來做到簡單配置和自定義配置組合在一起的效果。
在Tabs組件中,我們就用到了這樣的功能,調(diào)用當時如下所示:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0} className = "tabs-bar">
<TabPane
order ='0'
tab = {<span><i className = "fa fa-home"> Home</i></span>}>
第一個Tab里面的內(nèi)容
</TabPane>
<TabPane
order ='1'
tab = {<span><i className = "fa fa-book"> Library</i></span>}>
第二個Tab里面的內(nèi)容
</TabPane>
<TabPane
order ='3'
tab = {<span><i className = "fa fa-home"> Application</i></span>}>
第三個Tab里面的內(nèi)容
</TabPane>
<Tabs>
我們也可以加入更多的自定義元素,可以是多行的,甚至可以插入動態(tài)數(shù)據(jù)。這聽上去有些復(fù)雜,但是實現(xiàn)的過程其實非常簡單,下面寫在TabNav組件中簡化的渲染子組件集合的方法:
getTabs(){
const {classPrefix, activeIndex, panels} = this.props;
return React.Children.map(panels, (child) =>{
if(!child){return;}
const order = parseInt(child.props.order, 10);
let classes = classnames ({
[`${classPrefix} - tab `] : true,
[`${classPrefix} - active`] : activeIndex === order,
[`${classPrefix} - disable`] : child.props.disable,
});
return (
<li>{child.props.tab}</li>
);
});
}
上面的方法和getTabPanes方法非常像,關(guān)鍵在于通過遍歷TabPane組件的tab prop來實現(xiàn)我們想要的功能,不論tab是以字符串的形式還是以虛擬元素的形式存在,都可以直接在<li>標簽中渲染出來。