
壹 ❀ 引
我在從零開始的react入門教程(八),redux起源與基礎用法一文中,介紹了redux
的前輩Flux
,以及redux
關於單項數據更新的基本用法。我們在前文提到,相對Flux
支持多個store
,redux
推薦唯一數據源,也就是使用一個全局Store
去掌管所有數據。數據源雖然統一了,但我們要使用Store
還是得把Store
引入到需要的組件中,比如上文中的Counter
組件與Summary
組件,畢竟使用dispatch
或者監聽Store
變化都離不開這個數據源,但這就造就了兩個問題。
問題一,假設我們有多個組件都依賴了Store
數據,組件分布在不同文件夾,或者說我們使用的三方庫也依賴了此數據,用一處就得引一次,文件路徑的相對關系都是一個不小的麻煩。
問題二,可能有同學就想到,哎,react不是有個概念叫狀態提升嗎,大不了我在頂層組件引用一次,通過props
進行數據傳遞,但這樣就會造成多個組件其實並不需要這份數據,但為了子孫組件能順利訪問數據,都成了數據傳遞的搬運工。
針對上面兩個問題,我們其實可以通過Context
得以解決,Context
顧名思義就是上下文。就像在一個作用域內我們提前聲明了一個變量,后續代碼不需要再做引用操作,你都能直接訪問它,Context
的作用也是如此。
我在整理Context
資料的時候發現了一個問題,由於react
版本原因,react
對於Context
的解釋也是存在歷史變遷的。作為一個初學者,如果你在百度想搜Context
用法然后發現了不同的介紹,估計你也會納悶我到底應該用哪種(或者對於直接上手react-redux的同學可能根本沒了解過原生Context的用法),這里我先做個簡單的總結,在react
版本16.X之前,Context
的使用依賴childContextTypes
對象,然后手動定義Provider
組件,比如在《深入淺出react和Redux》一書中,代碼例子的react版本還是15.4.1,所以書中介紹的自然是前面提到的做法。而對於現在的版本比如官方文檔中,Context
的使用已經不需要手動定義Provider
組件了,而是createContext
方法手動創建,用法上會人性化很多。
本文還是會站在不同的兩個版本,去介紹它們的用法,以達到解決文章開頭關於Store
引用與傳遞的問題,當然,如果你已經確定了當前項目的react版本,你可以自由選擇對應的版本文檔了解其用法。
如果可以,我還是希望有緣看到這篇文章的人能跟着手敲代碼,感受其具體的用法,那么本文開始。
貳 ❀ Context 舊版(版本16.X之前)
說在前面,下面的代碼仍然基於上一篇文章的例子修改,當然如果沒有代碼,我盡可能將使用上的細節描述清楚(當然我還是推薦跟着例子來)。如果大家有簡單了解過Context
,腦海里一定對Provider
的單詞有所印象,不過對於老版本而言,我們並不能直接引用並使用它,而是需要自己創建,確實非常尷尬。
我們現在src目錄下新建一個Provider.js
的文件,里面的代碼為:
import {Component} from 'react';
import PropTypes from 'prop-types';
class Provider extends Component {
getChildContext() {
// 我們會通過store字段將全局store傳遞進來
return {
store: this.props.store
};
}
// 渲染Provoder所包裹的子組件內容
render() {
return this.props.children;
}
}
Provider.propTypes = {
store: PropTypes.object.isRequired
}
Provider.childContextTypes = {
store: PropTypes.object
};
export default Provider;
這段代碼有幾點需要擰出來說,第一個是關於PropTypes
,寫過react的同學都知道這是做組件屬性的類型檢查,比如我一個組件哪些屬性是必須提供,哪些是字符串等等。這個東西呢其實也存在一個歷史問題....早期版本的react,是可以直接通過引用拿到此對象然后使用,比如:
import { PropTypes } from 'react';
但是在react 15.5之后,此屬性被react官方廢棄掉了,如果你是版本比較高的react,像上面這樣引用會告訴你PropTypes
是undefied
並報錯,比如我參考的《深入淺出react和Redux》一書中都是這么用的,因為作者例子的react版本也比較低(15.4.1),而我在寫demo的react版本已經是16了,自然用不了,不過也沒有關系,咱們可以通過如下方式引用PropTypes
:
import PropTypes from 'prop-types';
prop-types
是一個獨立的三方庫,因此我們需要提前安裝這個包,比如執行命令yarn add prop-types
,若你是npm請執行npm i prop-types
,這里就不多介紹了,關於prop-types
后續也可能會專門寫一篇用法的文章。
回到上面的代碼中,Provider
組件定義的內容其實非常簡單,一個getChildContext
方法,用於創建子組件的上下文,而上下文中包含的東西其實也就是我們需要使用的store
數據,this.props.store
怎么來下面的代碼會交代。除此之外還有一個render方法,用於渲染Provider
包裹的子組件。關於this.props.children
這里做個簡單補充,比如我們有一個父組件A與一個子組件B,A包裹B,如下:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function A(props) {
console.log(props);
return <div>我是父組件{props.children}</div>
}
function B() {
return <div>我是子組件</div>
}
ReactDOM.render(
<A><B/></A>,
document.getElementById('root')
);

可以看到我們使用了A包裹了B,在A組件的返回中,我們通過{props.children}
成功拿到了包裹的B組件,並將其渲染了出來,通過控制台輸出也看的很明顯,這里的chindren
屬性其實就是組件A所包含的組件內容。
我們再過分點,直接修改為如下代碼:
ReactDOM.render(
<A>
{
<div>
<div>1</div>
<div>2</div>
</div>
}
</A>,
document.getElementById('root')
);

再看控制台,你會發現通過children
屬性,我們先訪問到了包裹的最外層的div,然后此div的children
又是一個數組,因為它又包含了兩個div,繼續再通過children
屬性,我們就可以找到數組第一個元素的孩子是一個數字1,這就是react中children
的作用,在實際開發中,我們也常會利用此屬性達到組件父子組件嵌套的目的。
OK,題外話說完了,再回到上述代碼,注意如下這段代碼:
Provider.childContextTypes = {
store: PropTypes.object
};
這段代碼是必須提供的,不然直接報錯,它的類型定義與getChildContext
方法中提供的類型相對應,它用於告訴react我現在為子組件提供了一個上下文,上下文中包含的數據有哪些,每個屬性是什么類型,關於Provider.js
先說到這里。
在上一篇文章的例子中,我們通過index.js
文件最終渲染了所有組件,這里我們需要做些修改,具體如下:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import store from './Store.js';
import Provider from './Provider.js';
import Counter from './Counter.js';
import Summary from './Summary.js';
class ControlPanel extends Component {
render() {
return (
<Provider store={store}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</Provider>
);
}
}
ReactDOM.render(
<ControlPanel />,
document.getElementById('root')
);
我們在此文件中引用了前面定義的Provider
組件,同時也引用了全局的Store
,然后通過Provider
組件將上篇文章中需要渲染的組件進行了包裹,同時通過store
字段將引用過來的store
作為props傳遞了下去,這里就對應了Provider.js
中getChildContext
方法this.props.store
的來源。
上述的修改其實很好理解,我們將Provider
作為頂層組件,為需要渲染的所有組件提供了一個共有的上下文,而這個上下文中存在一個store
屬性,也就是全局的Store
,現在子組件們不需要再分別引用Store.js
文件了,但這些子組件還需要做一些改變才能支持訪問上下文。
以Counter
組件為例,這里我們說下需要修改的幾個點:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as Actions from './Actions.js';
class Counter extends Component {
constructor(props,context) {
super(props,context);
// 初始化組件的state
this.state = this.getOwnState();
}
getOwnState = () => {
// 這里的this.props.caption其實就是前面說的First Second
return {
// 這里可以拿到當前的Store數據,並根據key取到對應的初始值
value: this.context.store.getState()[this.props.caption]
};
}
onIncrement = () => {
// Actions.increment返回的其實是一個action對象,注意這個函數其實只傳遞了一個參數,也就是上面提到的First Second類型
this.context.store.dispatch(Actions.increment(this.props.caption));
}
onDecrement = () => {
this.context.store.dispatch(Actions.decrement(this.props.caption));
}
// 用於更新state
onChange = () => {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
// 如果state的value變了,通知組件更新
return nextState.value !== this.state.value;
}
componentDidMount() {
// 監聽Store變化,Store變了我們就讓組件的state也跟着變
this.context.store.subscribe(this.onChange);
}
componentWillUnmount() {
this.context.store.unsubscribe(this.onChange);
}
render() {
const { value } = this.state;
const { caption } = this.props;
return (
<div>
<button onClick={this.onIncrement}>+</button>
<button onClick={this.onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
);
}
}
// 這里必須定義,不然訪問不到Context
Counter.contextTypes = {
store: PropTypes.object
}
export default Counter;
第一點就是我們同樣引入了PropTypes
,因為在代碼最下面,我們必須定義contextTypes
的類型,這里與Provider.js
中的childContextTypes
定義其實是對應的,上下文在創建的時候定義了,子組件在引用上下文時同樣得做一個定義聲明。
第二點,在constructor
中我們知道super
方法用於子組件在初始化時繼承父組件傳遞的屬性,而這里我們得額外添加一個context
,表示將上下文傳遞進來。
第三點,之前在Counter
中我們直接引入了Store.js
,因此可以直接訪問store
的數據以及API方法,但此時我們是通過上下文訪問,因此需要對之前所有使用到store
的前面添加上this.context
,具體可參照上述代碼。同理,我們將Summary
組件中也做上述三點修改,然后執行yarn start
運行項目,你會發現非常完美,項目成功跑起來了。
那么到這里,我們通過舊版的Context
做法取代了傳統Store
引用的做法,達到了只在index.js
一處引用統一管理,並可在所有子組件中訪問此上下文的目的。
叄 ❀ Context 新版(版本16.X之后)
其實對前面舊版的修改寫下來,你會發現這玩意還真不是那么好用,雖說不用每個組件引入Store
了吧,咱還得自己手寫Provider
組件不說,每個用到store
的組件還得專門定義contextTypes
的類型,實屬有點麻煩。沒事,我們繼續來看新版的Context
的用法。當然這次,至少咱們不用手寫Provider
組件了。
在對於新版本Context
資料查閱中,我看到了一句對於Context
作用描述比較精准的話,那就是Context
能實現組件跨層級的數據傳遞。比如Props
傳遞一定是逐層的,這可能就會對一些不需要這部分數據的組件造成感染,那么我想越級傳遞,中間的組件不需要感知這部分數據的存在,Context
就是一個不錯的渲染。當然回到上文,我們還是可以理解為Context
為相關聯的組件提供了一個共有上下文,子可見后代也可見,那么就不需要子幫忙傳遞后代都可以拿着用。因此,除了應對全局Store
的數據傳遞之外,某些部分組件的數據越級傳遞(比如數據與Store無關,單純幾個層級關系組件之間需要做傳遞),以及部分子組件,后代組件都需要訪問到父組件的部分數據,其實都可以使用此做法達到目的。
OK,新版Context
的幾個核心概念為createContext
,Provider
與Consumer
,我們一個個說。
叄 ❀ 壹 createContext
createContext
顧名思義,創建一個上下文也就是Context
對象,它的一般用法為:
const context = React.createContext();
而這個創建出來的context
對象中,又包含了Provider
與Consumer
兩個組件,輸出如下:

因此在使用時,其實也可以像下面這樣直接獲取到兩個組件:
const {Provider, Consumer} = React.createContext();
createContext
可以接受一個參數defaultValue
,表示我在創建這個上下文時,就默認定義了一部分的共有的數據,但這個默認數據生效是有條件的,這里引用官方文檔的描述:
createContext
創建一個 Context 對象。當 React 渲染一個訂閱了這個 Context 對象的組件,這個組件會從組件樹中離自身最近的Provider
中讀取到當前的 context 值。而如果當前組件所處的組件樹中都沒有匹配到Provider
是,這時候defaultValue
就會生效。
怎么理解呢?也就是說我們在父組件創建了一個上下文,但后代組件中只用了Consumer
組件,而沒有使用Provider
對應提供數據,那這時候相當於處於保護措施,我們讓defaultValue
生效,保證Consumer
能拿到默認的數據,免得組件渲染報錯了,實屬吃低保的行為了。關於這部分的例子,可以參閱React.createContext point of defaultValue?的問題回復,因為這部分知識又涉及到了hook
的useContext
,簡單理解就是父組件中createContext
創建上下文,而在子組件中可以使用useContext
解析context
中的數據,這里我們先不細談。
叄 ❀ 貳 Provider
故名思域,與舊版我們定義的Provider
作用大致相同,它用於包裹需要享有相同上下文的所有組件,以及為其提供上下文中共有的數據,但需要注意的是,這里的數據傳遞必須通過value
字段,比如:
<Provider value={/*需要傳遞的共享數據*/}>
/*被包裹的組件們*/
</Provider>
多個Provider
可以嵌套使用,但是里層的Provider
的value會覆蓋掉外層的Provider
的value,因此Consumer
訪問context注定是訪問距離自己最近的Provider
。除此之外還有一點,當Provider
傳遞的value發生了變化時,Provider
內部的所有Consumer
組件都會被強制重新渲染,shouldComponentUpdate
這玩意都不會限制住它,目的是保證所有消費者組件永遠同步感知最新的context變化。
叄 ❀ 叄 Consumer
如名稱理解的那樣,消費者,也就是消費(使用)Provider
傳遞下來數據的組件。正常情況下,Consumer
組件得嵌套在Provider
組件之下,但如果如上面所說我們沒用Provider
組件只用了Consumer
組件,那么Consumer
組件能訪問的上下文就是在createContext
中定義的defaultValue
。
基本API都介紹了,我們來通過這種方式再來改寫我們前面的例子。
首先,我們在src目錄下新建一個Context.js
文件,代碼如下:
import React from 'react';
const context = React.createContext();
export default context;
之后,在index.js
文件引入context,這里直接再貼上代碼:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import store from './Store.js';
import Counter from './Counter.js';
import Summary from './Summary.js';
import context from './Context.js';
class ControlPanel extends Component {
render() {
return (
//我們使用了Provider包裹子組件,通過value傳遞store
<context.Provider value={store}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</context.Provider>
);
}
}
ReactDOM.render(
<ControlPanel />,
document.getElementById('root')
);
同理,我們再次修改Counter
組件,還是直接上代碼:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as Actions from './Actions.js';
import context from './Context.js';
// const context = React.createContext();
class Counter extends Component {
// static contextType = context;
constructor(props,context) {
super(props,context);
// 初始化組件的state
this.state = this.getOwnState();
}
getOwnState = () => {
// 這里的this.props.caption其實就是前面說的First Second
return {
// 這里可以拿到當前的Store數據,並根據key取到對應的初始值
value: this.context.getState()[this.props.caption]
};
}
onIncrement = () => {
// Actions.increment返回的其實是一個action對象,注意這個函數其實只傳遞了一個參數,也就是上面提到的First Second類型
this.context.dispatch(Actions.increment(this.props.caption));
}
onDecrement = () => {
this.context.dispatch(Actions.decrement(this.props.caption));
}
// 用於更新state
onChange = () => {
this.setState(this.getOwnState());
}
shouldComponentUpdate(nextProps, nextState) {
// 如果state的value變了,通知組件更新
return nextState.value !== this.state.value;
}
componentDidMount() {
// 監聽Store變化,Store變了我們就讓組件的state也跟着變
this.context.subscribe(this.onChange);
}
componentWillUnmount() {
this.context.unsubscribe(this.onChange);
}
render() {
const { value } = this.state;
const { caption } = this.props;
return (
<div>
<button onClick={this.onIncrement}>+</button>
<button onClick={this.onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
);
}
}
Counter.contextType = context;
export default Counter;
因為我們需要在Counter
組件使用context
,因此也需要引入context
。之后,我們通過Counter.contextType = context;
為當前組件綁定context
對象,同理,在constructor
中還是得初始化context
,之后在組件任意地方,我們都可以通過this.context
訪問到傳遞進來的store
,注意啊,這里的this.context
已經等同於store
本身了,所以代碼中是this.context.subscribe
直接調用store
上的API。你可能有點不習慣,還是希望this.context.store
去訪問,那就像如下方式這樣傳遞,比如假設我們需要給Provider
傳遞多個值:
class ControlPanel extends Component {
render() {
const value = {
store,
name:1
};
return (
<context.Provider value={value}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</context.Provider>
);
}
}
我們再去Counter
斷點this,你就發現這就是你預期的樣子了

其實可以發現,新版的context
在使用上與舊版還是有些類似的,在使用context
的地方同樣得為組件做contextType
的定義以及context
的初始化,我們同理去修改掉Summary
中的代碼,執行運行項目的命令,你會發現也能完美跑起來,那么到這里,我們又通過新版Context
的做法修改了例子。
當然到這里我們還沒用到Consumer
,那么接下來我們再單獨用一個例子,再次結合把Provider
與Consumer
用一用。接下來我們定義ABC三個組件,A嵌套B,B又嵌套C,直接修改index.js
中的代碼:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import context from './Context.js';
class A extends Component {
render() {
const name = '聽風是風';
return (
<context.Provider value={name}>
<div>{`我是A組件,我傳遞了${name}`}</div>
{/* 注意,這里我們並沒有將name作為props傳遞下去 */}
<B />
</context.Provider>
)
}
}
function B() {
return (
<context.Consumer>
{
(name) => {
console.log(name);
return (
<div>
{`我是B組件,我接受了${name}`}
<C />
</div>
)
}
}
</context.Consumer>
)
}
function C() {
return (
<context.Consumer>
{
(name)=>{
return (
<div>
{`我是C組件,我接受了${name}`}
</div>
)
}
}
</context.Consumer>
)
}
ReactDOM.render(
<A />,
document.getElementById('root')
);

可以看到,在子組件需要使用context
的地方,我們通過context.Consumer
將其包裹,而context.Consumer
之間接受一個函數,此函數接受一個參數(參數隨便你叫什么),此參數就是Provider
的映射,比如我們上面傳遞的是一個字符串,注意,只有一層花括號進行了包裹,所以函數形參name
直接就是所傳遞值的映射。
那假設我們傳遞了多個參數呢?還是一樣,我們稍作修改,這里只貼上修改的部分,並在子組件函數中嘗試打印:
class A extends Component {
render() {
const name = '聽風是風';
const age = '28';
return (
<context.Provider value={{name,age}}>
<div>{`我是A組件,我傳遞了${name}`}</div>
{/* 注意,這里我們並沒有將name作為props傳遞下去 */}
<B />
</context.Provider>
)
}
}
function B() {
return (
<context.Consumer>
{
// 參數其實可以隨便你取名
(aaa) => {
console.log(aaa);
return (
<div>
{`我是B組件,我接受了${aaa.name}`}
<C />
</div>
)
}
}
</context.Consumer>
)
}

當然實際開發中,我們不會推薦這樣傳遞多個參數,因為上述代碼中value={{name,age}}
部分,代碼每次執行{name,age}
可以理解為每次都是一個全新的對象,由於對象引用不同這會導致react
認為value
每次都在發生變化,從而引發子組件全部更新,推薦的做法是使用一個變量去聲明一個對象包含這兩個變量,比如:
// 這里只貼主要修改部分
const user = {
name:'聽風是風',
age:28
}
return (
<context.Provider value={user}>
<div>{`我是A組件,我傳遞了${user.name}`}</div>
{/* 注意,這里我們並沒有將name作為props傳遞下去 */}
<B />
</context.Provider>
)
<context.Consumer>
{
(user) => {
return (
<div>
{`我是B組件,我接受了${user.name}`}
<C />
</div>
)
}
}
</context.Consumer>
那么到這里,我們其實展示了兩種在子組件中訪問context
的方式,第一種是為組件綁定contextType
,第二種就是使用Consumer
,那么我們直接將C組件修改成如下的方式:
class C extends Component {
constructor(props, context) {
super(props, context)
}
render() {
return (
<div>
{`我是C組件,我接受了${this.context}`}
</div>
)
}
}
C.contextType = context;
可以看到我們沒有借用Consumer
,而是借用組件contextType
綁定后,同樣成功訪問到了父組件傳遞的數據。
那么到這里,我們介紹了react
中新舊context
的基本用法,舊版context
需要自定義Provider
,並結合getChildContext
定義為子組件傳遞的數據。而新版context
在使用上相對友好了不少,我們可以通過createContext
創建一個context
實例,並可以直接使用Provider
提供數據,使用Consumer
消費數據。通過文中新舊例子對比,其實兩者在使用上存在不少相同點。
在下一篇文章中,我們來了解react-redux
基本用法,其實本篇文章與上一篇文章屬於react-redux
的鋪墊篇,在了解了react原生的概念后,我想在理解三方封裝時應該會容易很多,那么到這里本文結束。
參考
深入淺出react和Redux 第三章組件context部分