很久不見!(兩個多月沒更新內容,慚愧了三分鐘)。接下來的文章主要是開始對react的內容作一些整理(瘋狂立Flag)。本文的對象是Context
.react
在React中,數據傳遞通常使用props傳遞數據,維持單向數據流,這樣可讓組件之間的關係變得簡單且可預測,可是單項數據流在某些場景中並不適用,看一個官方給出的例子:
有三個組件APP
, Toolbar
,ThemedButton
,關係如圖:(爲了方便你們理解(偷懶),這個例子我會全文通用。)
編程
APP
存放着主題相關的參數theme
,須要傳遞組件ThemedButton
, 若是考慮使用props
,那麼代碼就長這樣:api
class App extends React.Component { render() { return <Toolbar theme="dark" />; // 1. 將theme傳遞給 } } function Toolbar(props) { // Toolbar 組件接受一個額外的「theme」屬性,而後傳遞給 ThemedButton 組件。 return ( <div> <ThemedButton theme={props.theme} /> // 2. 繼續往下傳遞給Button </div> ); } class ThemedButton extends React.Component { render() { return <Button theme={this.props.theme} />; // 最終獲取到參數 } }
能夠看到,實際上須要參數的是組件ThemedButton
,可是卻必須經過Toolbar
做爲中介傳遞。不妨再引伸思考一下:數據結構
ThemedButton
並不是末級子節點,那麼參數必須繼續向下傳遞App
中,還有除了<ThemedButton>
之外的組件,也須要theme
參數,那麼也必須按照這種形式逐層傳遞那麼數據結構圖大概如圖所示:
react-router
結構圖placeholder:層層傳遞ide
顯然,這樣作太!繁!瑣!了!函數式編程
接下來,就要介紹今天的主角--Context函數
Context 提供了一種在組件之間共享此類值的方式,而沒必要顯式地經過組件樹的逐層傳遞 props。
上面是官方對於context的介紹,簡單來講,就是能夠把context
當作是特定一個組件樹內共享的store,用來作數據傳遞。
爲何這裏要加粗強調組件樹呢?由於它是基於樹形結構共享的數據:在某個節點開啓提供context後,全部後代節點compoent均可以獲取到共享的數據。this
語言描述略顯抽象,直接上代碼:spa
如下介紹的是在react 16.x之前的傳統寫法
class App extends React.Component { // 核心代碼1: 首先在提供context的組件(即provider)裏 使用`getChildContext`定義要共享給後代組件的數據,同時使用`childContextTypes`作類型聲明 static childContextTypes = { theme: PropTypes.string }; getChildContext () { return { theme: 'dark' } } render() { return <Toolbar />; // 無需再將theme經過props傳遞 } } function Toolbar(props) { return ( <div> <ThemedButton /> // Toolbar 組件再也不接受一個額外的「theme」屬性 </div> ); } // 核心代碼2: 而後在須要使用context數據(即consumer)的節點,用`contextTypes`聲明須要讀取的context屬性,不然讀不到text class ThemedButton extends React.Component { static contextTypes = { theme: PropTypes.string } render() { return <h2>{this.context.theme}</h2>; // 直接從context獲取到參數 爲了直觀 這裏改用<h2>直接顯示出來 } }
這個結構圖就不畫了,顯然,就是把theme
從層層傳遞的props
中解放出來了。
在代碼中咱們提到了provider
和consumer
,這裏簡單解釋下:context
使用的生產者provider
- 消費者consumer
模式,
context
的叫作provider
,好比例子中的APP
,context
的稱爲consumer
,對應例子中的ThemedButton
。若是咱們在APP
組件提供了一個切換主題的按鈕,那就須要context
可以更新而且通知到相應的consumer
。
因爲context
自己提供了相關功能:
getChildContext
方法在每次state
和props
改變時會被調用;provider
改變了context
,全部的後代組件中的consumer
都會從新渲染。因此一般的方式是:將context
的數據保存在Provide
的state
屬性中,每次經過setState
更新對應的屬性時。
class App extends React.Component { static childContextTypes = { theme: PropTypes.string }; constructor(props) { super(props); this.state = {theme:'dark'}; } getChildContext () { return { theme: this.state.theme // 核心代碼,將`context`的值保存在`state` } } render() { return <Toolbar />; } }
可是官方文檔同時提到了這種方法是有隱患的,下一節進行詳細解析。
shouldComponentUpdate
再次強調,如下介紹的是在react 16.x之前的版本,關於context新的api會在後面介紹
官方文檔提到:
The problem is, if a context value provided by component changes, descendants that use that value won’t update if an intermediate parent returns false from shouldComponentUpdate.
(皇家翻譯上場) 拿前面的例子來講,咱們在第二節經過使用context
,將theme
的傳遞方式由本來的APP
->Toolbar
->ThemedButton
經過props
層層傳遞變成:
可是組件自己的層級關係依然是APP
->Toolbar
->ThemedButton
。若是咱們在中間層Toolbar
()
的生命週期shouldComponent
返回false
會怎麼樣呢?接下來咱們針對Toolbar
作一些改動
// 舊寫法 function Toolbar(props) { return ( <div> <ThemedButton /> </div> ); } // 新寫法 使用PureComponent render內容同樣, // PS:PureComponent內置的shouldComponentUpdate對state和props作了淺比較,這裏爲了省事直接使用 //若是不熟悉PureComponent能夠直接用React.Component,而後補上shouldComponentUpdate裏的 淺比較判斷 class Toolbar extends React.PureComponent { render(){ return ( <div> <ThemedButton /> </div> ); } }
這裏爲了省事,咱們直接使用了PureComponent
,接下來會發現:
每次APP
更新theme
的值時,ThemedButton沒法再取到變動後的theme
新的結構圖是這樣的(注意紅線表示來自toolbar
的抵抗):
如今問題來了:
因爲Toolbar
組件是PureComponent
,沒法重寫shouldComponentUpdate
,這就意味着位於Toolbar
以後的後代節點都沒法獲取到context
的更新!
第一種思路:首先,咱們先看看問題的根源之一,是context
更新以後,後代節點沒法及時獲取到更新,那麼若是context不發生更,那就不存在這個問題了.【我我的以爲這個思路有點相似於,解決不了問題,能夠考慮解決提出問題的人】,也就意味着:
immutable
constructor
函數中獲取一次context
。context
中保存具體的狀態值,而是隻利用它作個依賴注入。繞開SCU(shouldComponentUpdate)
,從根本上解決問題。 例如,能夠經過發佈訂閱模型建立一個自我管理的ThemeManage
類來解決問題。具體實現以下:// 核心代碼 class ThemeManager { constructor(theme) { this.theme = theme this.subscriptions = [] } // 變動顏色時 提示相關的訂閱者 setColor(theme) { this.theme = theme this.subscriptions.forEach(f => f()) } // 訂閱者接收到響應 觸發對應的callbck保證本身的及時更新 subscribe(f) { this.subscriptions.push(f) } } class App extends React.Component { static childContextTypes = { themeManager: PropTypes.object // 本次經過context傳遞一個theme對象 }; constructor(props) { super(props); this.themeManager = new ThemeManager('dark') // 核心代碼 } getChildContext () { return {theme: this.themeManager} // 核心代碼 } render() { return <Toolbar />; } } // Toolbar依然是個PureComponent class Toolbar extends React.PureComponent { render(){ return ( <div> <ThemedButton /> </div> ); } } class ThemedButton extends React.Component { constructor(){ super(); this.state = { theme: theme:this.context.themeManager.theme } } componentDidMount() { this.context.themeManager.subscribe(() => this.setState({ theme: this.context.themeManager.theme // 核心代碼 保證theme的更新 })) } render() { return <Button theme={this.state.theme} />; // 核心代碼 } }
OK,回頭看看咱們都幹了些什麼:
context
傳遞 theme
值,而是傳遞一個themeManager
注入對象,這個對象的特色是內置了狀態更新和消息通知的功能。ThemedButton
訂閱theme
的變化,而且利用setState
做爲回調函數,保證theme
值的及時更新。從而完美繞開了context
的傳遞問題。其實,它一樣符合咱們第一個解決方案:經過context
傳遞的對象,只被接受一次,而且後續都沒有更新(都是同一個themeManager
對象,更新是經過themeManager
內部的自我管理實現的。)
講完基本用法,接着聊聊context
在16.x版本以後的API。
先說一個好消息!使用新API後
每當 Provider(提供者) 的 value 屬性發生變化時,全部做爲 Provider(提供者) 後代的 consumer(使用者) 組件 都將從新渲染。 從Provider 到其後代使用者的傳播不受 shouldComponentUpdate 方法的約束,所以即便祖先組件退出更新,也會更新 consumer(使用者)
換句話說 若是使用context
的新API,第三節能夠跳過不看。(因此我把那一段寫前面去了)
在傳統版本,使用getChildContext
和childContextTypes
來使用context,而在16.x版本以後,前面的例子能夠改寫成這樣:
首先使用createContext
建立一個context,該方法返回一個對象,包含Provider
(生產者)和Consumer
(消費者)兩個組件:
const themeContext = React.createContext('light'); // 這裏light是默認值 後續使用時能夠改變
使用Provider
組件,指定context
須要做用的組件樹範圍
class App extends React.Component { render() { // 使用一個 Provider 來將當前的 theme 傳遞給如下的組件樹。 // 不管多深,任何組件都能讀取這個值。 // 在這個例子中,咱們將 「dark」 做爲當前的值傳遞下去。 return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ); } } // 中間的組件不再必指明往下傳遞 theme 了。 function Toolbar(props) { return ( <ThemedButton /> ); }
後代組件根據須要,指定contextType
須要做用的組件樹範圍
class ThemedButton extends React.Component { // 指定 contextType 讀取當前的 theme context。 // React 會往上找到最近的 theme Provider,而後使用它的值。 // 在這個例子中,當前的 theme 值爲 「dark」。 static contextType = ThemeContext; render() { return <Button theme={this.context} />; } } // 除了寫static contextType = ThemeContext 也能夠這樣寫: ThemedButton.contextType = ThemeContext;
固然,也能夠經過Consumer
組件指定消費者
class ThemedButton extends React.Component { static contextType = ThemeContext; render() { // Consumer的children必須是一個函數,傳遞的等於組件樹中層這個 context 最接近的 Provider 的對應屬性 <ThemeContext.Consumer> { theme =><Button theme={theme} />; // 核心代碼 } </ThemeContext.Consumer> } }
這兩種方式的主要區別是若是須要傳遞多個可能同名的context
時(例如這個例子中Toolbar
組件也經過context傳遞一個theme
屬性,而ThemedButton
須要的是從APP
來的theme),只能用Consumer
來寫
對於context
的使用,須要注意的主要是如下2點:
context
,由於react
重視函數式編程,講究複用,而使用了context
的組件,複用性大大下降react
,尤爲要注意context
在本身的可控範圍內,其實最大的問題也就是前面說的SUC
的問題context
的值變動時,Consumer
會受到相應的通知,所以要注意某些隱含非預期的變化,例如:// bad 示例, 由於每次render時{something: 'something'}都指向一個新對象(引用類型的值是老問題,不贅述了) class App extends React.Component { render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } } // good 示例 使用固定的變量存儲值 固然能夠選擇除了state之外的其餘變量 class App extends React.Component { constructor(props) { super(props); this.state = { value: {something: 'something'}, }; } render() { return ( <Provider value={this.state.value}> <Toolbar /> </Provider> ); } }
順便提一下react-router
其實也用了Context
的原理。<Router />
、<Link />
以及<Route />
這些組件之間共享一個router, 才能完成完成複雜的路由操做。有興趣的能夠自行查閱源碼。(瘋狂偷懶)
本文主要介紹了context
在react
的經常使用場景,以及在新舊API模式下的使用方法,着重介紹了shouldComponent
的處理方案。
-----慣例偷懶分割線-----
若是以爲寫得很差/有錯誤/表述不明確,都歡迎指出
若是有幫助,歡迎點贊和收藏,轉載請徵得贊成後著明出處。若是有問題也歡迎私信交流,主頁有郵箱地址
若是以爲做者很辛苦,也歡迎打賞~