「React 进阶」 学好这些 React 设计模式,能让你的 React 项目飞起来

345次阅读  |  发布于3年以前

一 前言

今天我们来悉数一下 React 中一些不错的设计模式,这些设计模式能够解决一些功能复杂逻辑复用 的问题,还能锻炼开发者的设计和编程能力,以为多年开发经验来看,学好这些设计模式,那就是一个字

基本上每一个设计模式,笔者都会绞尽脑汁的想出两个 demo,希望屏幕前的你能给笔者赏个,以此鼓励我继续创作前端硬文。

老规矩,我们带着疑问开始今天的阅读:

我相信读完这篇文章,这些问题全都会迎刃而解。

首先我们想一个问题,那就是 为什么要学习设计模式? 原因我总结有以下几个方面。

场景一:

在一个项目中,全局有一个状态,可以称之为 theme (主题),那么有很多 UI 功能组件需要这个主题,而且这个主题是可以切换的,就像 github 切换暗黑模式一样,那么如何优雅的实现这个功能呢?

这个场景如果我们用 React 的提供者模式,就能轻松搞定了,通过 context 保存全局的主题,然后将 theme 通过 Provider 形式传递下去,需要 theme ,那么消费 context ,就可以了,这样的好处是,只要 theme 改变,消费 context 的组件就会重新更新,达到了切换主题的目的。

场景二:

表单设计场景也需要一定程度上的 React 的设计模式,首先对于表单状态的整体验证需要外层的 Form 绑定事件控制,调度表单的状态下发,验证功能。内层对于每一个表单控件还需要 FormItem 收集数据,让控件变成受控的。这样的 FormFormItem 方式,就是通过组合模式实现的。

熟练运用 React 的设计模式,可以培养开发者的设计能力,比如 HOC 的设计公共组件的设计自定义 hooks 的设计,一些开源的优秀的库就是通过 React 的灵活性和优秀的设计模式实现的。

例子一:

比如在 React 状态管理工具中,无论是 react-redux ,还是 mobx-react,一方面想要把 statedispatch 函数传递给组件,另一方面订阅 state 变化,来促使业务组件更新,那么整个流程中,需要一个或多个 HOC 来搞定。于是 react-redux 提供了 connect,mobx-react 提供了 injectobserver 等优秀的 hoc。由此可见,学会 React 的设计模式,有助于开发者小到编写公共组件,大到开发开源项目。

今天我重点介绍 React 的五种设计模式,分别是:

二 组合模式

1 介绍

组合模式适合一些容器组件场景,通过外层组件包裹内层组件,这种方式在 Vue 中称为 slot 插槽,外层组件可以轻松的获取内层组件的 props 状态,还可以控制内层组件的渲染,组合模式能够直观反映出 父 -> 子组件的包含关系,首先我来举个最简单的组合模式例子。

<Tabs onChange={ (type)=> console.log(type)  } >
    <TabItem name="react"  label="react" >React</TabItem>
    <TabItem name="vue" label="vue" >Vue</TabItem>
    <TabItem name="angular" label="angular"  >Angular</TabItem>
</Tabs>

如上 TabsTabItem 组合,构成切换 tab 功能,那么 Tabs 和 TabItem 的分工如下:

我们直观上看到 Tabs 和 TabItem 并没有做某种关联,但是却无形的联系起来。这种就是组合模式的精髓所在,这种组合模式的组件,给使用者感觉很舒服,因为大部分工作,都在开发组合组件的时候处理了。所以编写组合模式的嵌套组件,对锻炼开发者的 React 组件封装能力是很有帮助的。

接下来我们一起看一下,组合模式内部是如何实现的。

2 原理揭秘

实际组合模式的实现并没有想象中那么复杂,主要分为外层和内层两部分,当然可能也存在多层组合嵌套的情况,但是万变不离其宗,原理都是一样的。首先我们看一个简单的组合结构:

<Groups>
    <Item  name="《React进阶实践指南》" />
</Groups>

那么 Groups 能对 Item 做一些什么操作呢 ?

Item 在 Groups的形态

首先如果如上组合模式的写法,会被 jsx 编译成 React element 形态,Item 可以通过 Groupsprops.children 访问到。

function Groups (props){
    console.log( props.children  ) // Groups element
    console.log( props.children.props ) // { name : 'React进阶实践指南》' }
    return  props.children
}

但是这是针对单一节点的情况,事实情况下,外层容器可能有多个子组件的情况。

<Groups>
    <Item  name="《React进阶实践指南》" />
    <Item name="《Nodejs深度学习手册》" />
</Groups>

这种情况下,props.children 就是一个数组结构,如果想要访问每一个的 props ,那么需要通过 React.Children.forEach 遍历 props.children。

function Groups (props){
    console.log( props.children  ) // Groups element
    React.Children.forEach(props.children,item=>{
        console.log( item.props )  //依次打印 props
    })
    return  props.children
}

隐式混入 props

这个是组合模式的精髓所在,就是可以通过 React.cloneElement 向 children 中混入其他的 props,那么子组件就可以使用容器父组件提供的特有的 props 。我们来看一下具体实现:

function Item (props){
    console.log(props) // {name: "《React进阶实践指南》", author: "alien"}
    return <div> 名称: {props.name} </div>
}

function Groups (props){
    const newChilren = React.cloneElement(props.children,{ author:'alien' })
    return  newChilren
}

这里还是 Groups 只有单一节点的情况,有些同学会问直接在原来的 children 基础上加入新属性不就可以了吗?像如下这样:

props.children.props.author = 'alien'

控制渲染

组合模式可以通过 children 方式获取内层组件,也可以根据内层组件的状态来控制其渲染。比如如下的情况:

export default ()=>{
    return <Groups>
    <Item  isShow name="《React进阶实践指南》" />
    <Item  isShow={false} name="《Nodejs深度学习手册》" />
    <div>hello,world</div>
    { null }
</Groups>
}

实际处理这个很简单,也是通过遍历 children ,然后通过对比 props ,选择需要渲染的 children 。接下来一起看一下如何控制:

function Item (props){
    return <div> 名称: {props.name} </div>
}
/* Groups 组件 */
function Groups (props){
    const newChildren = []
    React.Children.forEach(props.children,(item)=>{
        const { type ,props } = item || {}
        if(isValidElement(item) && type === Item && props.isShow  ){
            newChildren.push(item)
        }
    })
    return  newChildren
}

内外层通信

组合模式可以轻松的实现内外层通信的场景,原理就是通过外层组件,向内层组件传递回调函数 callback ,内层通过调用 callback 来实现两层组合模式的通信关系。

function Item (props){
    return <div>
        名称:{props.name}
        <button onClick={()=> props.callback('let us learn React!')} >点击</button>
    </div>
}

function Groups (props){
    const handleCallback = (val) =>  console.log(' children 内容:',val )
    return <div>
        {React.cloneElement( props.children , { callback:handleCallback } )}
    </div>
}

复杂的组合场景

组合模式还有一种场景,在外层容器中,进行再次组合,这样组件就会一层一层的包裹,一次又一次的强化。这里举一个例子:

function Item (props){
    return <div>
        名称:{props.name}     <br/>
        作者:{props.author}   <br/>
        对大家说:{props.mes}   <br/>
    </div>
}
/* 第二层组合 -> 混入 mes 属性  */
function Wrap(props){
    return React.cloneElement( props.children,{ mes:'let us learn React!' } )
}
/* 第一层组合,里面进行第二次组合,混入 author 属性  */
function Groups (props){
    return <Wrap>
        {React.cloneElement( props.children, { author:'alien' } )}
    </Wrap>
}

export default ()=>{
    return <Groups>
    <Item name="《React进阶实践指南》" />
</Groups>
}

1.jpg

这种组合模式能够一层层强化原始组件,外层组件不用过多关心内层到底做了些什么? 只需要处理 children 就可以,同样内层 children 在接受业务层的 props 外,还能使用来自外层容器组件的状态方法等。

3 注意细节

组合模式也有很多细节值得注意,首先最应该想到的就是对于 children 的类型校验,因为组合模式,外层容器组件对 children 的属性状态是未知的。如果在不确定 children 的状态下,如果直接挂载,就会出现报错等情况。所以验证 children 的合法性就显得非常重要。

验证children

比如如下,本质上形态是属于 render props 形式。

<Groups>
   {()=>  <Item  isShow name="《React进阶实践指南》" />}
</<Groups>

上面的情况,如果 Groups 直接用 children 挂载的话。

function Groups (props){
    return props.children
}

这样的情况,就会报 Functions are not valid as a React child 的错误。那么需要在 Groups 做判断,我们来一起看一下:

function Groups (props){
    return  React.isValidElement(props.children)
     ? props.children
     : typeof props.children === 'function' ?
       props.children() : null
}

绑定静态属性

现在还有一个暴露的问题是,外层组件和内层组件通过什么识别身份呢?比如如下的场景:

<Groups>
   <Item  isShow name="《React进阶实践指南》" />
   <Text />
<Groups>

如下,Groups 内部有两个组件,一个是 Item ,一个是 Text ,但是只有 Item 是有用的,那么如何证明 Item 组件呢。那么我们需要给组件函数或者类绑定静态属性,这里可以统一用 displayName 来标记组件的身份。

那么只需要这么做就可以了:

function Item(){ ... }
Item.displayName = 'Item'

那么在 Groups 中就可以找到对应的 Item 组件,排除 Text 组件。具体可以通过 children 上的 type 属性找到对应的函数或者是类,然后判断 type 上的 displayName 属性找到对应的 Item 组件,本质上 displayName 主要用于调试,这里要记住组合方式,可以使用子组件的静态属性就可以了。 当然也可以通过内存空间相同的方式。

具体参考方式:

function Groups (props){
    const newChildren = []
    React.Children.forEach(props.children,(item)=>{
        const { type ,props } = item || {}
        if(isValidElement(item) && type.displayName === 'Item' ){
            newChildren.push(item)
        }
    })
    return  newChildren
}

通过 displayName 属性找到 Item。

4 实践demo

接下来,我们来简单实现刚开始的 tab,tabItem 切换功能。

tab实现

const Tab = ({ children ,onChange }) => {
    const activeIndex = useRef(null)
    const [,forceUpdate] = useState({})
    /* 提供给 tab 使用  */
    const tabList = []
    /* 待渲染组件 */
    let renderChildren = null
    React.Children.forEach(children,(item)=>{
        /* 验证是否是 <TabItem> 组件  */
        if(React.isValidElement(item) && item.type.displayName === 'tabItem' ){
            const { props } = item
            const { name, label } = props
            const tabItem = {
                name,
                label,
                active: name === activeIndex.current,
                component: item
            }
            if(name === activeIndex.current) renderChildren = item
            tabList.push(tabItem)
        }
    })
    /* 第一次加载,或者 prop chuldren 改变的情况 */
    if(!renderChildren && tabList.length > 0){
        const fisrtChildren = tabList[0]
        renderChildren = fisrtChildren.component
        activeIndex.current = fisrtChildren.component.props.name
        fisrtChildren.active = true
    }

    /* 切换tab */
    const changeTab=(name)=>{
        activeIndex.current = name
        forceUpdate({})
        onChange && onChange(name)
    }

    return <div>
        <div className="header"   >
            {
                tabList.map((tab,index) => (
                    <div className="header_item" key={index}  onClick={() => changeTab(tab.name)} >
                        <div className={'text'}  >{tab.label}</div>
                        {tab.active && <div className="active_bored" ></div>}
                    </div>
                ))
            }
        </div>
        <div>{renderChildren}</div>
    </div>
}

Tab.displayName = 'tab' 

我写的这个 Tab,负责了整个 Tab 切换的主要功能,包括 TabItem 的过滤状态收集控制对应的子组件展示

TabItem 的实现

const TabItem = ({ children }) => {
    return <div>{children}</div>
}
TabItem.displayName = 'tabItem'

这个 demo 中的 TabItem 功能十分简单,大部分事情都交给 Tab 做了。

TabItem 做的事情是:

效果

2.gif

5 总结

组合模式在日常开发中,用途还是比较广泛的,尤其是在一些比较出色的开源项目中,组合模式的总结内容如下:

总结流程图如下:

13.jpg

三 render props模式

1 介绍

render props 模式和组合模式类似。区别不同的是,用函数的形式代替 children。函数的参数,由容器组件提供,这样的好处,将容器组件的状态,提升到当前外层组件中,这个是一个巧妙之处,也是和组合模式相比最大的区别。

我们先来看一下一个基本的 render props 长什么样子:

export default function App (){
    const aProps = {
        name:'《React进阶实践指南》'
    }
    return <Container>
        {(cProps) => <Children {...cProps} { ...aProps }  />}
    </Container>
}

如上是 render props 的基本样子。可以清楚的看到:

接下来我们研究一下 render props 原理和细节。

2 原理和细节

首先一个问题是 render props 这种方式到底适合什么场景,实际这种模式更适合一种,容器包装,状态的获取。可能这么说有的同学不明白。那么一起看一下 context 中的 Consumer。就采用 render props 模式。

const Context = React.createContext(null)
function Index(){
    return <Context.Consumer>
           {(contextValue)=><div>
               名称:{contextValue.name}
               作者:{contextValue.author}
           </div>}
         </Context.Consumer>
}

export default function App(){
    const value = {
        name:'《React进阶实践指南》',
        author:'我不是外星人'
    }
    return <Context.Provider value={value} >
        <Index />
    </Context.Provider>
}

那么接下来模拟一下 Consumer 的内部实现。

function myConsumer(props){
    const contextValue = useContext(Context)
    return props.children(contextValue)
}

如上就模拟了一个 Consumer 功能,从 Consumer 的实现看 render props 本质就是容器组件产生状态,再通过 children 函数传递下去。所以这种模式我们应该更在乎的是,容器组件能提供些什么?

派生新状态

相比传统的组合模式,render props 还有一个就是灵活性,可以通过容器组件的状态和当前组件的状态结合,派生出新的状态。比如如下

 <Container>
        {(cProps) => {
            const  const nProps =  getNewProps( aProps , cProps )
            return <Children {...nProps} />
        }}
 </Container>

反向状态回传

这种情况比较极端,笔者也用过这种方法,就是可以通过 render props 中的状态,提升到当前组件中,也就是把容器组件内的状态,传递给父组件。比如如下情况。

function GetContanier(props){
    const dom = useRef()
    const getDom = () =>  dom.current
    return <div ref={dom} >
        {props.children({ getDom })}
    </div>
}

export default function App(){
     /* 保存 render props 回传的状态 */
     const getChildren = useRef(null)
     useEffect(()=>{
        const childDom = getChildren.current()
        console.log( childDom,'childDom' )
     },[])
    return <GetContanier>
        {({getDom})=>{
            getChildren.current = getDom
            return <div></div>
        }}
    </GetContanier>
}

3.jpg

但是现实情况不可能是获取一个 dom 这么简单,真实情景下,回传的内容可能更加复杂。

3 注意问题

render props 的注意问题还是对 children 的校验,和组合模式不同的是,这种模式需要校验 children 是一个函数,只有是函数的情况下,才能执行函数,传递 props 。打一个比方:

function Container (props){
    const renderChildren =  props.children
    return typeof renderChildren === 'function' ? renderChildren({ name:'《React进阶时间指南》' }) : null
}
export default function App(){
    return <Container>
        {(props)=> <div> 名称 :{props.name} </div>}
    </Container>
}

4 实践demo

接下来我们实现一个 demo。通过 render props 实现一个带 loading 效果的容器组件,被容器组件包裹,会通过 props 回传开启 loading 的方法 ( 现实场景下,不一定会这么做,这里只是方便同学学习 render props 模式 ) 。

容器组件 Container

function Container({ children }){
   const [ showLoading, setShowLoading ] = useState(false)
   const renderChildren = useMemo(()=> typeof children === 'function' ? children({ setShowLoading }) : null  ,[children] )
   return <div style={{ position:'relative' }} >
     {renderChildren}
     {showLoading &&  <div className="mastBox" >
          {<SyncOutlined  className="icon"  spin twoToneColor="#52c41a" />}
     </div>}
   </div>
}

外层使用

export default function Index(){
    const setLoading = useRef(null)
    return <div>
        <Container>
            {({ setShowLoading })=>{
                console.log('渲染')
                setLoading.current = setShowLoading
                return <div>
                     <div className="index1" >
                         <button onClick={() => setShowLoading(true)} >loading</button>
                     </div>
                </div>
            }}
        </Container>
        <button onClick={() => setLoading.current && setLoading.current(false)} >取消 loading </button>
    </div>
}

效果

4.gif

5 总结

接下来我们总结一下 render props 的特点。

这种模式下的原理图如下所示:

14.jpg

四 hoc 模式

1 介绍

hoc 高阶组件模式也是 React 比较常用的一种包装强化模式之一,高阶函数是接收一个函数,返回一个函数,而所谓高阶组件,就是接收一个组件,返回一个组件,返回的组件是根据需要对原始组件的强化。

我们来看一下 hoc 的通用模式。hoc 本质上就是一个函数。

function Hoc (Component){
    return class Wrap extends React.Component{
        //---------
        // 强化操作
        //---------
        render(){
            return <Component { ...this.props } />
        }
    }
}

传统的 HOC 模式如上,我们可以看清楚一个传统的 HOC 做了哪些事。

2 原理

接下来我们看一下 hoc 的具体实现原理。hoc 的实现有两种方式,属性代理反向继承

属性代理所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,我们可以做一些,对源组件的代理操作。我们可以理解为父子组件关系,父组件对子组件进行一系列强化操作。而 hoc 本身就是返回强化子组件的父组件。

function HOC(WrapComponent){
    return class Advance extends React.Component{
       state={
           name: '《React 进阶实践指南》',
           author:'我不是外星人'
       }
       render(){
           return <WrapComponent  { ...this.props } { ...this.state }  />
       }
    }
}

属性代理特点:

反向继承

反向继承和属性代理有一定的区别,在于包装后的组件继承了业务组件本身,所以我们我无须再去实例化我们的业务组件。当前高阶组件就是继承后,加强型的业务组件。这种方式类似于组件的强化,所以你必须要知道当前继承的组件的状态,内部做了些什么?

class Index extends React.Component{
  render(){
    return <div> hello,world  </div>
  }
}
function HOC(Component){
    return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */

    }
}
export default HOC(Index) 

3 功能及注意事项

上面介绍了 hoc 的二种实现方式,接下来看一下 hoc 能做些什么?以及 hoc 模式的注意事项。

HOC 的功能

对于属性代理HOC,我们可以:

对于反向代理的HOC,我们可以:

如果你对上面的每一个功能的具体场景不清楚的话,建议看一下笔者的另外一篇文章:一文吃透React高阶组件(HOC)

HOC 注意事项

4 实践demo

之前有同学在面试中,遇到了这样一个问题,就是如果控制组件挂载的先后顺序,比如如下的场景

export default function Index(){
    return <div>
        <ComponentA />
        <ComponentB />
        <ComponentC />
    </div>
}

如上,有三个子组件,ComponentAComponentBComponentC,现在期望执行顺序是 ComponentA 渲染完成,挂载 ComponentB ,ComponentB 渲染完成,挂载 ComponentC,也就是三个组件是按照先后顺序渲染挂载的,那么如何实现呢?

实际上,这种情况完全可以用一个 hoc 来实现,那么接下来,请大家跟上我的思路实现这个场景。 首先这个 hoc 是针对当前 index 下面,ComponentA | ComponentB | ComponentC 一组 component 进行功能强化。所以这个 hoc 最好可以动态创建,而且服务于当前一组组件。那么可以声明一个生产 hoc 的函数工厂。

function createHoc(){
   const renderQueue = []            /* 待渲染队列 */
    return function Hoc(Component){  /* Component - 原始组件   */
        return class Wrap extends React.Component{  /* hoc 包装组件 */

        }
    }
}

那么我们需要先创建一个 hoc,作为这一组组件的使用。

使用:

const loadingHoc = createHoc()

知道了 hoc 的动态产生,接下来具体实现一下这个 hoc 。

function createHoc(){
    const renderQueue = [] /* 待渲染队列 */
    return function Hoc(Component){

        function RenderController(props){  /* RenderController 用于真正挂载原始组件  */
            const { renderNextComponent ,...otherprops  } = props
            useEffect(()=>{
                renderNextComponent() /* 通知执行下一个需要挂载的组件任务 */
            },[])
            return <Component  {...otherprops}  />
        }

        return class Wrap extends React.Component{
            constructor(){
                super()
                this.state = {
                    isRender:false
                }
                const tryRender = ()=>{
                    this.setState({
                        isRender:true
                    })
                }
                if(renderQueue.length === 0) this.isFirstRender = true
                renderQueue.push(tryRender)
            }
            isFirstRender = false      /* 是否是队列中的第一个挂载任务 */
            renderNextComponent=()=>{  /* 从更新队列中,取出下一个任务,进行挂载 */
                if(renderQueue.length > 0 ){
                    console.log('挂载下一个组件')
                    const nextRender = renderQueue.shift()
                    nextRender()
                }
            }
            componentDidMount(){  /* 如果是第一个挂载任务,那么需要 */
                this.isFirstRender && this.renderNextComponent()
            }
            render(){
                const { isRender } = this.state
                return isRender ? <RenderController {...this.props} renderNextComponent={this.renderNextComponent}  /> : <SyncOutlined   spin />
            }
        }
    }
}

分析一下主要流程:

使用:

/* 创建 hoc  */
const loadingHoc = createHoc()

function CompA(){
    useEffect(()=>{
        console.log('组件A挂载完成')
    },[])
    return <div>组件 A </div>
}
function CompB(){
    useEffect(()=>{
        console.log('组件B挂载完成')
    },[])
    return <div>组件 B </div>
}
function CompC(){
    useEffect(()=>{
        console.log('组件C挂载完成')
    },[])
    return  <div>组件 C </div>
}

function CompD(){
    useEffect(()=>{
        console.log('组件D挂载完成')
    },[])
    return  <div>组件 D </div>
}
function CompE(){
    useEffect(()=>{
        console.log('组件E挂载完成')
    },[])
    return  <div>组件 E </div>
}


const ComponentA = loadingHoc(CompA)
const ComponentB = loadingHoc(CompB)
const ComponentC = loadingHoc(CompC)
const ComponentD = loadingHoc(CompD)
const ComponentE = loadingHoc(CompE)

export default function Index(){
    const [ isShow, setIsShow ] = useState(false)
    return <div>
        <ComponentA />
        <ComponentB />
        <ComponentC />
        {isShow && <ComponentD />}
        {isShow && <ComponentE />}
        <button onClick={()=> setIsShow(true)} > 挂载组件D ,E </button>
    </div>
}

效果:

5.gif

11.jpg

完美达成需求。

5 总结

HOC 在实际项目中,应用还是很广泛的,尤其是一些优秀的开源项目中,这里总结了一下 HOC 的原理图:

属性代理

反向继承

16.jpg

五 提供者模式

1 介绍

首先我们来思考一下,为什么 React 会有提供者这种模式呢?

带着这个疑问,首先假设一个场景:在 React 的项目有一个全局变量 themetheme 可能是初始化数据交互获得的,也有可能是切换主题变化的),有一些视图 UI 组件(比如表单 input 框、 button 按钮),需要 theme 里面的变量来做对应的视图渲染,现在的问题是怎么能够把 theme 传递下去,合理分配到用到这个 theme 的地方。

如果用 props 解决这个问题,那么需要通过 props 层层绑定,而且还要考虑 pureComponentmemo 策略的影响。

所以这个时候用提供者模式最好不过了。React 提供了 context ‘提供者’模式,具体模式是这样的,React组件树 Root 节点,用 Provider 提供者注入 theme,然后在需要 theme的 地方,用 Consumer 消费者形式取出theme,供给组件渲染使用即可,这样减少很多无用功。用官网上的一句话形容就是Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

但是必须注意一点是,提供者永远要在消费者上层,正所谓水往低处流,提供者一定要是消费者的某一层父级。提供者模式的结构图如下:

8.jpg

2 用法介绍

对于提供者模式的用法,有老版本的 context 和新版本的 context 之分。接下来重点介绍一下两种方式。

老版本提供者模式

在 React v16.3.0 之前,要实现提供者,就要实现一个 React 组件,不过这个组件要做特殊处理。下面就是一个实现“提供者”的例子,组件名为 ThemeProvider:

提供者

class ThemeProvider extends React.Component {
  getChildContext() {
    return {
      theme: this.props.value
    }
  }

  render() {
    return (
      <div>
         { this.props.children }
      </div>
    );
  }
}
ThemeProvider.childContextTypes = {
  theme: PropTypes.object
}

使用

<ThemeProvider value={ { color:'pink' } } >
    <Index />
</ThemeProvider>

消费者

const ThemeConsumer = (props, context) => {
  const {color} = context.theme
  return (
    <p style={{color }}>
      {props.children}
    </p>
  );
}

ThemeConsumer.contextTypes = {
  theme: PropTypes.object
}

新版本提供者模式

到了 React v16.3.0 的时候,新的 Context API 出来了,开发者可以创建一个 Context , Context 上有两个属性就是 ProviderConsumer

那么接下来介绍一下具体如何使用,首先开发者需要用 createContext api 创建一个 context。

const ThemeContext = React.createContext();

然后就是新版本 ProviderConsumer的实现。

新版提供者

function ThemeProvider(){
    const theme = { color:'pink' }
    return <ThemeContext.Provider value={ theme } >
        <Index />
    </ThemeContext.Provider>
}

新版消费者

function ThemeConsumer(props){
    return <ThemeContext.Consumer>
      { (theme)=>{ /* render children函数 */
          const { color } = theme
          return <p style={{color }}>
           {props.children}
       </p>
      } }
    </ThemeContext.Consumer>
}

3 实践demo

接下来我们实现一个提供者模式的实践 demo ,通过动态 context 来让消费 context 的 Consumer 动态渲染。


const ThemeContext = React.createContext(null) // 创建一个 context 上下文 ,主题颜色Context

function ConsumerDemo(){
    return <div>
         <ThemeContext.Consumer>
        {
            (theme) => <div style={{ ...theme}} >
                  <p>i am alien!</p>
                  <p>let us learn React!</p>
             </div>
        }
        </ThemeContext.Consumer>
    </div>
}

class Index extends React.PureComponent{
    render(){
        return <div>
            <ConsumerDemo />
        </div>
    }
}

export default function ProviderDemo(){
    const [ theme , setTheme ]= useState({ color:'pink' , background:'#ccc' })
    return <div>
       <ThemeContext.Provider value={theme}  >
          <Index  />
       </ThemeContext.Provider>
       <button onClick={()=>setTheme({ color:'blue' , background:'orange'  })} >点击</button>
    </div>
}

效果:

9.gif

4 总结

提供者模式在日常开发中,用的频率还是很高的,比如全局传递状态,保存状态。这里用一幅图总结提供者模式的原理。

17.jpg

六 类组件继承

1 介绍

React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用 虽然 React 官方推荐用组合方式,而非继承方式。但是也不是说明继承这种方式没有用武之地,继承方式还是有很多应用场景的。

在 class 组件盛行之后,我们可以通过继承的方式进一步的强化我们的组件。这种模式的好处在于,可以封装基础功能组件,然后根据需要去 extends 我们的基础组件,按需强化组件,但是值得注意的是,必须要对基础组件有足够的掌握,否则会造成一些列意想不到的情况发生。

我们先来看一个

class Base extends React.Component{
  constructor(){
    super()
    this.state={
      name:'《React 进阶实践之指南》'
    }
  }
  componentDidMount(){}
  say(){
    console.log('base components')
  }
  render(){
    return <div> hello,world <button onClick={ this.say.bind(this) } >点击</button>  </div>
  }
}
class Index extends Base{
  componentDidMount(){
    console.log( this.state.name )
  }
  say(){ /* 会覆盖基类中的 say  */
    console.log('extends components')
  }
}
export default Index

2 特性

继承增强效果很优秀。它的优势如下:

但是也有值得注意的地方,就是 state 和生命周期会被继承后的组件修改。像上述 demo 中, Person 组件中的 componentDidMount 生命周期将不会被执行。

3 实践demo

接下来我们实现一个继承功能,继承的组件就是耳熟能详的 React-Router 中的 Route 组件,强化它,使它变成可以受到权限的控制。

代码编写

import { Route } from 'react-router'

const RouterPermission = React.createContext()

class PRoute extends Route{
    static contextType = RouterPermission  /* 使用 context */
    constructor(...arg){
        super(...arg)
        const { path } = this.props
        /* 如果有权限 */
        console.log(this.context)
        const isPermiss = this.context.indexOf(path) >= 0 /* 判断是否有权限 */
        if(!isPermiss) {
            /* 修改 render 函数,如果没有权限,重新渲染一个 Route ,ui 是无权限展示的内容  */
            this.render = () =>  <Route  {...this.props}   >
                <div>暂无权限</div>
            </Route>
        }
    }
}
export default (props)=>{
    /* 模拟的有权限的路由列表 */
    const permissionList = [ '/extends/a' , '/extends/b'  ]
   return  <RouterPermission.Provider value={permissionList} >
       <Index {...props} />
   </RouterPermission.Provider>
}

使用

function Test1 (){
    return <div>权限路由测试一</div>
}

function Test2 (){
    return <div>权限路由测试二</div>
}

function Test3(){
    return <div>权限路由测试三</div>
}

function Index({ history }){
    const routerlist=[
        { name:'测试一' ,path:'/extends/a' },
        { name:'测试二' ,path:'/extends/b' },
        { name:'测试三' ,path:'/extends/c' }
    ]
    return <div>
        {
            routerlist.map(item=> <button key={item.path}
                onClick={()=> history.push(item.path)}
                                  >{item.path}</button> )
        }
        <PRoute component={Test1}
            path="/extends/a"
        />
        <PRoute component={Test2}
            path="/extends/b"
        />
        <PRoute component={Test3}
            path="/extends/c"
        />
    </div>
}

效果

7.gif

4 总结

继承模式的应用前提是,你需要知道被继承的组件是什么,内部都有什么状态和方法,对继承的组件内部的运转是透明的。接下来用一幅图表示继承模式原理。

18.jpg

七 总结

本章节讲了 React 中常用的几个设计模式。希望同学们看完可以手动敲起来,把这些设计模式运用到真实的项目中。

参考资料

「react进阶」一文吃透React高阶组件(HOC)

React进阶实践指南

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8