今天我们来悉数一下 React 中一些不错的设计模式,这些设计模式能够解决一些功能复杂,逻辑复用 的问题,还能锻炼开发者的设计和编程能力,以为多年开发经验来看,学好这些设计模式,那就是一个字 香!
基本上每一个设计模式,笔者都会绞尽脑汁的想出两个 demo,希望屏幕前的你能给笔者赏个赞,以此鼓励我继续创作前端硬文。
老规矩,我们带着疑问开始今天的阅读:
我相信读完这篇文章,这些问题全都会迎刃而解。
首先我们想一个问题,那就是 为什么要学习设计模式? 原因我总结有以下几个方面。
场景一:
在一个项目中,全局有一个状态,可以称之为 theme (主题),那么有很多 UI 功能组件需要这个主题,而且这个主题是可以切换的,就像 github 切换暗黑模式一样,那么如何优雅的实现这个功能呢?
这个场景如果我们用 React 的提供者模式,就能轻松搞定了,通过 context
保存全局的主题,然后将 theme
通过 Provider
形式传递下去,需要 theme ,那么消费 context ,就可以了,这样的好处是,只要 theme 改变,消费 context 的组件就会重新更新,达到了切换主题的目的。
场景二:
表单设计场景也需要一定程度上的 React 的设计模式,首先对于表单状态的整体验证需要外层的 Form
绑定事件控制,调度表单的状态下发,验证功能。内层对于每一个表单控件还需要 FormItem
收集数据,让控件变成受控的。这样的 Form
和 FormItem
方式,就是通过组合模式实现的。
熟练运用 React 的设计模式,可以培养开发者的设计能力,比如 HOC
的设计 ,公共组件的设计 ,自定义 hooks 的设计,一些开源的优秀的库就是通过 React 的灵活性和优秀的设计模式实现的。
例子一:
比如在 React 状态管理工具中,无论是 react-redux
,还是 mobx-react
,一方面想要把 state
和 dispatch
函数传递给组件,另一方面订阅 state 变化,来促使业务组件更新,那么整个流程中,需要一个或多个 HOC 来搞定。于是 react-redux 提供了 connect
,mobx-react 提供了 inject
,observer
等优秀的 hoc。由此可见,学会 React 的设计模式,有助于开发者小到编写公共组件,大到开发开源项目。
今天我重点介绍 React 的五种设计模式,分别是:
组合模式适合一些容器组件场景,通过外层组件包裹内层组件,这种方式在 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>
如上 Tabs
和 TabItem
组合,构成切换 tab 功能,那么 Tabs 和 TabItem 的分工如下:
我们直观上看到 Tabs 和 TabItem 并没有做某种关联,但是却无形的联系起来。这种就是组合模式的精髓所在,这种组合模式的组件,给使用者感觉很舒服,因为大部分工作,都在开发组合组件的时候处理了。所以编写组合模式的嵌套组件,对锻炼开发者的 React 组件封装能力是很有帮助的。
接下来我们一起看一下,组合模式内部是如何实现的。
实际组合模式的实现并没有想象中那么复杂,主要分为外层和内层两部分,当然可能也存在多层组合嵌套的情况,但是万变不离其宗,原理都是一样的。首先我们看一个简单的组合结构:
<Groups>
<Item name="《React进阶实践指南》" />
</Groups>
Groups
能对 Item
做一些什么操作呢 ?Item 在 Groups的形态
首先如果如上组合模式的写法,会被 jsx
编译成 React element
形态,Item
可以通过 Groups
的 props.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
}
React.cloneElement
创建一个新的 element,然后混入其他的 props -> author 属性,React.cloneElement 的第二个参数,会和之前的 props 进行合并 ( merge )。这里还是 Groups 只有单一节点的情况,有些同学会问直接在原来的 children 基础上加入新属性不就可以了吗?像如下这样:
props.children.props.author = 'alien'
cloneElement
来实现。控制渲染
组合模式可以通过 children 方式获取内层组件,也可以根据内层组件的状态来控制其渲染。比如如下的情况:
export default ()=>{
return <Groups>
<Item isShow name="《React进阶实践指南》" />
<Item isShow={false} name="《Nodejs深度学习手册》" />
<div>hello,world</div>
{ null }
</Groups>
}
isShow = true
的 Item 组件。那么外层组件是如何处理的呢?实际处理这个很简单,也是通过遍历 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
}
newChildren
存放满足要求的 React Element ,通过 Children.forEach
遍历 children 。isValidElement
排除非 element 节点;type
指向 Item
函数内存,排除非 Item 元素;获取 isShow 属性,只展示 isShow = true 的 Item
,最终效果满足要求。内外层通信
组合模式可以轻松的实现内外层通信的场景,原理就是通过外层组件,向内层组件传递回调函数 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>
}
Groups
向 Item
组件中隐式传入回调函数 callback
,将作为新的 props 传递。Item
可以通过调用 callback
向 Groups
传递信息。实现了内外层的通信。复杂的组合场景
组合模式还有一种场景,在外层容器中,进行再次组合,这样组件就会一层一层的包裹,一次又一次的强化。这里举一个例子:
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>
}
Groups
组件里通过 Wrap
再进行组合。经过两次组合,把 author
和 mes
混入到 props 中。1.jpg
这种组合模式能够一层层强化原始组件,外层组件不用过多关心内层到底做了些什么? 只需要处理 children 就可以,同样内层 children 在接受业务层的 props 外,还能使用来自外层容器组件的状态,方法等。
组合模式也有很多细节值得注意,首先最应该想到的就是对于 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
}
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。
接下来,我们来简单实现刚开始的 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 的过滤,状态收集,控制对应的子组件展示。
Children.forEach
找到符合条件的 TabItem
。收集 TabItem
的 props,形成菜单结构。children
,渲染正确的 children 。changeTab
。Tab
组件。这个主要目的方便调试。TabItem 的实现
const TabItem = ({ children }) => {
return <div>{children}</div>
}
TabItem.displayName = 'tabItem'
这个 demo 中的 TabItem 功能十分简单,大部分事情都交给 Tab 做了。
TabItem 做的事情是:
children
( 我们写在 TabItem 里面的内容 )displayName
。效果
2.gif
组合模式在日常开发中,用途还是比较广泛的,尤其是在一些比较出色的开源项目中,组合模式的总结内容如下:
总结流程图如下:
13.jpg
render props
模式和组合模式类似。区别不同的是,用函数的形式代替 children
。函数的参数,由容器组件提供,这样的好处,将容器组件的状态,提升到当前外层组件中,这个是一个巧妙之处,也是和组合模式相比最大的区别。
我们先来看一下一个基本的 render props 长什么样子:
export default function App (){
const aProps = {
name:'《React进阶实践指南》'
}
return <Container>
{(cProps) => <Children {...cProps} { ...aProps } />}
</Container>
}
如上是 render props 的基本样子。可以清楚的看到:
cProps
为 Container
组件提供的状态。aProps
为 App
提供的状态。这种模式优点是,能够给 App 的子组件 Container 的状态提升到 App 的 render 函数中。然后可以组合成新的 props,传递给 Children,这种方式让容器化的感念更显而易见。接下来我们研究一下 render props 原理和细节。
首先一个问题是 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>
}
contextValue
从下游向上游提取。那么接下来模拟一下 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
GetContanier
将获取元素的方法 getDom
通过 render props 回传给父组件。getChildren
保存 render props 回传的内容,在 useEffect
调用 getDom 方法,打印内容如下:但是现实情况不可能是获取一个 dom 这么简单,真实情景下,回传的内容可能更加复杂。
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>
}
typeof
判断 children
是一个函数,如果是函数,那么执行函数,传递 props 。接下来我们实现一个 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>
}
useState
用于显示 loading 效果,useMemo 用于执行 children
函数,把改变 state 的方法 setShowLoading 传入 props 中。这里有一个好处就是当 useState 改变的时候,不会触发 children
的渲染。showLoading
来显示 loading 效果。外层使用
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>
}
setShowLoading(true)
显示 loading 效果。Container
外层也可以调用 setShowLoading 来让 loading 效果消失。效果
4.gif
接下来我们总结一下 render props 的特点。
这种模式下的原理图如下所示:
14.jpg
hoc 高阶组件模式也是 React 比较常用的一种包装强化模式之一,高阶函数是接收一个函数,返回一个函数,而所谓高阶组件,就是接收一个组件,返回一个组件,返回的组件是根据需要对原始组件的强化。
我们来看一下 hoc 的通用模式。hoc 本质上就是一个函数。
function Hoc (Component){
return class Wrap extends React.Component{
//---------
// 强化操作
//---------
render(){
return <Component { ...this.props } />
}
}
}
传统的 HOC 模式如上,我们可以看清楚一个传统的 HOC 做了哪些事。
Component
,也就是原始组件本身。Component
。接下来我们看一下 hoc 的具体实现原理。hoc 的实现有两种方式,属性代理和反向继承。
属性代理所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,我们可以做一些,对源组件的代理操作。我们可以理解为父子组件关系,父组件对子组件进行一系列强化操作。而 hoc 本身就是返回强化子组件的父组件。
function HOC(WrapComponent){
return class Advance extends React.Component{
state={
name: '《React 进阶实践指南》',
author:'我不是外星人'
}
render(){
return <WrapComponent { ...this.props } { ...this.state } />
}
}
}
属性代理特点:
props
属性增强,只负责控制子组件渲染和传递额外的 props
就可以,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的 hoc
,目前开源的 HOC
基本都是通过这个模式实现的。class
声明组件,和 function
声明的组件。反向继承
反向继承和属性代理有一定的区别,在于包装后的组件继承了业务组件本身,所以我们我无须再去实例化我们的业务组件。当前高阶组件就是继承后,加强型的业务组件。这种方式类似于组件的强化,所以你必须要知道当前继承的组件的状态,内部做了些什么?
class Index extends React.Component{
render(){
return <div> hello,world </div>
}
}
function HOC(Component){
return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */
}
}
export default HOC(Index)
上面介绍了 hoc 的二种实现方式,接下来看一下 hoc 能做些什么?以及 hoc 模式的注意事项。
HOC 的功能
对于属性代理HOC,我们可以:
对于反向代理的HOC,我们可以:
如果你对上面的每一个功能的具体场景不清楚的话,建议看一下笔者的另外一篇文章:一文吃透React高阶组件(HOC)
HOC 注意事项
hoist-non-react-statics
自动拷贝所有的静态方法。ref
,通过 forwardRef
转发 ref
。HOC
,如果在 render 声明 hoc,可能会造成组件反复挂载情况发生。之前有同学在面试中,遇到了这样一个问题,就是如果控制组件挂载的先后顺序,比如如下的场景
export default function Index(){
return <div>
<ComponentA />
<ComponentB />
<ComponentC />
</div>
}
如上,有三个子组件,ComponentA
,ComponentB
,ComponentC
,现在期望执行顺序是 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 />
}
}
}
}
分析一下主要流程:
createHoc
来创建需要顺序加载的 hoc ,renderQueue
存放待渲染的队列。Component
。RenderController
用于真正挂载原始组件,用 useEffect 通知执行下一个需要挂载的组件任务,在 hooks 原理的文章中,我讲过 useEffect 采用异步执行,也就是说明,是在渲染之后,浏览器绘制已经完成。RenderController
,主要用于渲染更新任务,isFirstRender
证明是否是队列中的第一个挂载任务,如果是第一个挂载任务,那么需要在 componentDidMount
开始挂载第一个组件。tryRender
方法,里面调用了 setState 来渲染 RenderController
。renderNextComponent
原理很简单,就是获取第一个更新任务,然后执行就可以了。使用:
/* 创建 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
完美达成需求。
HOC 在实际项目中,应用还是很广泛的,尤其是一些优秀的开源项目中,这里总结了一下 HOC 的原理图:
属性代理
反向继承
16.jpg
首先我们来思考一下,为什么 React 会有提供者这种模式呢?
带着这个疑问,首先假设一个场景:在 React 的项目有一个全局变量 theme
( theme
可能是初始化数据交互获得的,也有可能是切换主题变化的),有一些视图 UI 组件(比如表单 input
框、 button
按钮),需要 theme
里面的变量来做对应的视图渲染,现在的问题是怎么能够把 theme
传递下去,合理分配到用到这个 theme
的地方。
如果用 props
解决这个问题,那么需要通过 props
层层绑定,而且还要考虑 pureComponent
, memo
策略的影响。
所以这个时候用提供者模式最好不过了。React 提供了 context ‘提供者’模式,具体模式是这样的,React组件树 Root 节点,用 Provider 提供者注入 theme,然后在需要 theme的 地方,用 Consumer 消费者形式取出theme,供给组件渲染使用即可,这样减少很多无用功。用官网上的一句话形容就是Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
但是必须注意一点是,提供者永远要在消费者上层,正所谓水往低处流,提供者一定要是消费者的某一层父级。提供者模式的结构图如下:
8.jpg
对于提供者模式的用法,有老版本的 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
}
getChildContext
方法,用于返回数据就是向子孙组件传递的上下文;childContextTypes
属性,声明“上下文”的结构类型。使用
<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
}
contextTypes
指定将要消费哪个 context ,否则将无效。到了 React v16.3.0 的时候,新的 Context API 出来了,开发者可以创建一个 Context , Context 上有两个属性就是 Provider
和 Consumer
。
Provider
用于提供 context 。Consumer
用于消费 context 。那么接下来介绍一下具体如何使用,首先开发者需要用 createContext api 创建一个 context。
const ThemeContext = React.createContext();
然后就是新版本 Provider
和 Consumer
的实现。
新版提供者
function ThemeProvider(){
const theme = { color:'pink' }
return <ThemeContext.Provider value={ theme } >
<Index />
</ThemeContext.Provider>
}
ThemeContext
上的 Provider
传递主题信息 theme
。新版消费者
function ThemeConsumer(props){
return <ThemeContext.Consumer>
{ (theme)=>{ /* render children函数 */
const { color } = theme
return <p style={{color }}>
{props.children}
</p>
} }
</ThemeContext.Consumer>
}
useContext
自定义 hooks ,对于类组件有 contextType
静态属性。接下来我们实现一个提供者模式的实践 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
提供者模式在日常开发中,用的频率还是很高的,比如全局传递状态,保存状态。这里用一幅图总结提供者模式的原理。
17.jpg
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
Base
为基础组件,提供一些基础的方法和功能,包括 UIIndex
为基于 Base 继承的组件,可以针对 Index 做一些功能性的强化。继承增强效果很优秀。它的优势如下:
但是也有值得注意的地方,就是 state
和生命周期会被继承后的组件修改。像上述 demo
中, Person
组件中的 componentDidMount
生命周期将不会被执行。
接下来我们实现一个继承功能,继承的组件就是耳熟能详的 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>
}
/extends/a
和 /extends/b
。react-router
中的 Route
组件。contextType
消费指定的权限上下文 RouterPermission context
。constructor
中进行判断,如果有权限,那么不用做任何处理,如果没有权限,那么重写 render 函数,用 Route 做一个展示容器,展示无权限的 UI 。使用
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
[ '/extends/a' , '/extends/b' ]
权限能展示,无权限提示暂无权限,完美达到效果。继承模式的应用前提是,你需要知道被继承的组件是什么,内部都有什么状态和方法,对继承的组件内部的运转是透明的。接下来用一幅图表示继承模式原理。
18.jpg
本章节讲了 React 中常用的几个设计模式。希望同学们看完可以手动敲起来,把这些设计模式运用到真实的项目中。
「react进阶」一文吃透React高阶组件(HOC)
React进阶实践指南
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ghO-T_kTlXppPq9Z1nAdXQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。