Context
被翻译为上下文,在编程领域,这是一个常常会接触到的概念,React中也有。javascript
在React的官方文档中,Context
被归类为高级部分(Advanced),属于React的高级API,但官方并不建议在稳定版的App中使用Context。html
The vast majority of applications do not need to use content.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.vue
不过,这并不是意味着咱们不须要关注Context
。事实上,不少优秀的React组件都经过Context来完成本身的功能,好比react-redux的<Provider />
,就是经过Context
提供一个全局态的store
,拖拽组件react-dnd,经过Context
在组件中分发DOM的Drag和Drop事件,路由组件react-router经过Context
管理路由状态等等。在React组件开发中,若是用好Context
,可让你的组件变得强大,并且灵活。java
今天就想跟你们聊一聊,我在开发当中,所认识到的这个Context
,以及我是如何使用它来进行组件开发的。node
注:本文中全部提到的App皆指Web端App。
React文档官网并未对Context
给出“是什么”的定义,更可能是描述使用的Context
的场景,以及如何使用Context
。react
官网对于使用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.
简单说就是,当你不想在组件树中经过逐层传递props
或者state
的方式来传递数据时,可使用Context
来实现跨层级的组件数据传递。redux
使用props或者state传递数据,数据自顶下流。缓存
使用Context
,能够跨越组件进行数据传递。react-router
若是要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> ) } }
<AppLayout />
定义了一个Context
接口requestAddOnRenderer()
,requestAddOnRenderer()
接口根据name
返回一个函数,这个返回的函数会根据name
访问addOnRenderers
的属性,addOnRenderers
就是<AddOn />
的内容缓存对象。
<Slot />
的实现很简单,代码以下:
// props, context const Slot = ({ name, children }, { requestAddOnRenderer }) => { const addOnRenderer = requestAddOnRenderer(name) return (addOnRenderer && addOnRenderer()) || children || null } Slot.displayName = 'Slot' Slot.contextTypes = { requestAddOnRenderer: PropTypes.func } Slot.propTypes = { name: PropTypes.string } Slot.defaultProps = { name: '$$default' }
能够看到<Slot />
经过context
获取到<AppLayout />
提供的接口requestAddOnRenderer()
,最终渲染的主要对象就是缓存在<AppLayout />
中的<AddOn />
的内容。若是没有获取到指定的<AddOn />
的内容,则渲染<Slot />
自身的children
。
<AddOn />
更简单:
const AddOn = () => null AddOn.propTypes = { slot: PropTypes.string } AddOn.defaultTypes = { slot: '$$default' } AddOn.displayName = 'AddOn'
<AddOn />
不作任何事情,仅仅返回null
,它的做用就是让<AppLayout />
缓存分发给插槽的内容。
<AppLayout />
更具通用性经过上文的代码,基本将<AppLayout />
改形成了一个具有插槽分发能力的组件,可是很明显的,<AppLayout />
并不具有通用性,咱们能够将它提高成一个独立通用的组件。
我给这个组件命名为SlotProvider
function getDisplayName (component) { return component.displayName || component.name || 'component' } const slotProviderHoC = (WrappedComponent) => { return class extends React.Component { static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})` 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 (<WrappedComponent {...restProps} />) } } } export const SlotProvider = slotProviderHoC
使用React的高阶组件对原来的<AppLayout />
进行改造,将其转变为一个独立通用的组件。对于原来的<AppLayout />
,可使用这个SlotProvider
高阶组件,转换成一个具有插槽分发能力的组件。
import { SlotProvider } from './SlotProvider.js' 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> ) } } export default SlotProvider(AppLayout)
经过以上的经历,能够看到,当设计开发一个组件时,
SlotProvider
与<Slot />
和<AddOn />
一块儿配合使用,SlotProvider
做为根组件,而<Slot />
和<AddOn />
都算是子组件。SlotProvider
而言,<Slot />
的位置是不肯定的,它会处在被SlotProvider
这个高阶组件所包裹的组件的模板的任何位置,而对于<Slot />
和<AddOn />
,他们直接的位置也不肯定,一个在SlotProvider
包装的组件的内部,另外一个是SlotProvider
的children
。<Slot />
实际渲染的内容来自于SlotProvider
收集到的<AddOn />
的内容。这时咱们就须要借助一个中间者做为媒介来共享数据,相比额外引入redux这些第三方模块,直接使用Context
能够更优雅。
使用新版的Context API对以前的插槽分发组件进行改造。
// SlotProvider.js function getDisplayName (component) { return component.displayName || component.name || 'component' } export const SlotContext = React.createContext({ requestAddOnRenderer: () => {} }) const slotProviderHoC = (WrappedComponent) => { return class extends React.Component { static displayName = `SlotProvider(${getDisplayName(WrappedComponent)})` // 用于缓存每一个<AddOn />的内容 addOnRenderers = {} requestAddOnRenderer = (name) => { if (!this.addOnRenderers[name]) { return undefined } return () => ( this.addOnRenderers[name] ) } 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 ( <SlotContext.Provider value={ requestAddOnRenderer: this.requestAddOnRenderer }> <WrappedComponent {...restProps} /> </SlotContext.Provider> ) } } } export const SlotProvider = slotProviderHoC
移除了以前的childContextTypes
和getChildContext()
,除了局部的调整,总体核心的东西没有大变化。
// Slot.js import { SlotContext } from './SlotProvider.js' const Slot = ({ name, children }) => { return ( <SlotContext.Consumer> {(context) => { const addOnRenderer = requestAddOnRenderer(name) return (addOnRenderer && addOnRenderer()) || children || null }} </SlotContext.Consumer> ) } Slot.displayName = 'Slot' Slot.propTypes = { name: PropTypes.string } Slot.defaultProps = { name: '$$default' }
因为以前就按照生产者消费者的模式来使用Context
,加上组件自身也比较简单,所以使用新的API进行改造后,差异不大。
props
和state
,React的Context
能够实现跨层级的组件通讯。childContextTypes
声明,而后经过实例方法getChildContext()
建立Context
对象。消费者一方,经过组件静态属性contextTypes
申请要用到的Context
属性,而后经过实例的context
访问Context
的属性。Context
须要多一些思考,不建议在App中使用Context
,但若是开发组件过程当中能够确保组件的内聚性,可控可维护,不破坏组件树的依赖关系,影响范围小,能够考虑使用Context
解决一些问题。Context
暴露API或许在必定程度上给解决一些问题带来便利,但我的认为不是一个很好的实践,须要慎重。Context
的更新须要依赖setState()
,是不可靠的,不过这个问题在新版的API中得以解决。Context
当作组件的做用域来看待,可是须要关注Context
的可控性和影响范围,使用以前,先分析是否真的有必要使用,避免过分使用所带来的一些反作用。Context
当作媒介,进行App级或者组件级的数据共享。Context
或许能够更加优雅。以上是个人分享内容,若有不足或者错误的地方,欢迎批评指正。