淺談react context


1.為什么需要使用Context

在react中,數據傳遞一般使用props傳遞數據,維持單向數據流,這樣可以讓組件之間的關系變得簡單且可預測,但是單項數據流在某些場景中並不適用,看一個官方給出的例子:
有三個組件APP, Toolbar ,ThemedButton,關系如圖:(為了方便大家理解(偷懶),這個例子我會全文通用。

APP存放着主題相關的參數theme,需要傳遞組件ThemedButton, 如果考慮使用props,那么代碼就長這樣:

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作為中介傳遞。不妨再引申思考一下:

  1. 如果ThemedButton並非末級子節點,那么參數必須繼續向下傳遞
  2. 如果App中,還有除了<ThemedButton>以外的組件,也需要theme參數,那么也必須按照這種形式逐層傳遞

那么數據結構圖大概如圖所示:

結構圖placeholder:層層傳遞,顯然,這樣做太!繁!瑣!了!接下來,就要介紹今天的主角--Context

 

2. Context的用法介紹

Context 提供了一種在組件之間共享此類值的方式,而不必顯式地通過組件樹的逐層傳遞 props。

上面是官方對於context的介紹,簡單來說,就是可以把context當做是特定一個組件樹內共享的store,用來做數據傳遞。
為什么這里要加粗強調組件樹呢?因為它是基於樹形結構共享的數據:在某個節點開啟提供context后,所有后代節點compoent都可以獲取到共享的數據。

語言描述略顯抽象,直接上代碼:

1. 基本使用

以下介紹的是在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。

2. 更新context

如果我們在APP組件提供了一個切換主題的按鈕,那就需要context能夠更新並且通知到相應的consumer。
由於context本身提供了相關功能:

  1. getChildContext方法在每次state和props改變時會被調用;
  2. 一旦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 />; } }

但是官方文檔同時提到了這種方法是有隱患的,下一節進行詳細解析。

資源搜索網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com

3. 當context遇到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的更新!

  1. 第一種思路:首先,我們先看看問題的根源之一,是context更新之后,后代節點無法及時獲取到更新,那么如果context不發生更,那就不存在這個問題了.【我個人覺得這個思路有點類似於,解決不了問題,可以考慮解決提出問題的人】,也就意味着:

    • 設定為不可變對象immutable
    • 后代組件應該僅在constructor函數中獲取一次context。
  2. 第二種思路,我們不在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,回頭看看我們都干了些什么:

  1. 我們現在不再利用context傳遞 theme值,而是傳遞一個themeManager注入對象,這個對象的特點是內置了狀態更新和消息通知的功能
  2. 消費組件ThemedButton訂閱theme的變化,並且利用setState作為回調函數,保證theme值的及時更新。

從而完美繞開了context的傳遞問題。其實,它同樣符合我們第一個解決方案:通過context傳遞的對象,只被接受一次,並且后續都沒有更新(都是同一個themeManager對象,更新是通過themeManager內部的自我管理實現的。)

 

4. 16.x后的新API

講完基本用法,接着聊聊context在16.x版本之后的API。
先說一個好消息!使用新API后

每當 Provider(提供者) 的 value 屬性發生變化時,所有作為 Provider(提供者) 后代的 consumer(使用者) 組件 都將重新渲染。 從Provider 到其后代使用者的傳播不受 shouldComponentUpdate 方法的約束,因此即使祖先組件退出更新,也會更新 consumer(使用者)

換句話說 如果使用context的新API,第三節可以跳過不看。(所以我把那一段寫前面去了)

在傳統版本,使用getChildContext和childContextTypes來使用context,而在16.x版本之后,前面的例子可以改寫成這樣:

  1. 首先使用createContext創建一個context,該方法返回一個對象,包含Provider(生產者)和Consumer(消費者)兩個組件:

    const themeContext = React.createContext('light'); // 這里light是默認值 后續使用時可以改變
  2. 使用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 /> ); }
  3. 后代組件根據需要,指定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來寫

 

5. 注意事項和其他

對於context的使用,需要注意的主要是以下2點:

  1. 減少不必要使用context,因為react重視函數式編程,講究復用,而使用了context的組件,復用性大大降低
  2. 傳統版本的react,尤其要注意context在自己的可控范圍內,其實最大的問題也就是前面說的SUC的問題
  3. 前面說到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> ); } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM