縱觀react的tab組件中,即便是github上star數多的tab組件,實現原理都很是冗餘。html
例如Github上star數超四百星的react-tab,其在render的時候都會動態計算哪一個tab是被選中的,哪一個該被隱藏:react
getChildren() { let index = 0; let count = 0; const children = this.props.children; const state = this.state; const tabIds = this.tabIds = this.tabIds || []; const panelIds = this.panelIds = this.panelIds || []; let diff = this.tabIds.length - this.getTabsCount(); // Add ids if new tabs have been added // Don't bother removing ids, just keep them in case they are added again // This is more efficient, and keeps the uuid counter under control while (diff++ < 0) { tabIds.push(uuid()); panelIds.push(uuid()); } // Map children to dynamically setup refs return React.Children.map(children, (child) => { // null happens when conditionally rendering TabPanel/Tab // see https://github.com/rackt/react-tabs/issues/37 if (child === null) { return null; } let result = null; // Clone TabList and Tab components to have refs if (count++ === 0) { // TODO try setting the uuid in the "constructor" for `Tab`/`TabPanel` result = cloneElement(child, { ref: 'tablist', children: React.Children.map(child.props.children, (tab) => { // null happens when conditionally rendering TabPanel/Tab // see https://github.com/rackt/react-tabs/issues/37 if (tab === null) { return null; } const ref = `tabs-${index}`; const id = tabIds[index]; const panelId = panelIds[index]; const selected = state.selectedIndex === index; const focus = selected && state.focus; index++; return cloneElement(tab, { ref, id, panelId, selected, focus, }); }), }); // Reset index for panels index = 0; } // Clone TabPanel components to have refs else { const ref = `panels-${index}`; const id = panelIds[index]; const tabId = tabIds[index]; const selected = state.selectedIndex === index; index++; result = cloneElement(child, { ref, id, tabId, selected, }); } return result; }); }
getChildren每次都會在render裏面執行,雖然每次動態計算都會比較耗時,但這不是個大問題,真正讓人擔憂的是裏面用到的是cloneElement,cloneElement會生成新的實例對象,而這就會致使沒必要要的re-render(從新渲染)!!就算是銀彈頭pure render checking也無力挽回。git
難道一個小小的tab組件用react實現就這麼複雜嗎?jQuery也就沒幾行代碼,若是是這樣那還不如使用jQuery,ReactJS的組件優點又是什麼。。github
如今咱們迴歸到問題的本質,爲何要實現上面的代碼?上面的代碼實際上是動態給組件增長props屬性,例如給每一個TabTitle組件添加是否selected的狀態,由於組件內部沒法知道selected狀態,只能經過外部傳入,但每一個TabTitle組件又都須要這些組件,這就致使一個問題我要遍歷全部TabTitle組件,而後把屬性傳進去。像上面的代碼用在扁平結構的HTML標籤倒還好,例如:redux
<Tabs> <TabTitle to="1"> tab1 </TabTitle> <TabTitle to="2"> tab2 </TabTitle> <TabPanel for="1"> TabPanel1 </TabPanel> <TabPanel for="2"> TabPanel2 </TabPanel> </Tabs>
但若是我要支持組件組合使用,例以下面這樣:app
<Tabs onSelect={ this.onSelect } activeLinkStyle={ { color: 'red' } } defaultSelectedTab="2"> <div> <TabTitle to="1"> tab1 </TabTitle> </div> <div> <TabTitle to="2"> tab2 </TabTitle> </div> <div> <TabPanel for="1"> TabPanel1 </TabPanel> </div> <div> <TabPanel for="2"> TabPanel2 </TabPanel> </div> </Tabs>
上面的代碼其實應用場景更普遍,由於若是你沒法控制產品經理,他就會給你整這麼一出!ui
這樣的話前面的getChildren可能就要遞歸遍歷子元素查找,時間複雜度又增長了。this
即便解決了這麼個問題,若是個人產品裏一個tab裏面嵌套了另外一個tab,如何才能不讓它們衝突呢?spa
<Tab defaultSelectedTab="b"> <TabTitle label="a"> TabTitle a </TabTitle> <TabTitle label="b"> TabTitle b </TabTitle> <TabTitle label="c"> TabTitle c </TabTitle> <TabPanel for="a"> TabPanel a </TabPanel> <TabPanel for="b"> TabPanel b </TabPanel> <TabPanel for="c"> <Tab> <TabTitle label="a"> TabTitle a </TabTitle> <TabTitle label="b"> TabTitle b </TabTitle> <TabPanel for="a"> TabPanel a </TabPanel> <TabPanel for="b"> TabPanel b </TabPanel> </Tab> </TabPanel> </Tab>
尼瑪,這也太複雜了吧!!rest
若是單純只用state和props來處理就是這樣麻煩,就算是使用redux(雖然我並不推薦使用redux封裝組件)也要每次本身管理全局狀態。
Context to rescue
什麼是context?
context是react的一個高級技巧,經過它你能夠不用給每一個組件都傳props。具體解釋請看官方文檔: context
咱們的根組件的context屬性能夠在子元素任意位置下獲取到,利用這個特性咱們就能夠很輕易地實現上面說的組合組件和嵌套Tabs。
實現代碼的代碼能夠在個人github裏查看到,裏面還有可執行的·demo。也歡迎你們點贊~~
咱們把selectedTab放到context裏面,這樣子組件經過this.context.selectedTab是否和本身相同就能夠推斷出當前是否被激活了。
export default class Tabs extends Component { constructor(props, context) { super(props, context); this.state = { selectedTab: null }; this.firstTabLabel = null; } getChildContext(){ return { onSelect: this.onSelect.bind(this), selectedTab: this.state.selectedTab || this.props.defaultSelectedTab, activeStyle: this.props.activeLinkStyle || defaultActiveStyle, firstTabLabel: this.firstTabLabel }; } onSelect(tab, ...rest) { if(this.state.selectedTab === tab) return; this.setState({ selectedTab: tab }); if(typeof this.props.onSelect === 'function') { this.props.onSelect(tab, ...rest); } } findfirstTabLabel(children){ if (typeof children !== 'object' || this.firstTabLabel) { return; } React.Children.forEach(children, (child) => { if(child.props && child.props.label) { if(this.firstTabLabel == null){ this.firstTabLabel = child.props.label; return; } } this.findfirstTabLabel(child.props && child.props.children); }); } render() { this.findfirstTabLabel(this.props.children); return ( <div {...this.props}> {this.props.children} </div> ); } } Tabs.defaultProps = { onSelect: null, activeLinkStyle: null, defaultSelectedTab: '' }; Tabs.propTypes = { onSelect: PropTypes.func, activeLinkStyle: PropTypes.object, defaultSelectedTab: PropTypes.string }; Tabs.childContextTypes = { onSelect: PropTypes.func, selectedTab: PropTypes.string, activeStyle: PropTypes.object, firstTabLabel: PropTypes.string };
上面是Tab組件的實現代碼,咱們在context裏還增長了onSelect, activeStyle, 和firstTabLabel。
onSelect是指咱們自定義的onSelect事件, firstTabLabel主要是用來保存第一個Tab的label名稱的,若是使用者沒有指定默認tab就使用第一個。
接下來是TabTitle和TabPanel的實現:
const defaultActiveStyle = { fontWeight: 'bold' }; export class TabTitle extends Component { constructor(props, context){ super(props, context); this.onSelect = this.onSelect.bind(this); } onSelect(){ this.context.onSelect(this.props.label); } componentDidMount() { if (this.context.selectedTab === this.props.label || this.context.firstTabLabel === this.props.label) { this.context.onSelect(this.props.label); } } render() { let style = null; let isActive = this.context.selectedTab === this.props.label; if (isActive) { style = this.context.activeStyle; } return ( <div className={ this.props.className + (isActive ? ' active' : '') } style={style} onClick={ this.onSelect } > {this.props.children} </div> ); } } TabTitle.defaultProps = { label: '', className: 'tab-link' }; TabTitle.propTypes = { label: PropTypes.string.isRequired, className: PropTypes.string }; TabTitle.contextTypes = { onSelect: PropTypes.func, firstTabLabel: PropTypes.string, activeStyle: PropTypes.object, selectedTab: PropTypes.string };
const styles = { visible: { display: 'block' }, hidden: { display: 'none' } }; export class TabPanel extends Component { constructor(props, context){ super(props, context); } render() { let displayStyle = this.context.selectedTab === this.props.for ? styles.visible : styles.hidden; return ( <div className={ this.props.className } style={ displayStyle }> {this.props.children} </div> ); } } TabPanel.defaultProps = { for: '', className: 'tab-content' }; TabPanel.propTypes = { for: PropTypes.string.isRequired, className: PropTypes.string }; TabPanel.contextTypes = { selectedTab: PropTypes.string };
使用context後代碼量少多了,並且還實現了更復雜的功能,真是一箭雙鵰。
更多請參考個人github: https://github.com/LukeLin/react-tab/blob/master/index.js