在 iOS 開發(fā)領(lǐng)域工作3年半了,都說金三銀四,今天四月最后一天,工作還沒有落實(shí)。想當(dāng)初還是個(gè)小白的時(shí)候,一天能安排4、5個(gè)面試,如今找工作如此之難,真是慌得一比。當(dāng)然,不能怨天尤人,總歸是自己實(shí)力不夠硬朗。
最近閑來無(wú)事,撿起了快被自己忘的一干二凈的 ReactNative ,簡(jiǎn)單了寫了個(gè)小項(xiàng)目,算是溫故知新了。
Redux
Redux 單項(xiàng)數(shù)據(jù)流框架,其特點(diǎn)是嚴(yán)格的數(shù)據(jù)流控制,開發(fā)過程中應(yīng)遵循3個(gè)原則:
- 唯一數(shù)據(jù)源
- 保持狀態(tài)只讀
- 數(shù)據(jù)改變只能通過純函數(shù)
1. 唯一數(shù)據(jù)源
Redux 應(yīng)用中應(yīng)保持?jǐn)?shù)據(jù)源的唯一性,說白了整個(gè)應(yīng)用中只保持一個(gè) Store,所有組件的數(shù)據(jù)源就是這個(gè) Store 上的狀態(tài),Store 是個(gè)樹形結(jié)構(gòu),往往某一個(gè)組件或者模塊的數(shù)據(jù)來源于 Store 上的某個(gè)節(jié)點(diǎn)。
2. 保持狀態(tài)只讀
Redux 強(qiáng)調(diào)要改變 Store 的狀態(tài)只能通過 action ,action 返回對(duì)象,提供給 Redux 完成新的狀態(tài)的組裝。
3. 數(shù)據(jù)改變只能通過純函數(shù)
這里的純函數(shù)就是 Reducer ,其函數(shù)簽名包含兩個(gè)參數(shù) state、action,顧名思義就是通過 action 去改變 state,一般來說是返回一個(gè)新的 state,其函數(shù)簽名大致如下export default (state = initialState, action) => {}。
總結(jié)一下
Reudx 包含 Store、State、Reudcer、action,主要為這四部分組成:
- Store
一個(gè)全局的對(duì)象, 其包含 Reudcer ,是一個(gè)樹形結(jié)構(gòu),每個(gè) Reducer 算一個(gè)節(jié)點(diǎn)。 - Reducer
返回一個(gè)新的狀態(tài),簡(jiǎn)單來說,通過什么樣的 action 產(chǎn)生一個(gè)改變了某一個(gè)節(jié)點(diǎn)的 State - State
狀態(tài)樹,同樣樹形結(jié)構(gòu),可以理解為 Reducer 下面的子節(jié)點(diǎn),state 的設(shè)計(jì)應(yīng)盡量扁平,一個(gè)模塊控制一個(gè)狀態(tài)節(jié)點(diǎn)、避免冗余數(shù)據(jù)。 - action
返回一個(gè)對(duì)象, 供 Reducer 使用。 action 函數(shù)返回的對(duì)象大致如下{ type: actionType, data: ooxx };這里說一下 actionType:一個(gè)常亮,用來區(qū)分不同的 action
聰明組件&傻瓜組件(容器組件&展示組件)
所謂聰明、傻瓜只是相對(duì)來說,同樣也叫容器組件和展示組件。 鑒于專業(yè)性,下文一律采用容器組件和展示組件的叫法。容器組件負(fù)責(zé)將 Store 中的狀態(tài),通過 props 傳遞給展示組件,展示組件只負(fù)責(zé)渲染頁(yè)面,無(wú)需持有狀態(tài)。將一個(gè)組件拆分為容器組件和展示組件是設(shè)計(jì) React 組件的一種模式,和 Redux 無(wú)關(guān)。
前兩者的結(jié)合 react-redux
上面講到,Redux 負(fù)責(zé)管理狀態(tài),組件拆分為容器組件和展示組件。容器組件需要狀態(tài),狀態(tài)來自哪呢?當(dāng)然是 Redux 。 故,可以將 Redux 和組件做一個(gè)結(jié)合,達(dá)到更好的效果,那就是 react-redux,他幫助開發(fā)者抽取了可復(fù)用的容器組件,開發(fā)者只需關(guān)注展示組件即可。
相比于 Redux ,react-redux 多了 Provider 和 connect 兩部分
Provider
提供了 Store 的容器組件,Provider 應(yīng)位于根組件最頂層,管理所有子組件。其中會(huì)檢查這一次渲染時(shí) Store 代表的 props 和上次是否一致,這樣做避免了多次渲染用了不同的 Store ,所以項(xiàng)目中應(yīng)只有一個(gè) Store。react-redux 提供了創(chuàng)建 Store 的方法。
connect
一個(gè)函數(shù),負(fù)責(zé)展示組件和容器組件的連接。大致是這樣export default connect(mapStateToProps, mapDispatchToProps)(SearchBar)
這里邊其實(shí)是兩次函數(shù)的執(zhí)行,首先 connect 函數(shù)執(zhí)行并返回了另一個(gè)函數(shù)然后執(zhí)行,參數(shù)是展示組件。
- mapStateToProps
一個(gè)返回對(duì)象的函數(shù),可選的。通過函數(shù)簽名可以知道是將 Store 中的某一個(gè) State 轉(zhuǎn)化為展示組件所需要的 props,決定暫時(shí)組件顯示什么樣的數(shù)據(jù)。 - mapDispatchToProps
一個(gè)返回對(duì)象的函數(shù),可選的。展示組件不能只負(fù)責(zé)展示,當(dāng)然也有一定的交互,也就是觸發(fā) action 。在 react-redux 中觸發(fā)一個(gè) action 是通過 dispatch 執(zhí)行的。所以這個(gè)函數(shù)是將 dispatch 轉(zhuǎn)換為 props 供展示組件使用。
總結(jié)一下
不難理解 react-redux 告訴我們,展示組件僅僅負(fù)責(zé)展示,不需要持有任何狀態(tài),展示組件的所有 state、action 全部來源于 props ,容器組件通過 props 傳遞給展示組件。
示例代碼
Demo地址
- actionType
export const HOMEPAGE_SHOWSEARCHBAR = 'HOMEPAGE/SHOWSEARCHBAR';
export const HOMEPAGE_MENU_PAGECHANGE = 'HOMEPAGE/MENU/PAGECHANGE';
- action
export const searchBarFetch = (text) => ({
type: HOMEPAGE_FETCH_SEARCHBAR,
text: text
});
export const updateHomePageMenuPage = (page) => ({
type: HOMEPAGE_MENU_PAGECHANGE,
currentPage: page
});
- State (我自認(rèn)為不是很扁平,懶著改了)
const initialState = {
homepage: {
menuInfo: {
items: common.menuInfos,
currentPage: 0
},
gridInfos: [],
sections: [{
title: '',
data: []
}],
},
searchBar: {
text: '搜一下'
}
};
- Reducer
Reducer 可以是多個(gè),一般一個(gè)模塊一個(gè) Reducer
export default (state = initialState, action) => {
switch (action.type) {
case HOMEPAGE_FETCH_SEARCHBAR:
{
return {
...state,
searchBar: {
text: action.text
}
};
}
case HOMEPAGE_MENU_PAGECHANGE:
{
return {
...state,
homepage: {
menuInfo: {
items: state.homepage.menuInfo.items,
currentPage: action.currentPage
},
gridInfos: state.homepage.gridInfos,
sections: state.homepage.sections
}
}
}
}
return state;
};
- Store
react-redux 提供了 合并多個(gè) Rducer 的方法,和創(chuàng)建 Store 的方法
const reducers = combineReducers({
homepageReudcer
});
export default createStore(reducers);
- 展示組件
class HomeMenu extends Component {
render() {
const {menuInfos, currentPage} = this.props;
const items = menuInfos.map(({title, icon}) => (<HomeMenuItem title={title} icon={icon} key={title}/>));
const pageCount = Math.ceil(items.length / 10);
let menuViews = [];
for (let i = 0; i < pageCount; i++) {
const itemSlices = items.slice(i * 10, i * 10 + 10);
const view = <View style={styles.itemsView} key={i}>
{itemSlices}
</View>
menuViews.push(view);
}
return (
<View style={styles.container}>
<ScrollView
horizontal
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
onScroll={this._onScroll}>
{menuViews}
</ScrollView>
<PageControl
style={styles.pageControl}
numberOfPages={pageCount}
currentPage={currentPage}
currentPageIndicatorTintColor={color.primary}
pageIndicatorTintColor={color.gray}/>
<View style={styles.line}/>
<HomeGridView/>
<View style={styles.line}/>
</View>
);
}
_onScroll = (event) => {
const x = event.nativeEvent.contentOffset.x;
const page = Math.round(x / common.screen.width);
const {currentPage, setCurrentPage} = this.props;
if (currentPage !== page) {
setCurrentPage(page);
}
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
},
itemsView: {
flexDirection: 'row',
flexWrap: 'wrap',
width: common.screen.width
},
pageControl: {
margin: 10
},
line: {
backgroundColor: color.paper,
width: common.screen.width,
height: 10,
}
});
const mapStateToProps = (state) => ({menuInfos: state.homepageReudcer.homepage.menuInfo.items, currentPage: state.homepageReudcer.homepage.menuInfo.currentPage});
const mapDispatchToProps = (dispatch) => ({
setCurrentPage: (page) => {
dispatch(updateHomePageMenuPage(page));
}
})
export default connect(mapStateToProps, mapDispatchToProps)(HomeMenu)