由于<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> <header> <Slot></Slot> </header> <main> <Slot></Slot> </main> <footer> <Slot></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