Context
被翻譯爲上下文,在編程領域,這是一個常常會接觸到的概念,React中也有。javascript
在React的官方文檔中,Context
被歸類爲高級部分(Advanced),屬於React的高級API,但官方並不建議在穩定版的App中使用Context。html
The vast majority of applications do not need to use content.vue
If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.java
不過,這並不是意味着咱們不須要關注Context
。事實上,不少優秀的React組件都經過Context來完成本身的功能,好比react-redux的<Provider />
,就是經過Context
提供一個全局態的store
,拖拽組件react-dnd,經過Context
在組件中分發DOM的Drag和Drop事件,路由組件react-router經過Context
管理路由狀態等等。在React組件開發中,若是用好Context
,可讓你的組件變得強大,並且靈活。node
今天就想跟你們聊一聊,我在開發當中,所認識到的這個Context
,以及我是如何使用它來進行組件開發的。react
注:本文中全部提到的App皆指Web端App。編程
React文檔官網並未對Context
給出「是什麼」的定義,更可能是描述使用的Context
的場景,以及如何使用Context
。redux
官網對於使用Context
的場景是這樣描述的:緩存
In Some Cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful "context" API.react-router
簡單說就是,當你不想在組件樹中經過逐層傳遞props
或者state
的方式來傳遞數據時,可使用Context
來實現跨層級的組件數據傳遞。
使用props或者state傳遞數據,數據自頂下流。
使用Context
,能夠跨越組件進行數據傳遞。
若是要Context
發揮做用,須要用到兩種組件,一個是Context
生產者(Provider),一般是一個父節點,另外是一個Context
的消費者(Consumer),一般是一個或者多個子節點。因此Context
的使用基於生產者消費者模式。
對於父組件,也就是Context
生產者,須要經過一個靜態屬性childContextTypes
聲明提供給子組件的Context
對象的屬性,並實現一個實例getChildContext
方法,返回一個表明Context
的純對象 (plain object) 。
import React from 'react' import PropTypes from 'prop-types' class MiddleComponent extends React.Component { render () { return <ChildComponent /> } } class ParentComponent extends React.Component { // 聲明Context對象屬性 static childContextTypes = { propA: PropTypes.string, methodA: PropTypes.func } // 返回Context對象,方法名是約定好的 getChildContext () { return { propA: 'propA', methodA: () => 'methodA' } } render () { return <MiddleComponent /> } }
而對於Context
的消費者,經過以下方式訪問父組件提供的Context
。
import React from 'react' import PropTypes from 'prop-types' class ChildComponent extends React.Component { // 聲明須要使用的Context屬性 static contextTypes = { propA: PropTypes.string } render () { const { propA, methodA } = this.context console.log(`context.propA = ${propA}`) // context.propA = propA console.log(`context.methodA = ${methodA}`) // context.methodA = undefined return ... } }
子組件須要經過一個靜態屬性contextTypes
聲明後,才能訪問父組件Context
對象的屬性,不然,即便屬性名沒寫錯,拿到的對象也是undefined
。
對於無狀態子組件(Stateless Component),能夠經過以下方式訪問父組件的Context
import React from 'react' import PropTypes from 'prop-types' const ChildComponent = (props, context) => { const { propA } = context console.log(`context.propA = ${propA}`) // context.propA = propA return ... } ChildComponent.contextProps = { propA: PropTypes.string }
而在接下來的發行版本中,React對Context
的API作了調整,更加明確了生產者消費者模式的使用方式。
import React from 'react'; import ReactDOM from 'react-dom'; const ThemeContext = React.createContext({ background: 'red', color: 'white' });
經過靜態方法React.createContext()
建立一個Context
對象,這個Context
對象包含兩個組件,<Provider />
和<Consumer />
。
class App extends React.Component { render () { return ( <ThemeContext.Provider value={{background: 'green', color: 'white'}}> <Header /> </ThemeContext.Provider> ); } }
<Provider />
的value
至關於如今的getChildContext()
。
class Header extends React.Component { render () { return ( <Title>Hello React Context API</Title> ); } } class Title extends React.Component { render () { return ( <ThemeContext.Consumer> {context => ( <h1 style={{background: context.background, color: context.color}}> {this.props.children} </h1> )} </ThemeContext.Consumer> ); } }
<Consumer />
的children
必須是一個函數,經過函數的參數獲取<Provider />
提供的Context
。
可見,Context
的新API更加貼近React的風格。
實際上,除了實例的context
屬性(this.context
),React組件還有不少個地方能夠直接訪問父組件提供的Context
。好比構造方法:
constructor(props, context)
好比生命週期:
componentWillReceiveProps(nextProps, nextContext)
shouldComponentUpdate(nextProps, nextState, nextContext)
componetWillUpdate(nextProps, nextState, nextContext)
對於面向函數的無狀態組件,能夠經過函數的參數直接訪問組件的Context
。
const StatelessComponent = (props, context) => ( ...... )
以上是Context
的基礎,更具體的指南內容可參見這裏
OK,說完基礎的東西,如今聊一聊我對React的Context
的理解。
使用React的開發者都知道,一個React App本質就是一棵React組件樹,每一個React組件至關於這棵樹上的一個節點,除了App的根節點,其餘每一個節點都存在一條父組件鏈。
例如上圖,<Child />
的父組件鏈是<SubNode />
-- <Node />
-- <App />
,<SubNode />
的父組件鏈是<Node />
-- <App />
,<Node />
的父組件鏈只有一個組件節點,就是<App />
。
這些以樹狀鏈接的組件節點,實際上也組成了一棵Context
樹,每一個節點的Context
,來自父組件鏈上全部組件節點經過getChildContext()
所提供的Context
對象組合而成的對象。
有了解JS做用域鏈概念的開發者應該都知道,JS的代碼塊在執行期間,會建立一個相應的做用域鏈,這個做用域鏈記錄着運行時JS代碼塊執行期間所能訪問的活動對象,包括變量和函數,JS程序經過做用域鏈訪問到代碼塊內部或者外部的變量和函數。
假如以JS的做用域鏈做爲類比,React組件提供的Context
對象其實就比如一個提供給子組件訪問的做用域,而Context
對象的屬性能夠當作做用域上的活動對象。因爲組件的Context
由其父節點鏈上全部組件經過getChildContext()
返回的Context
對象組合而成,因此,組件經過Context
是能夠訪問到其父組件鏈上全部節點組件提供的Context
的屬性。
因此,我借鑑了JS做用域鏈的思路,把Context
當成是組件的做用域來使用。
不過,做爲組件做用域來看待的Context
與常見的做用域的概念 (就我我的目前接觸到的編程語言而言) 是有所區別的。咱們須要關注Context
的可控性和影響範圍。
在咱們平時的開發中,用到做用域或者上下文的場景是很常見,很天然,甚至是無感知的,然而,在React中使用Context
並非那麼容易。父組件提供Context
須要經過childContextTypes
進行「聲明」,子組件使用父組件的Context
屬性須要經過contextTypes
進行「申請」,因此,我認爲React的Context
是一種「帶權限」的組件做用域。
這種「帶權限」的方式有何好處?就我我的的理解,首先是保持框架API的一致性,和propTypes
同樣,使用聲明式編碼風格。另外就是,能夠在必定程度上確保組件所提供的Context
的可控性和影響範圍。
React App的組件是樹狀結構,一層一層延伸,父子組件是一對多的線性依賴。隨意的使用Context
其實會破壞這種依賴關係,致使組件之間一些沒必要要的額外依賴,下降組件的複用性,進而可能會影響到App的可維護性。
經過上圖能夠看到,本來線性依賴的組件樹,因爲子組件使用了父組件的Context
,致使<Child />
組件對<Node />
和<App />
都產生了依賴關係。一旦脫離了這兩個組件,<Child />
的可用性就沒法保障了,減低了<Child />
的複用性。
在我看來,經過Context
暴露數據或者API不是一種優雅的實踐方案,儘管react-redux是這麼幹的。所以須要一種機制,或者說約束,去下降沒必要要的影響。
經過childContextTypes
和contextTypes
這兩個靜態屬性的約束,能夠在必定程度保障,只有組件自身,或者是與組件相關的其餘子組件才能夠爲所欲爲的訪問Context
的屬性,不管是數據仍是函數。由於只有組件自身或者相關的子組件能夠清楚它能訪問Context
哪些屬性,而相對於那些與組件無關的其餘組件,不管是內部或者外部的 ,因爲不清楚父組件鏈上各父組件的childContextTypes
「聲明」了哪些Context
屬性,因此無法經過contextTypes
「申請」相關的屬性。因此我理解爲,給組件的做用域Context
「帶權限」,能夠在必定程度上確保Context
的可控性和影響範圍。
在開發組件過程當中,咱們應該時刻關注這一點,不要隨意的使用Context
。
做爲React的高級API,React並不推薦咱們優先考慮使用Context
。個人理解是:
Context
目前還處於實驗階段,可能會在後面的發行版本中有大的變化,事實上這種狀況已經發生了,因此爲了不給從此升級帶來較大影響和麻煩,不建議在App中使用Context
。Context
,但對於組件而言,因爲影響範圍小於App,若是能夠作到高內聚,不破壞組件樹的依賴關係,那麼仍是能夠考慮使用Context
的。props
或者state
解決,而後再考慮用其餘第三方成熟庫解決的,以上方法都不是最佳選擇的時候,那麼再考慮使用Context
。Context
的更新須要經過setState()
觸發,可是這並非可靠的。Context
支持跨組件訪問,可是,若是中間的子組件經過一些方法不響應更新,好比shouldComponentUpdate()
返回false
,那麼不能保證Context
的更新必定可達使用Context
的子組件。所以,Context
的可靠性須要關注。不過更新的問題,在新版的API中得以解決。簡而言之,只要你能確保Context
是可控的,使用Context
並沒有大礙,甚至若是可以合理的應用,Context
其實能夠給React組件開發帶來很強大的體驗。
官方所提到Context
能夠用來進行跨組件的數據通訊。而我,把它理解爲,比如一座橋,做爲一種做爲媒介進行數據共享。數據共享能夠分兩類:App級與組件級。
App根節點組件提供的Context
對象能夠當作是App級的全局做用域,因此,咱們利用App根節點組件提供的Context
對象建立一些App級的全局數據。現成的例子能夠參考react-redux,如下是<Provider />
組件源碼的核心實現:
export function createProvider(storeKey = 'store', subKey) { const subscriptionKey = subKey || `${storeKey}Subscription` class Provider extends Component { getChildContext() { return { [storeKey]: this[storeKey], [subscriptionKey]: null } } constructor(props, context) { super(props, context) this[storeKey] = props.store; } render() { return Children.only(this.props.children) } } // ...... Provider.propTypes = { store: storeShape.isRequired, children: PropTypes.element.isRequired, } Provider.childContextTypes = { [storeKey]: storeShape.isRequired, [subscriptionKey]: subscriptionShape, } return Provider } export default createProvider()
App的根組件用<Provider />
組件包裹後,本質上就爲App提供了一個全局的屬性store
,至關於在整個App範圍內,共享store
屬性。固然,<Provider />
組件也能夠包裹在其餘組件中,在組件級的全局範圍內共享store
。
若是組件的功能不能單靠組件自身來完成,還須要依賴額外的子組件,那麼能夠利用Context
構建一個由多個子組件組合的組件。例如,react-router。
react-router的<Router />
自身並不能獨立完成路由的操做和管理,由於導航連接和跳轉的內容一般是分離的,所以還須要依賴<Link />
和<Route />
等子組件來一同完成路由的相關工做。爲了讓相關的子組件一同發揮做用,react-router的實現方案是利用Context
在<Router />
、<Link />
以及<Route />
這些相關的組件之間共享一個router
,進而完成路由的統一操做和管理。
下面截取<Router />
、<Link />
以及<Route />
這些相關的組件部分源碼,以便更好的理解上述所說的。
// Router.js /** * The public API for putting history on context. */ class Router extends React.Component { static propTypes = { history: PropTypes.object.isRequired, children: PropTypes.node }; static contextTypes = { router: PropTypes.object }; static childContextTypes = { router: PropTypes.object.isRequired }; getChildContext() { return { router: { ...this.context.router, history: this.props.history, route: { location: this.props.history.location, match: this.state.match } } }; } // ...... componentWillMount() { const { children, history } = this.props; // ...... this.unlisten = history.listen(() => { this.setState({ match: this.computeMatch(history.location.pathname) }); }); } // ...... }
儘管源碼還有其餘的邏輯,但<Router />
的核心就是爲子組件提供一個帶有router
屬性的Context
,同時監聽history
,一旦history
發生變化,便經過setState()
觸發組件從新渲染。
// Link.js /** * The public API for rendering a history-aware <a>. */ class Link extends React.Component { // ...... static contextTypes = { router: PropTypes.shape({ history: PropTypes.shape({ push: PropTypes.func.isRequired, replace: PropTypes.func.isRequired, createHref: PropTypes.func.isRequired }).isRequired }).isRequired }; handleClick = event => { if (this.props.onClick) this.props.onClick(event); if ( !event.defaultPrevented && event.button === 0 && !this.props.target && !isModifiedEvent(event) ) { event.preventDefault(); // 使用<Router />組件提供的router實例 const { history } = this.context.router; const { replace, to } = this.props; if (replace) { history.replace(to); } else { history.push(to); } } }; render() { const { replace, to, innerRef, ...props } = this.props; // ... const { history } = this.context.router; const location = typeof to === "string" ? createLocation(to, null, null, history.location) : to; const href = history.createHref(location); return ( <a {...props} onClick={this.handleClick} href={href} ref={innerRef} /> ); } }
<Link />
的核心就是渲染<a>
標籤,攔截<a>
標籤的點擊事件,而後經過<Router />
共享的router
對history
進行路由操做,進而通知<Router />
從新渲染。
// Route.js /** * The public API for matching a single path and rendering. */ class Route extends React.Component { // ...... state = { match: this.computeMatch(this.props, this.context.router) }; // 計算匹配的路徑,匹配的話,會返回一個匹配對象,不然返回null computeMatch( { computedMatch, location, path, strict, exact, sensitive }, router ) { if (computedMatch) return computedMatch; // ...... const { route } = router; const pathname = (location || route.location).pathname; return matchPath(pathname, { path, strict, exact, sensitive }, route.match); } // ...... render() { const { match } = this.state; const { children, component, render } = this.props; const { history, route, staticContext } = this.context.router; const location = this.props.location || route.location; const props = { match, location, history, staticContext }; if (component) return match ? React.createElement(component, props) : null; if (render) return match ? render(props) : null; if (typeof children === "function") return children(props); if (children && !isEmptyChildren(children)) return React.Children.only(children); return null; } }
<Route />
有一部分源碼與<Router />
類似,能夠實現路由的嵌套,但其核心是經過Context
共享的router
,判斷是否匹配當前路由的路徑,而後渲染組件。
經過上述的分析,能夠看出,整個react-router其實就是圍繞着<Router />
的Context
來構建的。
以前,經過Context
開發過一個簡單的組件,插槽分發組件。本章就藉着這個插槽分發組件的開發經歷,聊聊如何使用Context
進行組件的開發。
首先說說什麼是插槽分發組件,這個概念最初是在Vuejs中認識的。插槽分發是一種經過組件的組合,將父組件的內容插入到子組件模板的技術,在Vuejs中叫作Slot
。
爲了讓你們更加直觀的理解這個概念,我從Vuejs搬運了一段關於插槽分發的Demo。
對於提供的插槽的組件<my-component />
,模板以下:
<div> <h2>我是子組件的標題</h2> <slot> 只有在沒有要分發的內容時顯示 </slot> </div>
對於父組件,模板以下:
<div> <h1>我是父組件的標題</h1> <my-component> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </my-component> </div>
最終渲染的結果:
<div> <h1>我是父組件的標題</h1> <div> <h2>我是子組件的標題</h2> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </div> </div>
能夠看到組件<my-component />
的<slot />
節點最終被父組件中<my-component />
節點下的內容所替換。
Vuejs還支持具名插槽。
例如,一個佈局組件<app-layout />
:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
而在父組件模板中:
<app-layout> <h1 slot="header">這裏多是一個頁面標題</h1> <p>主要內容的一個段落。</p> <p>另外一個段落。</p> <p slot="footer">這裏有一些聯繫信息</p> </app-layout>
最終渲染的結果:
<div class="container"> <header> <h1>這裏多是一個頁面標題</h1> </header> <main> <p>主要內容的一個段落。</p> <p>另外一個段落。</p> </main> <footer> <p>這裏有一些聯繫信息</p> </footer> </div>
插槽分發的好處體如今,它可讓組件具備可抽象成模板的能力。組件自身只關心模板結構,具體的內容交給父組件去處理,同時,不打破HTML描述DOM結構的語法表達方式。我以爲這是一項頗有意義的技術,惋惜,React對於這項技術的支持不是那麼友好。因而我便參考Vuejs的插槽分發組件,開發了一套基於React的插槽分發組件,可讓React組件也具模板化的能力。
對於<AppLayout />
組件,我但願能夠寫成下面這樣:
class AppLayout extends React.Component { static displayName = 'AppLayout' render () { return ( <div class="container"> <header> <Slot name="header"></Slot> </header> <main> <Slot></Slot> </main> <footer> <Slot name="footer"></Slot> </footer> </div> ) } }
在外層使用時,能夠寫成這樣:
<AppLayout> <AddOn slot="header"> <h1>這裏多是一個頁面標題</h1> </AddOn> <AddOn> <p>主要內容的一個段落。</p> <p>另外一個段落。</p> </AddOn> <AddOn slot="footer"> <p>這裏有一些聯繫信息</p> </AddOn> </AppLayout>
根據前面所想的,先整理一下實現思路。
不難看出,插槽分發組件須要依靠兩個子組件——插槽組件<Slot />
和分發組件<AddOn />
。插槽組件,負責打樁,提供分發內容的坑位。分發組件,負責收集分發內容,並提供給插槽組件去渲染分發內容,至關於插槽的消費者。
顯然,這裏遇到了一個問題,<Slot />
組件與<AddOn />
組件是獨立的,如何將<AddOn />
的內容填充到<Slot />
中呢?解決這個問題不難,兩個獨立的模塊須要創建聯繫,就給他們創建一個橋樑。那麼這個橋樑要如何搭建呢?回過頭來看看以前的設想的代碼。
對於<AppLayout />
組件,但願寫成下面這樣:
class AppLayout extends React.Component { static displayName = 'AppLayout' render () { return ( <div class="container"> <header> <Slot name="header"></Slot> </header> <main> <Slot></Slot> </main> <footer> <Slot name="footer"></Slot> </footer> </div> ) } }
在外層使用時,寫成這樣:
<AppLayout> <AddOn slot="header"> <h1>這裏多是一個頁面標題</h1> </AddOn> <AddOn> <p>主要內容的一個段落。</p> <p>另外一個段落。</p> </AddOn> <AddOn slot="footer"> <p>這裏有一些聯繫信息</p> </AddOn> </AppLayout>
不管是<Slot />
仍是<AddOn />
,其實都在<AppLayout />
的做用域內。<Slot />
是<AppLayout />
組件render()
方法返回的組件節點,而<AddOn />
則是<AppLayout />
的children
節點,因此,能夠將<AppLayout />
視爲<Slot />
與<AddOn />
的橋樑的角色。那麼,<AppLayout />
經過什麼給<Slot />
和<AddOn />
創建聯繫呢?這裏就用到本文的主角——Context
。接下來的問題就是,如何使用Context
給<Slot />
和<AddOn />
創建聯繫?
前面提到了<AppLayout />
這座橋樑。在外層組件,<AppLayout />
負責經過<AddOn />
收集爲插槽填充的內容。<AppLayout />
自身藉助Context
定義一個獲取填充內容的接口。在渲染的時候,由於<Slot />
是<AppLayout />
渲染的節點,因此,<Slot />
能夠經過Context
獲取到<AppLayout />
定義的獲取填充內容的接口,而後經過這個接口,獲取到填充內容進行渲染。
因爲<AddOn />
是<AppLayout />
的children
節點,而且<AddOn />
是特定的組件,咱們能夠經過name
或者displayName
識別出來,因此,<AppLayout />
在渲染以前,也就是render()
的return
以前,對children
進行遍歷,以slot
的值做爲key
,將每個<AddOn />
的children
緩存下來。若是<AddOn />
沒有設置slot
,那麼將其視爲給非具名的<Slot />
填充內容,咱們能夠給這些非具名的插槽定一個key
,好比叫$$default
。
對於<AppLayout />
,代碼大體以下:
class AppLayout extends React.Component { static childContextTypes = { requestAddOnRenderer: PropTypes.func } // 用於緩存每一個<AddOn />的內容 addOnRenderers = {} // 經過Context爲子節點提供接口 getChildContext () { const requestAddOnRenderer = (name) => { if (!this.addOnRenderers[name]) { return undefined } return () => ( this.addOnRenderers[name] ) } return { requestAddOnRenderer } } render () { const { children, ...restProps } = this.props if (children) { // 以k-v的方式緩存<AddOn />的內容 const arr = React.Children.toArray(children) const nameChecked = [] this.addOnRenderers = {} arr.forEach(item => { const itemType = item.type if (item.type.displayName === 'AddOn') { const slotName = item.props.slot || '$$default' // 確保內容惟一性 if (nameChecked.findIndex(item => item === stubName) !== -1) { throw new Error(`Slot(${slotName}) has been occupied`) } this.addOnRenderers[stubName] = item.props.children nameChecked.push(stubName) } }) } return ( <div class="container"> <header> <Slot name="header"></Slot> </header> <main> <Slot></Slot> </main> <footer> <Slot name="footer"></Slot> </footer> </div> ) }