「React 進(jìn)階」 學(xué)好這些 React 設(shè)計(jì)模式,能讓你的 React 項(xiàng)目飛起來
一 前言
今天我們來悉數(shù)一下 React 中一些不錯(cuò)的設(shè)計(jì)模式,這些設(shè)計(jì)模式能夠解決一些功能復(fù)雜,邏輯復(fù)用 的問題,還能鍛煉開發(fā)者的設(shè)計(jì)和編程能力,以為多年開發(fā)經(jīng)驗(yàn)來看,學(xué)好這些設(shè)計(jì)模式,那就是一個(gè)字 香!
基本上每一個(gè)設(shè)計(jì)模式,筆者都會(huì)絞盡腦汁的想出兩個(gè) demo,希望屏幕前的你能給筆者賞個(gè)贊,以此鼓勵(lì)我繼續(xù)創(chuàng)作前端硬文。
老規(guī)矩,我們帶著疑問開始今天的閱讀:
1 React 的常見設(shè)計(jì)模式有哪些? 2 組合模式功能強(qiáng)大,都用于哪些場(chǎng)景。 3 render props 使用,開發(fā)者應(yīng)該注意些什么? 4 hoc 模式的應(yīng)用場(chǎng)景。 5 提供者模式實(shí)現(xiàn)狀態(tài)管理和狀態(tài)下發(fā)。 6 如何使用繼承模式,繼承模式的優(yōu)缺點(diǎn)是什么?
我相信讀完這篇文章,這些問題全都會(huì)迎刃而解。
首先我們想一個(gè)問題,那就是 為什么要學(xué)習(xí)設(shè)計(jì)模式? 原因我總結(jié)有以下幾個(gè)方面。
1 功能復(fù)雜,邏輯復(fù)用問題。首先 React 靈活多變性,就決定了 React 項(xiàng)目可以應(yīng)用多種設(shè)計(jì)模式。但是這些設(shè)計(jì)模式的產(chǎn)生也確實(shí)辦了實(shí)事:
場(chǎng)景一:
在一個(gè)項(xiàng)目中,全局有一個(gè)狀態(tài),可以稱之為 theme (主題),那么有很多 UI 功能組件需要這個(gè)主題,而且這個(gè)主題是可以切換的,就像 github 切換暗黑模式一樣,那么如何優(yōu)雅的實(shí)現(xiàn)這個(gè)功能呢?
這個(gè)場(chǎng)景如果我們用 React 的提供者模式,就能輕松搞定了,通過 context 保存全局的主題,然后將 theme 通過 Provider 形式傳遞下去,需要 theme ,那么消費(fèi) context ,就可以了,這樣的好處是,只要 theme 改變,消費(fèi) context 的組件就會(huì)重新更新,達(dá)到了切換主題的目的。
場(chǎng)景二:
表單設(shè)計(jì)場(chǎng)景也需要一定程度上的 React 的設(shè)計(jì)模式,首先對(duì)于表單狀態(tài)的整體驗(yàn)證需要外層的 Form 綁定事件控制,調(diào)度表單的狀態(tài)下發(fā),驗(yàn)證功能。內(nèi)層對(duì)于每一個(gè)表單控件還需要 FormItem 收集數(shù)據(jù),讓控件變成受控的。這樣的 Form 和 FormItem 方式,就是通過組合模式實(shí)現(xiàn)的。
2 培養(yǎng)設(shè)計(jì)能力,編程能力
熟練運(yùn)用 React 的設(shè)計(jì)模式,可以培養(yǎng)開發(fā)者的設(shè)計(jì)能力,比如 HOC 的設(shè)計(jì) ,公共組件的設(shè)計(jì) ,自定義 hooks 的設(shè)計(jì),一些開源的優(yōu)秀的庫就是通過 React 的靈活性和優(yōu)秀的設(shè)計(jì)模式實(shí)現(xiàn)的。
例子一:
比如在 React 狀態(tài)管理工具中,無論是 react-redux ,還是 mobx-react,一方面想要把 state 和 dispatch 函數(shù)傳遞給組件,另一方面訂閱 state 變化,來促使業(yè)務(wù)組件更新,那么整個(gè)流程中,需要一個(gè)或多個(gè) HOC 來搞定。于是 react-redux 提供了 connect,mobx-react 提供了 inject ,observer 等優(yōu)秀的 hoc。由此可見,學(xué)會(huì) React 的設(shè)計(jì)模式,有助于開發(fā)者小到編寫公共組件,大到開發(fā)開源項(xiàng)目。
今天我重點(diǎn)介紹 React 的五種設(shè)計(jì)模式,分別是:
組合模式 render props模式 hoc 模式 提供者模式 類組件繼承
二 組合模式
1 介紹
組合模式適合一些容器組件場(chǎng)景,通過外層組件包裹內(nèi)層組件,這種方式在 Vue 中稱為 slot 插槽,外層組件可以輕松的獲取內(nèi)層組件的 props 狀態(tài),還可以控制內(nèi)層組件的渲染,組合模式能夠直觀反映出 父 -> 子組件的包含關(guān)系,首先我來舉個(gè)最簡(jiǎn)單的組合模式例子??。
<Tabs onChange={ (type)=> console.log(type) } >
<TabItem name="react" label="react" >React</TabItem>
<TabItem name="vue" label="vue" >Vue</TabItem>
<TabItem name="angular" label="angular" >Angular</TabItem>
</Tabs>
如上 Tabs 和 TabItem 組合,構(gòu)成切換 tab 功能,那么 Tabs 和 TabItem 的分工如下:
Tabs 負(fù)責(zé)展示和控制對(duì)應(yīng)的 TabItem 。綁定切換 tab 回調(diào)方法 onChange。當(dāng) tab 切換的時(shí)候,執(zhí)行回調(diào)。 TabItem 負(fù)責(zé)展示對(duì)應(yīng)的 tab 項(xiàng),向 Tabs 傳遞 props 相關(guān)信息。
我們直觀上看到 Tabs 和 TabItem 并沒有做某種關(guān)聯(lián),但是卻無形的聯(lián)系起來。這種就是組合模式的精髓所在,這種組合模式的組件,給使用者感覺很舒服,因?yàn)榇蟛糠止ぷ?,都在開發(fā)組合組件的時(shí)候處理了。所以編寫組合模式的嵌套組件,對(duì)鍛煉開發(fā)者的 React 組件封裝能力是很有幫助的。
接下來我們一起看一下,組合模式內(nèi)部是如何實(shí)現(xiàn)的。
2 原理揭秘
實(shí)際組合模式的實(shí)現(xiàn)并沒有想象中那么復(fù)雜,主要分為外層和內(nèi)層兩部分,當(dāng)然可能也存在多層組合嵌套的情況,但是萬變不離其宗,原理都是一樣的。首先我們看一個(gè)簡(jiǎn)單的組合結(jié)構(gòu):
<Groups>
<Item name="《React進(jìn)階實(shí)踐指南》" />
</Groups>
那么 Groups 能對(duì) Item 做一些什么操作呢 ?
Item 在 Groups的形態(tài)
首先如果如上組合模式的寫法,會(huì)被 jsx 編譯成 React element 形態(tài),Item 可以通過 Groups 的 props.children 訪問到。
function Groups (props){
console.log( props.children ) // Groups element
console.log( props.children.props ) // { name : 'React進(jìn)階實(shí)踐指南》' }
return props.children
}
但是這是針對(duì)單一節(jié)點(diǎn)的情況,事實(shí)情況下,外層容器可能有多個(gè)子組件的情況。
<Groups>
<Item name="《React進(jìn)階實(shí)踐指南》" />
<Item name="《Nodejs深度學(xué)習(xí)手冊(cè)》" />
</Groups>
這種情況下,props.children 就是一個(gè)數(shù)組結(jié)構(gòu),如果想要訪問每一個(gè)的 props ,那么需要通過 React.Children.forEach 遍歷 props.children。
function Groups (props){
console.log( props.children ) // Groups element
React.Children.forEach(props.children,item=>{
console.log( item.props ) //依次打印 props
})
return props.children
}
隱式混入 props
這個(gè)是組合模式的精髓所在,就是可以通過 React.cloneElement 向 children 中混入其他的 props,那么子組件就可以使用容器父組件提供的特有的 props 。我們來看一下具體實(shí)現(xiàn):
function Item (props){
console.log(props) // {name: "《React進(jìn)階實(shí)踐指南》", author: "alien"}
return <div> 名稱: {props.name} </div>
}
function Groups (props){
const newChilren = React.cloneElement(props.children,{ author:'alien' })
return newChilren
}
用 React.cloneElement創(chuàng)建一個(gè)新的 element,然后混入其他的 props -> author 屬性,React.cloneElement 的第二個(gè)參數(shù),會(huì)和之前的 props 進(jìn)行合并 ( merge )。
這里還是 Groups 只有單一節(jié)點(diǎn)的情況,有些同學(xué)會(huì)問直接在原來的 children 基礎(chǔ)上加入新屬性不就可以了嗎?像如下這樣:
props.children.props.author = 'alien'
這樣會(huì)報(bào)錯(cuò),對(duì)于 props ,React 會(huì)進(jìn)行保護(hù),我們無法對(duì) props 進(jìn)行拓展。所以要想隱式混入 props ,只能通過 cloneElement來實(shí)現(xiàn)。
控制渲染
組合模式可以通過 children 方式獲取內(nèi)層組件,也可以根據(jù)內(nèi)層組件的狀態(tài)來控制其渲染。比如如下的情況:
export default ()=>{
return <Groups>
<Item isShow name="《React進(jìn)階實(shí)踐指南》" />
<Item isShow={false} name="《Nodejs深度學(xué)習(xí)手冊(cè)》" />
<div>hello,world</div>
{ null }
</Groups>
}
如上這種情況組合模式,只渲染 isShow = true的 Item 組件。那么外層組件是如何處理的呢?
實(shí)際處理這個(gè)很簡(jiǎn)單,也是通過遍歷 children ,然后通過對(duì)比 props ,選擇需要渲染的 children 。接下來一起看一下如何控制:
function Item (props){
return <div> 名稱: {props.name} </div>
}
/* Groups 組件 */
function Groups (props){
const newChildren = []
React.Children.forEach(props.children,(item)=>{
const { type ,props } = item || {}
if(isValidElement(item) && type === Item && props.isShow ){
newChildren.push(item)
}
})
return newChildren
}
通過 newChildren存放滿足要求的 React Element ,通過Children.forEach遍歷 children 。通過 isValidElement排除非 element 節(jié)點(diǎn);type指向Item函數(shù)內(nèi)存,排除非 Item 元素;獲取 isShow 屬性,只展示 isShow = true 的Item,最終效果滿足要求。
內(nèi)外層通信
組合模式可以輕松的實(shí)現(xiàn)內(nèi)外層通信的場(chǎng)景,原理就是通過外層組件,向內(nèi)層組件傳遞回調(diào)函數(shù) callback ,內(nèi)層通過調(diào)用 callback 來實(shí)現(xiàn)兩層組合模式的通信關(guān)系。
function Item (props){
return <div>
名稱:{props.name}
<button onClick={()=> props.callback('let us learn React!')} >點(diǎn)擊</button>
</div>
}
function Groups (props){
const handleCallback = (val) => console.log(' children 內(nèi)容:',val )
return <div>
{React.cloneElement( props.children , { callback:handleCallback } )}
</div>
}
Groups向Item組件中隱式傳入回調(diào)函數(shù)callback,將作為新的 props 傳遞。Item可以通過調(diào)用callback向Groups傳遞信息。實(shí)現(xiàn)了內(nèi)外層的通信。
復(fù)雜的組合場(chǎng)景
組合模式還有一種場(chǎng)景,在外層容器中,進(jìn)行再次組合,這樣組件就會(huì)一層一層的包裹,一次又一次的強(qiáng)化。這里舉一個(gè)例子:
function Item (props){
return <div>
名稱:{props.name} <br/>
作者:{props.author} <br/>
對(duì)大家說:{props.mes} <br/>
</div>
}
/* 第二層組合 -> 混入 mes 屬性 */
function Wrap(props){
return React.cloneElement( props.children,{ mes:'let us learn React!' } )
}
/* 第一層組合,里面進(jìn)行第二次組合,混入 author 屬性 */
function Groups (props){
return <Wrap>
{React.cloneElement( props.children, { author:'alien' } )}
</Wrap>
}
export default ()=>{
return <Groups>
<Item name="《React進(jìn)階實(shí)踐指南》" />
</Groups>
}
在 Groups組件里通過Wrap再進(jìn)行組合。經(jīng)過兩次組合,把author和mes混入到 props 中。

這種組合模式能夠一層層強(qiáng)化原始組件,外層組件不用過多關(guān)心內(nèi)層到底做了些什么? 只需要處理 children 就可以,同樣內(nèi)層 children 在接受業(yè)務(wù)層的 props 外,還能使用來自外層容器組件的狀態(tài),方法等。
3 注意細(xì)節(jié)
組合模式也有很多細(xì)節(jié)值得注意,首先最應(yīng)該想到的就是對(duì)于 children 的類型校驗(yàn),因?yàn)榻M合模式,外層容器組件對(duì) children 的屬性狀態(tài)是未知的。如果在不確定 children 的狀態(tài)下,如果直接掛載,就會(huì)出現(xiàn)報(bào)錯(cuò)等情況。所以驗(yàn)證 children 的合法性就顯得非常重要。
驗(yàn)證children
比如如下,本質(zhì)上形態(tài)是屬于 render props 形式。
<Groups>
{()=> <Item isShow name="《React進(jìn)階實(shí)踐指南》" />}
</<Groups>
上面的情況,如果 Groups 直接用 children 掛載的話。
function Groups (props){
return props.children
}
這樣的情況,就會(huì)報(bào) Functions are not valid as a React child 的錯(cuò)誤。那么需要在 Groups 做判斷,我們來一起看一下:
function Groups (props){
return React.isValidElement(props.children)
? props.children
: typeof props.children === 'function' ?
props.children() : null
}
首先判斷 children 是否是 React.element ,如果是那么直接渲染,如果不是,那么接下來判斷是否是函數(shù),如果是函數(shù),那么直接函數(shù),如果不是那么直接渲染 null就可以了。
綁定靜態(tài)屬性
現(xiàn)在還有一個(gè)暴露的問題是,外層組件和內(nèi)層組件通過什么識(shí)別身份呢?比如如下的場(chǎng)景:
<Groups>
<Item isShow name="《React進(jìn)階實(shí)踐指南》" />
<Text />
<Groups>
如下,Groups 內(nèi)部有兩個(gè)組件,一個(gè)是 Item ,一個(gè)是 Text ,但是只有 Item 是有用的,那么如何證明 Item 組件呢。那么我們需要給組件函數(shù)或者類綁定靜態(tài)屬性,這里可以統(tǒng)一用 displayName 來標(biāo)記組件的身份。
那么只需要這么做就可以了:
function Item(){ ... }
Item.displayName = 'Item'
那么在 Groups 中就可以找到對(duì)應(yīng)的 Item 組件,排除 Text 組件。具體可以通過 children 上的 type 屬性找到對(duì)應(yīng)的函數(shù)或者是類,然后判斷 type 上的 displayName 屬性找到對(duì)應(yīng)的 Item 組件,本質(zhì)上 displayName 主要用于調(diào)試,這里要記住組合方式,可以使用子組件的靜態(tài)屬性就可以了。 當(dāng)然也可以通過內(nèi)存空間相同的方式。
具體參考方式:
function Groups (props){
const newChildren = []
React.Children.forEach(props.children,(item)=>{
const { type ,props } = item || {}
if(isValidElement(item) && type.displayName === 'Item' ){
newChildren.push(item)
}
})
return newChildren
}
通過 displayName 屬性找到 Item。
4 實(shí)踐demo
接下來,我們來簡(jiǎn)單實(shí)現(xiàn)剛開始的 tab,tabItem 切換功能。
tab實(shí)現(xiàn)
const Tab = ({ children ,onChange }) => {
const activeIndex = useRef(null)
const [,forceUpdate] = useState({})
/* 提供給 tab 使用 */
const tabList = []
/* 待渲染組件 */
let renderChildren = null
React.Children.forEach(children,(item)=>{
/* 驗(yàn)證是否是 <TabItem> 組件 */
if(React.isValidElement(item) && item.type.displayName === 'tabItem' ){
const { props } = item
const { name, label } = props
const tabItem = {
name,
label,
active: name === activeIndex.current,
component: item
}
if(name === activeIndex.current) renderChildren = item
tabList.push(tabItem)
}
})
/* 第一次加載,或者 prop chuldren 改變的情況 */
if(!renderChildren && tabList.length > 0){
const fisrtChildren = tabList[0]
renderChildren = fisrtChildren.component
activeIndex.current = fisrtChildren.component.props.name
fisrtChildren.active = true
}
/* 切換tab */
const changeTab=(name)=>{
activeIndex.current = name
forceUpdate({})
onChange && onChange(name)
}
return <div>
<div className="header" >
{
tabList.map((tab,index) => (
<div className="header_item" key={index} onClick={() => changeTab(tab.name)} >
<div className={'text'} >{tab.label}</div>
{tab.active && <div className="active_bored" ></div>}
</div>
))
}
</div>
<div>{renderChildren}</div>
</div>
}
Tab.displayName = 'tab'
我寫的這個(gè) Tab,負(fù)責(zé)了整個(gè) Tab 切換的主要功能,包括 TabItem 的過濾,狀態(tài)收集,控制對(duì)應(yīng)的子組件展示。
首先通過 Children.forEach找到符合條件的TabItem。收集TabItem的 props,形成菜單結(jié)構(gòu)。找到對(duì)應(yīng)的 children,渲染正確的 children 。提供改變 tab 的方法 changeTab。displayName 標(biāo)記 Tab組件。這個(gè)主要目的方便調(diào)試。
TabItem 的實(shí)現(xiàn)
const TabItem = ({ children }) => {
return <div>{children}</div>
}
TabItem.displayName = 'tabItem'
這個(gè) demo 中的 TabItem 功能十分簡(jiǎn)單,大部分事情都交給 Tab 做了。
TabItem 做的事情是:
展示 children( 我們寫在 TabItem 里面的內(nèi)容 )綁定靜態(tài)屬性 displayName。
效果

5 總結(jié)
組合模式在日常開發(fā)中,用途還是比較廣泛的,尤其是在一些比較出色的開源項(xiàng)目中,組合模式的總結(jié)內(nèi)容如下:
組合模式通過外層組件獲取內(nèi)層組件 children ,通過 cloneElement 傳入新的狀態(tài),或者控制內(nèi)層組件渲染。 組合模式還可以和其他組件組合,或者是 render props,拓展性很強(qiáng),實(shí)現(xiàn)的功能強(qiáng)大。
總結(jié)流程圖如下:

三 render props模式
1 介紹
render props 模式和組合模式類似。區(qū)別不同的是,用函數(shù)的形式代替 children。函數(shù)的參數(shù),由容器組件提供,這樣的好處,將容器組件的狀態(tài),提升到當(dāng)前外層組件中,這個(gè)是一個(gè)巧妙之處,也是和組合模式相比最大的區(qū)別。
我們先來看一下一個(gè)基本的 render props 長(zhǎng)什么樣子:
export default function App (){
const aProps = {
name:'《React進(jìn)階實(shí)踐指南》'
}
return <Container>
{(cProps) => <Children {...cProps} { ...aProps } />}
</Container>
}
如上是 render props 的基本樣子??梢郧宄目吹剑?/p>
cProps為Container組件提供的狀態(tài)。aProps為App提供的狀態(tài)。這種模式優(yōu)點(diǎn)是,能夠給 App 的子組件 Container 的狀態(tài)提升到 App 的 render 函數(shù)中。然后可以組合成新的 props,傳遞給 Children,這種方式讓容器化的感念更顯而易見。
接下來我們研究一下 render props 原理和細(xì)節(jié)。
2 原理和細(xì)節(jié)
首先一個(gè)問題是 render props 這種方式到底適合什么場(chǎng)景,實(shí)際這種模式更適合一種,容器包裝,狀態(tài)的獲取??赡苓@么說有的同學(xué)不明白。那么一起看一下 context 中的 Consumer。就采用 render props 模式。
const Context = React.createContext(null)
function Index(){
return <Context.Consumer>
{(contextValue)=><div>
名稱:{contextValue.name}
作者:{contextValue.author}
</div>}
</Context.Consumer>
}
export default function App(){
const value = {
name:'《React進(jìn)階實(shí)踐指南》',
author:'我不是外星人'
}
return <Context.Provider value={value} >
<Index />
</Context.Provider>
}
我們看到 Consumer 就是一個(gè)容器組件,包裝即將渲染的內(nèi)容,然后通過 children render 函數(shù)執(zhí)行把狀態(tài) contextValue從下游向上游提取。
那么接下來模擬一下 Consumer 的內(nèi)部實(shí)現(xiàn)。
function myConsumer(props){
const contextValue = useContext(Context)
return props.children(contextValue)
}
如上就模擬了一個(gè) Consumer 功能,從 Consumer 的實(shí)現(xiàn)看 render props 本質(zhì)就是容器組件產(chǎn)生狀態(tài),再通過 children 函數(shù)傳遞下去。所以這種模式我們應(yīng)該更在乎的是,容器組件能提供些什么?
派生新狀態(tài)
相比傳統(tǒng)的組合模式,render props 還有一個(gè)就是靈活性,可以通過容器組件的狀態(tài)和當(dāng)前組件的狀態(tài)結(jié)合,派生出新的狀態(tài)。比如如下
<Container>
{(cProps) => {
const const nProps = getNewProps( aProps , cProps )
return <Children {...nProps} />
}}
</Container>
nProps 是通過當(dāng)前組件的狀態(tài) aProps 和 Container 容器組件 cProps ,合并計(jì)算得到的狀態(tài)。
反向狀態(tài)回傳
這種情況比較極端,筆者也用過這種方法,就是可以通過 render props 中的狀態(tài),提升到當(dāng)前組件中,也就是把容器組件內(nèi)的狀態(tài),傳遞給父組件。比如如下情況。
function GetContanier(props){
const dom = useRef()
const getDom = () => dom.current
return <div ref={dom} >
{props.children({ getDom })}
</div>
}
export default function App(){
/* 保存 render props 回傳的狀態(tài) */
const getChildren = useRef(null)
useEffect(()=>{
const childDom = getChildren.current()
console.log( childDom,'childDom' )
},[])
return <GetContanier>
{({getDom})=>{
getChildren.current = getDom
return <div></div>
}}
</GetContanier>
}

這是一個(gè)復(fù)雜的狀態(tài)回傳的場(chǎng)景,在 GetContanier將獲取元素的方法getDom通過 render props 回傳給父組件。父組件 App 通過 getChildren保存 render props 回傳的內(nèi)容,在useEffect調(diào)用 getDom 方法,打印內(nèi)容如下:
但是現(xiàn)實(shí)情況不可能是獲取一個(gè) dom 這么簡(jiǎn)單,真實(shí)情景下,回傳的內(nèi)容可能更加復(fù)雜。
3 注意問題
render props 的注意問題還是對(duì) children 的校驗(yàn),和組合模式不同的是,這種模式需要校驗(yàn) children 是一個(gè)函數(shù),只有是函數(shù)的情況下,才能執(zhí)行函數(shù),傳遞 props 。打一個(gè)比方:
function Container (props){
const renderChildren = props.children
return typeof renderChildren === 'function' ? renderChildren({ name:'《React進(jìn)階時(shí)間指南》' }) : null
}
export default function App(){
return <Container>
{(props)=> <div> 名稱 :{props.name} </div>}
</Container>
}
通過 typeof判斷children是一個(gè)函數(shù),如果是函數(shù),那么執(zhí)行函數(shù),傳遞 props 。
4 實(shí)踐demo
接下來我們實(shí)現(xiàn)一個(gè) demo。通過 render props 實(shí)現(xiàn)一個(gè)帶 loading 效果的容器組件,被容器組件包裹,會(huì)通過 props 回傳開啟 loading 的方法 ( 現(xiàn)實(shí)場(chǎng)景下,不一定會(huì)這么做,這里只是方便同學(xué)學(xué)習(xí) render props 模式 ) 。
容器組件 Container
function Container({ children }){
const [ showLoading, setShowLoading ] = useState(false)
const renderChildren = useMemo(()=> typeof children === 'function' ? children({ setShowLoading }) : null ,[children] )
return <div style={{ position:'relative' }} >
{renderChildren}
{showLoading && <div className="mastBox" >
{<SyncOutlined className="icon" spin twoToneColor="#52c41a" />}
</div>}
</div>
}
useState用于顯示 loading 效果,useMemo 用于執(zhí)行children函數(shù),把改變 state 的方法 setShowLoading 傳入 props 中。這里有一個(gè)好處就是當(dāng) useState 改變的時(shí)候,不會(huì)觸發(fā)children的渲染。通過 showLoading來顯示 loading 效果。
外層使用
export default function Index(){
const setLoading = useRef(null)
return <div>
<Container>
{({ setShowLoading })=>{
console.log('渲染')
setLoading.current = setShowLoading
return <div>
<div className="index1" >
<button onClick={() => setShowLoading(true)} >loading</button>
</div>
</div>
}}
</Container>
<button onClick={() => setLoading.current && setLoading.current(false)} >取消 loading </button>
</div>
}
通過直接調(diào)用 setShowLoading(true)顯示 loading 效果。用 useRef 保存狀態(tài) setShowLoading , Container外層也可以調(diào)用 setShowLoading 來讓 loading 效果消失。
效果

5 總結(jié)
接下來我們總結(jié)一下 render props 的特點(diǎn)。
容器組件作用是傳遞狀態(tài),執(zhí)行 children 函數(shù)。 外層組件可以根據(jù)容器組件回傳 props ,進(jìn)行 props 組合傳遞給子組件。 外層組件可以使用容器組件回傳狀態(tài)。
這種模式下的原理圖如下所示:

四 hoc 模式
1 介紹
hoc 高階組件模式也是 React 比較常用的一種包裝強(qiáng)化模式之一,高階函數(shù)是接收一個(gè)函數(shù),返回一個(gè)函數(shù),而所謂高階組件,就是接收一個(gè)組件,返回一個(gè)組件,返回的組件是根據(jù)需要對(duì)原始組件的強(qiáng)化。
我們來看一下 hoc 的通用模式。hoc 本質(zhì)上就是一個(gè)函數(shù)。
function Hoc (Component){
return class Wrap extends React.Component{
//---------
// 強(qiáng)化操作
//---------
render(){
return <Component { ...this.props } />
}
}
}
傳統(tǒng)的 HOC 模式如上,我們可以看清楚一個(gè)傳統(tǒng)的 HOC 做了哪些事。
1 HOC 本質(zhì)是一個(gè)函數(shù),傳入 Component,也就是原始組件本身。2 返回一個(gè)新的包裝的組件 Wrap ,我們可以在 Wrap 中做一些強(qiáng)化原始組件的事。 3 Wrap 中掛載原始組件本身 Component。
2 原理
接下來我們看一下 hoc 的具體實(shí)現(xiàn)原理。hoc 的實(shí)現(xiàn)有兩種方式,屬性代理和反向繼承。
屬性代理所謂正向?qū)傩源?,就是用組件包裹一層代理組件,在代理組件上,我們可以做一些,對(duì)源組件的代理操作。我們可以理解為父子組件關(guān)系,父組件對(duì)子組件進(jìn)行一系列強(qiáng)化操作。而 hoc 本身就是返回強(qiáng)化子組件的父組件。
function HOC(WrapComponent){
return class Advance extends React.Component{
state={
name: '《React 進(jìn)階實(shí)踐指南》',
author:'我不是外星人'
}
render(){
return <WrapComponent { ...this.props } { ...this.state } />
}
}
}
屬性代理特點(diǎn):
① 正常屬性代理可以和業(yè)務(wù)組件低耦合,零耦合,對(duì)于條件渲染和 props屬性增強(qiáng),只負(fù)責(zé)控制子組件渲染和傳遞額外的props就可以,所以無須知道,業(yè)務(wù)組件做了些什么。所以正向?qū)傩源?,更適合做一些開源項(xiàng)目的hoc,目前開源的HOC基本都是通過這個(gè)模式實(shí)現(xiàn)的。② 同樣適用于 class聲明組件,和function聲明的組件。③ 可以完全隔離業(yè)務(wù)組件的渲染,相比反向繼承,屬性代理這種模式??梢酝耆刂茦I(yè)務(wù)組件渲染與否,可以避免反向繼承帶來一些副作用,比如生命周期的執(zhí)行。 ④ 可以嵌套使用,多個(gè) hoc 是可以嵌套使用的,而且一般不會(huì)限制包裝HOC的先后順序。
反向繼承
反向繼承和屬性代理有一定的區(qū)別,在于包裝后的組件繼承了業(yè)務(wù)組件本身,所以我們我無須再去實(shí)例化我們的業(yè)務(wù)組件。當(dāng)前高階組件就是繼承后,加強(qiáng)型的業(yè)務(wù)組件。這種方式類似于組件的強(qiáng)化,所以你必須要知道當(dāng)前繼承的組件的狀態(tài),內(nèi)部做了些什么?
class Index extends React.Component{
render(){
return <div> hello,world </div>
}
}
function HOC(Component){
return class wrapComponent extends Component{ /* 直接繼承需要包裝的組件 */
}
}
export default HOC(Index)
① 方便獲取組件內(nèi)部狀態(tài),比如state,props ,生命周期,綁定的事件函數(shù)等 ② es6繼承可以良好繼承靜態(tài)屬性。我們無須對(duì)靜態(tài)屬性和方法進(jìn)行額外的處理。
3 功能及注意事項(xiàng)
上面介紹了 hoc 的二種實(shí)現(xiàn)方式,接下來看一下 hoc 能做些什么?以及 hoc 模式的注意事項(xiàng)。
HOC 的功能
對(duì)于屬性代理HOC,我們可以:
強(qiáng)化props & 抽離state。 條件渲染,控制渲染,分片渲染,懶加載。 劫持事件和生命周期。 ref控制組件實(shí)例。 添加事件監(jiān)聽器,日志
對(duì)于反向代理的HOC,我們可以:
劫持渲染,操縱渲染樹。 控制/替換生命周期,直接獲取組件狀態(tài),綁定事件。
如果你對(duì)上面的每一個(gè)功能的具體場(chǎng)景不清楚的話,建議看一下筆者的另外一篇文章:一文吃透React高階組件(HOC)
HOC 注意事項(xiàng)
1 謹(jǐn)慎修改原型鏈。 2 繼承靜態(tài)屬性,這里推薦一個(gè)庫 hoist-non-react-statics自動(dòng)拷貝所有的靜態(tài)方法。3 跨層級(jí)捕獲 ref,通過forwardRef轉(zhuǎn)發(fā)ref。4 render 中不要聲明 HOC,如果在 render 聲明 hoc,可能會(huì)造成組件反復(fù)掛載情況發(fā)生。
4 實(shí)踐demo
之前有同學(xué)在面試中,遇到了這樣一個(gè)問題,就是如果控制組件掛載的先后順序,比如如下的場(chǎng)景
export default function Index(){
return <div>
<ComponentA />
<ComponentB />
<ComponentC />
</div>
}
如上,有三個(gè)子組件,ComponentA ,ComponentB,ComponentC,現(xiàn)在期望執(zhí)行順序是 ComponentA 渲染完成,掛載 ComponentB ,ComponentB 渲染完成,掛載 ComponentC,也就是三個(gè)組件是按照先后順序渲染掛載的,那么如何實(shí)現(xiàn)呢?
實(shí)際上,這種情況完全可以用一個(gè) hoc 來實(shí)現(xiàn),那么接下來,請(qǐng)大家跟上我的思路實(shí)現(xiàn)這個(gè)場(chǎng)景。
首先這個(gè) hoc 是針對(duì)當(dāng)前 index 下面,ComponentA | ComponentB | ComponentC 一組 component 進(jìn)行功能強(qiáng)化。所以這個(gè) hoc 最好可以動(dòng)態(tài)創(chuàng)建,而且服務(wù)于當(dāng)前一組組件。那么可以聲明一個(gè)生產(chǎn) hoc 的函數(shù)工廠。
function createHoc(){
const renderQueue = [] /* 待渲染隊(duì)列 */
return function Hoc(Component){ /* Component - 原始組件 */
return class Wrap extends React.Component{ /* hoc 包裝組件 */
}
}
}
那么我們需要先創(chuàng)建一個(gè) hoc,作為這一組組件的使用。
使用:
const loadingHoc = createHoc()
知道了 hoc 的動(dòng)態(tài)產(chǎn)生,接下來具體實(shí)現(xiàn)一下這個(gè) hoc 。
function createHoc(){
const renderQueue = [] /* 待渲染隊(duì)列 */
return function Hoc(Component){
function RenderController(props){ /* RenderController 用于真正掛載原始組件 */
const { renderNextComponent ,...otherprops } = props
useEffect(()=>{
renderNextComponent() /* 通知執(zhí)行下一個(gè)需要掛載的組件任務(wù) */
},[])
return <Component {...otherprops} />
}
return class Wrap extends React.Component{
constructor(){
super()
this.state = {
isRender:false
}
const tryRender = ()=>{
this.setState({
isRender:true
})
}
if(renderQueue.length === 0) this.isFirstRender = true
renderQueue.push(tryRender)
}
isFirstRender = false /* 是否是隊(duì)列中的第一個(gè)掛載任務(wù) */
renderNextComponent=()=>{ /* 從更新隊(duì)列中,取出下一個(gè)任務(wù),進(jìn)行掛載 */
if(renderQueue.length > 0 ){
console.log('掛載下一個(gè)組件')
const nextRender = renderQueue.shift()
nextRender()
}
}
componentDidMount(){ /* 如果是第一個(gè)掛載任務(wù),那么需要 */
this.isFirstRender && this.renderNextComponent()
}
render(){
const { isRender } = this.state
return isRender ? <RenderController {...this.props} renderNextComponent={this.renderNextComponent} /> : <SyncOutlined spin />
}
}
}
}
分析一下主要流程:
首先通過 createHoc來創(chuàng)建需要順序加載的 hoc ,renderQueue存放待渲染的隊(duì)列。Hoc 接收原始組件 Component。RenderController用于真正掛載原始組件,用 useEffect 通知執(zhí)行下一個(gè)需要掛載的組件任務(wù),在 hooks 原理的文章中,我講過 useEffect 采用異步執(zhí)行,也就是說明,是在渲染之后,瀏覽器繪制已經(jīng)完成。Wrap 組件包裝了一層 RenderController,主要用于渲染更新任務(wù),isFirstRender證明是否是隊(duì)列中的第一個(gè)掛載任務(wù),如果是第一個(gè)掛載任務(wù),那么需要在componentDidMount開始掛載第一個(gè)組件。每一個(gè)掛載任務(wù)本質(zhì)上就是 tryRender方法,里面調(diào)用了 setState 來渲染RenderController。每一個(gè)掛載任務(wù)的函數(shù) renderNextComponent原理很簡(jiǎn)單,就是獲取第一個(gè)更新任務(wù),然后執(zhí)行就可以了。還有一些細(xì)節(jié)沒有處理,比如說繼承靜態(tài)屬性,ref 轉(zhuǎn)發(fā)等。
使用:
/* 創(chuàng)建 hoc */
const loadingHoc = createHoc()
function CompA(){
useEffect(()=>{
console.log('組件A掛載完成')
},[])
return <div>組件 A </div>
}
function CompB(){
useEffect(()=>{
console.log('組件B掛載完成')
},[])
return <div>組件 B </div>
}
function CompC(){
useEffect(()=>{
console.log('組件C掛載完成')
},[])
return <div>組件 C </div>
}
function CompD(){
useEffect(()=>{
console.log('組件D掛載完成')
},[])
return <div>組件 D </div>
}
function CompE(){
useEffect(()=>{
console.log('組件E掛載完成')
},[])
return <div>組件 E </div>
}
const ComponentA = loadingHoc(CompA)
const ComponentB = loadingHoc(CompB)
const ComponentC = loadingHoc(CompC)
const ComponentD = loadingHoc(CompD)
const ComponentE = loadingHoc(CompE)
export default function Index(){
const [ isShow, setIsShow ] = useState(false)
return <div>
<ComponentA />
<ComponentB />
<ComponentC />
{isShow && <ComponentD />}
{isShow && <ComponentE />}
<button onClick={()=> setIsShow(true)} > 掛載組件D ,E </button>
</div>
}
效果:


完美達(dá)成需求。
5 總結(jié)
HOC 在實(shí)際項(xiàng)目中,應(yīng)用還是很廣泛的,尤其是一些優(yōu)秀的開源項(xiàng)目中,這里總結(jié)了一下 HOC 的原理圖:
屬性代理
反向繼承

五 提供者模式
1 介紹
首先我們來思考一下,為什么 React 會(huì)有提供者這種模式呢?
帶著這個(gè)疑問,首先假設(shè)一個(gè)場(chǎng)景:在 React 的項(xiàng)目有一個(gè)全局變量 theme ( theme 可能是初始化數(shù)據(jù)交互獲得的,也有可能是切換主題變化的),有一些視圖 UI 組件(比如表單 input 框、 button 按鈕),需要 theme 里面的變量來做對(duì)應(yīng)的視圖渲染,現(xiàn)在的問題是怎么能夠把 theme 傳遞下去,合理分配到用到這個(gè) theme 的地方。
如果用 props 解決這個(gè)問題,那么需要通過 props 層層綁定,而且還要考慮 pureComponent, memo 策略的影響。
所以這個(gè)時(shí)候用提供者模式最好不過了。React 提供了 context ‘提供者’模式,具體模式是這樣的,React組件樹 Root 節(jié)點(diǎn),用 Provider 提供者注入 theme,然后在需要 theme的 地方,用 Consumer 消費(fèi)者形式取出theme,供給組件渲染使用即可,這樣減少很多無用功。用官網(wǎng)上的一句話形容就是Context 提供了一個(gè)無需為每層組件手動(dòng)添加 props,就能在組件樹間進(jìn)行數(shù)據(jù)傳遞的方法。
但是必須注意一點(diǎn)是,提供者永遠(yuǎn)要在消費(fèi)者上層,正所謂水往低處流,提供者一定要是消費(fèi)者的某一層父級(jí)。提供者模式的結(jié)構(gòu)圖如下:

2 用法介紹
對(duì)于提供者模式的用法,有老版本的 context 和新版本的 context 之分。接下來重點(diǎn)介紹一下兩種方式。
老版本提供者模式
在 React v16.3.0 之前,要實(shí)現(xiàn)提供者,就要實(shí)現(xiàn)一個(gè) React 組件,不過這個(gè)組件要做特殊處理。下面就是一個(gè)實(shí)現(xiàn)“提供者”的例子,組件名為 ThemeProvider:
提供者
class ThemeProvider extends React.Component {
getChildContext() {
return {
theme: this.props.value
}
}
render() {
return (
<div>
{ this.props.children }
</div>
);
}
}
ThemeProvider.childContextTypes = {
theme: PropTypes.object
}
需要實(shí)現(xiàn) getChildContext方法,用于返回?cái)?shù)據(jù)就是向子孫組件傳遞的上下文;需要定義 childContextTypes屬性,聲明“上下文”的結(jié)構(gòu)類型。
使用
<ThemeProvider value={ { color:'pink' } } >
<Index />
</ThemeProvider>
消費(fèi)者
const ThemeConsumer = (props, context) => {
const {color} = context.theme
return (
<p style={{color }}>
{props.children}
</p>
);
}
ThemeConsumer.contextTypes = {
theme: PropTypes.object
}
這里需要注意的是,需要通過 contextTypes指定將要消費(fèi)哪個(gè) context ,否則將無效。
新版本提供者模式
到了 React v16.3.0 的時(shí)候,新的 Context API 出來了,開發(fā)者可以創(chuàng)建一個(gè) Context , Context 上有兩個(gè)屬性就是 Provider 和 Consumer 。
Provider用于提供 context 。Consumer用于消費(fèi) context 。
那么接下來介紹一下具體如何使用,首先開發(fā)者需要用 createContext api 創(chuàng)建一個(gè) context。
const ThemeContext = React.createContext();
然后就是新版本 Provider 和 Consumer的實(shí)現(xiàn)。
新版提供者
function ThemeProvider(){
const theme = { color:'pink' }
return <ThemeContext.Provider value={ theme } >
<Index />
</ThemeContext.Provider>
}
通過 ThemeContext上的Provider傳遞主題信息theme。Index 是根部組件。
新版消費(fèi)者
function ThemeConsumer(props){
return <ThemeContext.Consumer>
{ (theme)=>{ /* render children函數(shù) */
const { color } = theme
return <p style={{color }}>
{props.children}
</p>
} }
</ThemeContext.Consumer>
}
Consumer 采用的就是上述講到的 render props 模式。 通過 Consumer 訂閱 context 變化,context 變化, render children 函數(shù)重新執(zhí)行。render children 函數(shù)中第一個(gè)參數(shù)就是保存的 context 信息。 在新版消費(fèi)者中,對(duì)于函數(shù)組件還有 useContext自定義 hooks ,對(duì)于類組件有contextType靜態(tài)屬性。
3 實(shí)踐demo
接下來我們實(shí)現(xiàn)一個(gè)提供者模式的實(shí)踐 demo ,通過動(dòng)態(tài) context 來讓消費(fèi) context 的 Consumer 動(dòng)態(tài)渲染。
const ThemeContext = React.createContext(null) // 創(chuàng)建一個(gè) context 上下文 ,主題顏色Context
function ConsumerDemo(){
return <div>
<ThemeContext.Consumer>
{
(theme) => <div style={{ ...theme}} >
<p>i am alien!</p>
<p>let us learn React!</p>
</div>
}
</ThemeContext.Consumer>
</div>
}
class Index extends React.PureComponent{
render(){
return <div>
<ConsumerDemo />
</div>
}
}
export default function ProviderDemo(){
const [ theme , setTheme ]= useState({ color:'pink' , background:'#ccc' })
return <div>
<ThemeContext.Provider value={theme} >
<Index />
</ThemeContext.Provider>
<button onClick={()=>setTheme({ color:'blue' , background:'orange' })} >點(diǎn)擊</button>
</div>
}
Provider 改變,消費(fèi)訂閱 Provider 的 Consumer 會(huì)重新渲染。
效果:

4 總結(jié)
提供者模式在日常開發(fā)中,用的頻率還是很高的,比如全局傳遞狀態(tài),保存狀態(tài)。這里用一幅圖總結(jié)提供者模式的原理。

六 類組件繼承
1 介紹
React 有十分強(qiáng)大的組合模式。我們推薦使用組合而非繼承來實(shí)現(xiàn)組件間的代碼重用
雖然 React 官方推薦用組合方式,而非繼承方式。但是也不是說明繼承這種方式?jīng)]有用武之地,繼承方式還是有很多應(yīng)用場(chǎng)景的。
在 class 組件盛行之后,我們可以通過繼承的方式進(jìn)一步的強(qiáng)化我們的組件。這種模式的好處在于,可以封裝基礎(chǔ)功能組件,然后根據(jù)需要去 extends 我們的基礎(chǔ)組件,按需強(qiáng)化組件,但是值得注意的是,必須要對(duì)基礎(chǔ)組件有足夠的掌握,否則會(huì)造成一些列意想不到的情況發(fā)生。
我們先來看一個(gè)
class Base extends React.Component{
constructor(){
super()
this.state={
name:'《React 進(jìn)階實(shí)踐之指南》'
}
}
componentDidMount(){}
say(){
console.log('base components')
}
render(){
return <div> hello,world <button onClick={ this.say.bind(this) } >點(diǎn)擊</button> </div>
}
}
class Index extends Base{
componentDidMount(){
console.log( this.state.name )
}
say(){ /* 會(huì)覆蓋基類中的 say */
console.log('extends components')
}
}
export default Index
Base為基礎(chǔ)組件,提供一些基礎(chǔ)的方法和功能,包括 UIIndex為基于 Base 繼承的組件,可以針對(duì) Index 做一些功能性的強(qiáng)化。
2 特性
繼承增強(qiáng)效果很優(yōu)秀。它的優(yōu)勢(shì)如下:
可以控制父類 render,還可以添加一些其他的渲染內(nèi)容; 可以共享父類方法,還可以添加額外的方法和屬性。
但是也有值得注意的地方,就是 state 和生命周期會(huì)被繼承后的組件修改。像上述 demo 中, Person 組件中的 componentDidMount 生命周期將不會(huì)被執(zhí)行。
3 實(shí)踐demo
接下來我們實(shí)現(xiàn)一個(gè)繼承功能,繼承的組件就是耳熟能詳?shù)?React-Router 中的 Route 組件,強(qiáng)化它,使它變成可以受到權(quán)限的控制。
當(dāng)頁面有權(quán)限,那么直接展示頁面內(nèi)容。 當(dāng)頁面沒有權(quán)限,那么展示無權(quán)限頁面。
代碼編寫
import { Route } from 'react-router'
const RouterPermission = React.createContext()
class PRoute extends Route{
static contextType = RouterPermission /* 使用 context */
constructor(...arg){
super(...arg)
const { path } = this.props
/* 如果有權(quán)限 */
console.log(this.context)
const isPermiss = this.context.indexOf(path) >= 0 /* 判斷是否有權(quán)限 */
if(!isPermiss) {
/* 修改 render 函數(shù),如果沒有權(quán)限,重新渲染一個(gè) Route ,ui 是無權(quán)限展示的內(nèi)容 */
this.render = () => <Route {...this.props} >
<div>暫無權(quán)限</div>
</Route>
}
}
}
export default (props)=>{
/* 模擬的有權(quán)限的路由列表 */
const permissionList = [ '/extends/a' , '/extends/b' ]
return <RouterPermission.Provider value={permissionList} >
<Index {...props} />
</RouterPermission.Provider>
}
在根組件傳入權(quán)限路由。通過 context 模式,保存的是存在權(quán)限的路由列表。這里模擬為 /extends/a和/extends/b。編寫 PRoute 權(quán)限路由,繼承 react-router中的Route組件。PRoute 通過 contextType消費(fèi)指定的權(quán)限上下文RouterPermission context。在 constructor中進(jìn)行判斷,如果有權(quán)限,那么不用做任何處理,如果沒有權(quán)限,那么重寫 render 函數(shù),用 Route 做一個(gè)展示容器,展示無權(quán)限的 UI 。
使用
function Test1 (){
return <div>權(quán)限路由測(cè)試一</div>
}
function Test2 (){
return <div>權(quán)限路由測(cè)試二</div>
}
function Test3(){
return <div>權(quán)限路由測(cè)試三</div>
}
function Index({ history }){
const routerlist=[
{ name:'測(cè)試一' ,path:'/extends/a' },
{ name:'測(cè)試二' ,path:'/extends/b' },
{ name:'測(cè)試三' ,path:'/extends/c' }
]
return <div>
{
routerlist.map(item=> <button key={item.path}
onClick={()=> history.push(item.path)}
>{item.path}</button> )
}
<PRoute component={Test1}
path="/extends/a"
/>
<PRoute component={Test2}
path="/extends/b"
/>
<PRoute component={Test3}
path="/extends/c"
/>
</div>
}
效果

可以看到,只有權(quán)限列表中的 [ '/extends/a' , '/extends/b' ]權(quán)限能展示,無權(quán)限提示暫無權(quán)限,完美達(dá)到效果。
4 總結(jié)
繼承模式的應(yīng)用前提是,你需要知道被繼承的組件是什么,內(nèi)部都有什么狀態(tài)和方法,對(duì)繼承的組件內(nèi)部的運(yùn)轉(zhuǎn)是透明的。接下來用一幅圖表示繼承模式原理。

七 總結(jié)
本章節(jié)講了 React 中常用的幾個(gè)設(shè)計(jì)模式。希望同學(xué)們看完可以手動(dòng)敲起來,把這些設(shè)計(jì)模式運(yùn)用到真實(shí)的項(xiàng)目中。
參考資料
「react進(jìn)階」一文吃透React高階組件(HOC)
React進(jìn)階實(shí)踐指南
