聊一聊我对 React Context 的理解以及应用(6)

由于<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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsdgxg.html