React数据流
在React
中,数据是自顶向下单向流动的,即从父组件到子组件。这条原则让组件之间的关系变得简单且可预测。
state
与props
是React
组件中最重要的概念,如果顶层组件初始化props
,那么React
会向下遍历整棵组件树,重新尝试渲染所有相关的子组件。而state
只关心每个组件自己内部的状态,这些状态只能在组件内改变。把组件看成一个函数,那么它接受了props
作为参数,内部由state作为函数的内部参数,返回一个virtual DOM
实现。
其中react
有三个非常重要的概念:state
、props
与context
。state
其实应该被称为内部状态或是局部状态。“内部”表示它很少"跑出"组件,状态意味着它经常发生改变。Props
与context
用于在组件中传递数据,props
仅仅支持逐层传递数据,但是context
则支持跨级传递。State
、props
与context
都是react
中的数据载体,他们都是各司其职,让数据在组件中优雅的变化和流动。
state
在使用React
之前,常见的MVC
框架也非常容易实现交互界面的状态管理。在MVC
框架将View
中与界面交互的状态解耦,一般将状态放在Model
中管理。但是React
没有结合Flux
或是Redux
框架前,React
也同样可以管理组件的内部状态。在React中,把这类状态统一称为state
。
当组件内部使用库内置的setState
方法时,最大的表现行为就是该组件会尝试重新渲染。这很好理解,因为我们改变了内部的状态,组件需要更新了。我们编写一个计数器组件:
import React, {Copmonent} from 'react';
class Counter extends Component{
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
this.state ={
count:0,
};
}
handleClick(e){
e.preventDefault();
this.setState({
count:this.state.count + 1,
});
}
render(){
return(
<div>
<p>{this.state.count}</p>
<a href='#' onClick = {this.handleClick}>更新</a>
</div>
);
}
}
在React
中常常在事件处理方法中更新state
,上面的例子中就是通过点击“更新”按钮不断地更新内部cout
的值,这样就可以把组件内状态封装在实现中。
值得注意的是,setState
是一个异步的方法,一个生命周期内所有的setState
方法会合并操作,有了这个特性,让React
变得充满想象力,我们完全可以只用React
来完成行为控制、数据的更新和界面的渲染。然而,随着内容的深入,我们发现官方并不推荐开发者滥用state
。因为过多的内部状态会让数据流混乱,数据变得难以维护。
我们再看一下Tabs
组件的state
,我们了解到应该有两个内部状态-- activeIndex
和preIndex
,这两个状态分别表示当前选中tab
的索引和前一次选中的tab
的索引,我们需要注意的是,当前选中的索引也是组件本身需要的参数之一。
我们针对activeIndex
做为state
,就有两种不同的视角:
-
activeIndex
在内部更新:当我们切换tab
标签时,可以看做是组件内部的交互行为,被选择后通过回调函数返回具体选择的索引。 -
activeIndex
在外部更新:当我们切换tab
标签时,可以看做是组件外部在传入具体的索引,而组件就像‘木偶’一样被操作。
这两种情况在React
组件的设计非常的常见,我们形象的把第一种和第二种视角写成的组件分别称为智能组件(smart component
)和木偶组件(dumb component
)
实现组件的时候,可以同时考虑兼容这两种。我们来看一下Tabs
组件初始化时实现部分:
constructor (props){
super(props);
const currProps = this.props;
let activeIndex = 0;
if('activeIndex' in currProps){
activeIndex = currProps.activeIndex;
}else if('defaultActiveIndex' in currProps){
activeIndex = currProps.defaultActiveIndex;
}
this.state ={
activeIndex ,
preIndex:activeIndex ;
}
}
props
props
是React
中的另外的一个重要概念。props
是React
用来让组件之间互相联系的一种机制,通俗的说就像方法传入参数一样。
props
的传统过程,对于React
组件来说是非常直观的。React
的单向数据流,主要的流动管道就是props
。props
本身是不可变的,当我们试图改变props
的原始值的时候,React
会报出类型错误的警告,组件的props
一定来自于 默认属性或通过父组件传递而来。如果说要渲染一个对props
加工后的值,最简单的方法就是使用局部变量或直接在JSX
中计算结果。
我们之前了解到Tabs
组件的数据都是通过data prop
传入的,也就是<Tabs data = {data} />
。那么Tabs
组件的props
还会有哪些,我们看一下下面的几项:
-
className
:根节点的class
,为了方便覆盖其原始样式,我们都会在根节点上定义class
。 -
classPrefix
:class
前缀,对于组件来说,定义一个统一的class
前缀,对样式与交互分离起了很重要的作用。 -
defaultActiveIndex
和activeIndex
:默认的激活索引。 -
onChange
:回调函数,当我们切换tab的时候,外组件需要知道组件的内部信息,尤其是当前tab
的索引号的信息,onChange
一般与activeIndex
搭配使用。
React
为props
同样提供了默认配置,通过defaultProp
静态变量的方式来定义。当组件被调用的时候,默认值
保证渲染后始终有值。在render
方法中,可以直接使用props
的值来渲染。这里,我们只需要默认设置classPrefix
和onChange
即可。因为defaultActiveIndex
和activeIndex
,我们需要保持只去其中一个条件:
static defaultProps = {
classPrefix : 'tabs',
onchange:()=>{},
};
但是Tabs
组件的信息全由一个对象传进来的方式真的好吗?对于React
组件来说,我们考虑设计组件一定要满足一大原则-- 直观。把基本设置与数据一起定义成一个组件或对象是初学者很容易犯的错误,对于React
来说,如果组件是可以分解的,那么一定要将它进行分解,使用子组件的方式来进行处理。
我们仔细观察一下Tabs
组件在web
界面的特征,一般来说,主要分成两个区域:切换区域和内容区域。那么我们根据上面说的,定义两个区域:切换区域和内容区域。TabNav
组件对应切换区域,TabContent
组件对应内容区域。这两个区域组件都存放一个有序的数组,都可以进行进一步的拆分, 具体的两种组织方式如下:
- 在
Tabs
组件的内部把所有定义的子组件都显示的展示出来。这么做的好处在于非常的易于理解,可以自定义的能力强,但是在调用的过程就会显得笨重。React-Bootstrap
和Material UI
组件库中的Tabs
组件采用的就是这样的方式,我们进行调用的方式如下:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0}>
<TabNav>
<TabHead>Tab 1</TabHead>
<TabHead>Tab 2</TabHead>
<TabHead>Tab 3</TabHead>
</TabNav>
<TabContent>第一个Tab里面的内容</TabContent>
<TabContent>第二个Tab里面的内容</TabContent>
<TabContent>第三个Tab里面的内容</TabContent>
</Tabs>
- 在
Tabs
组件内置显示定义内容区域的子组件集合,头部区域对应内部区域的每一个TabPane
组件的props
,让其在TabNav
组件内拼装。这种方式的调用写法比较简单,把复杂的逻辑留给了组件去实现。Ant Design
组件库中的Tabs
组件采用的就是这种方式。调用方式如下形式:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0}>
<TabPane key ={0} tab = {'Tab 1'}>第一个Tab里面的内容</TabPane>
<TabPane key ={1} tab = {'Tab 2'}>第二个Tab里面的内容</TabPane>
<TabPane key ={2} tab = {'Tab 3'}>第三个Tab里面的内容</TabPane>
</Tabs>
我们通过后面的一种方法进行具体的讲述,当基本结构确定后,我们需要看一下怎么渲染这个结构的内容。显然,不能让所以的参数都由Tabs
组件来承载。只有两个props
放在了Tabs
组件上面,而其他的参数直接放在了TabPane
组件上面,由它的父组件TabContent
隐式对TabPane
组件的拼装。
子组件prop
在React中有一个重要且内置的prop-children
,它代表了组件的子组件结合。children
可以根据传入子组件的数量来决定是否是数组类型。我们上面调用TabPane
组件的过程,翻译过来就是:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0} className = "tabs-bar"
children ={[
<TabPane key ={0} tab = {'Tab 1'}>第一个Tab里面的内容</TabPane>
<TabPane key ={1} tab = {'Tab 2'}>第二个Tab里面的内容</TabPane>
<TabPane key ={2} tab = {'Tab 3'}>第三个Tab里面的内容</TabPane>
]}
>
</Tabs>
实现的基本思路就是以TabContent
组件渲染TabPane
子组件集合为例来讲,其中渲染TabPane
组件的方法如下:
getTabPanes(){
const {classPrefix, activeIndex, panels, isActive } = this.props;
return React.Children.map(panels, (child) =>{
if(!child){return;}
const order = parseInt(child.props.order, 10);
const isActive = activeIndex === order;
return React.cloneElement(child,{
classPrefix,
isActive,
children:child.props.children,
key:'tabPane - ${order}',
});
});
}
上面的代码讲述了子组件组合是怎么渲染的,通过React.Children.map
方法遍历子组件将order
(渲染顺序)、isActive
(是否激活tab)、children
(Tabs
组件中传下的children
)和key
利用React
的cloneElement
方法克隆到TabPane
组件中,最后返回这个TabPane
组件集合。这也是Tabs
组件拼装子组件的基本原理。
其中,React.children
是React
官方提供的一系列操作children
的方法。它提供诸如map
、forEach
、count
等实用函数,可以为我们提供子组件提供便利。
最后,TabContent
组件的render
方法只需要调用getTabPanes
方法就可以完成渲染:
render(){
return (<div>{this.getTabPanes()}</div>)
}
假如我们把render
方法中的this.getTabPanes
方法中对子组件的遍历直接放进去,就会变成如下的形式
render(){
return (<div>{React.Children.map(this.props.children, (child) => {...})}</div>);
}
这种调用方式称为Dynamic Children
(动态子组件)。它指的是组件内的子组件是通过动态计算得到的。就像上述对子组件的遍历一样,我们一样可以对任何数据、字符串、数组或对象作动态计算。
用声明式编程的方式来渲染数据,这样的做法和关心所有的细节的命令式编程相比,会让我们轻松很多,当然,除了数据的map
函数。还可以用其他使用的高阶函数,如reduce
、filter
等函数。值得注意的是,与map函数相似但不返回调用结果的forEach
函数不能这么使用。
组件props
<TabPane key ={0} tab = {'Tab 1'}>第一个Tab里面的内容</TabPane>
现在tab prop
中传入的是一个字符串。但是,如果我们传入的是节点呢,是不是就可以自定义tab
头展示的形式了,这就是component props
。对于子组件而言,我们不仅可以直接使用this.props.children
定义,也可以将子组件以props
的形式传递。一般我们会用这种方法来让开发者定义组件的某一个prop
,让其具备多种类型,来做到简单配置和自定义配置组合在一起的效果。
在Tabs
组件中,我们就用到了这样的功能,调用当时如下所示:
<Tabs classPrefix = {'tabs'} defaultActiveIndex ={0} className = "tabs-bar">
<TabPane
order ='0'
tab = {<span><i className = "fa fa-home"> Home</i></span>}>
第一个Tab里面的内容
</TabPane>
<TabPane
order ='1'
tab = {<span><i className = "fa fa-book"> Library</i></span>}>
第二个Tab里面的内容
</TabPane>
<TabPane
order ='3'
tab = {<span><i className = "fa fa-home"> Application</i></span>}>
第三个Tab里面的内容
</TabPane>
<Tabs>
我们也可以加入更多的自定义元素,可以是多行的,甚至可以插入动态数据。这听上去有些复杂,但是实现的过程其实非常简单,下面写在TabNav
组件中简化的渲染子组件集合的方法:
getTabs(){
const {classPrefix, activeIndex, panels} = this.props;
return React.Children.map(panels, (child) =>{
if(!child){return;}
const order = parseInt(child.props.order, 10);
let classes = classnames ({
[`${classPrefix} - tab `] : true,
[`${classPrefix} - active`] : activeIndex === order,
[`${classPrefix} - disable`] : child.props.disable,
});
return (
<li>{child.props.tab}</li>
);
});
}
上面的方法和getTabPanes
方法非常像,关键在于通过遍历TabPane
组件的tab prop
来实现我们想要的功能,不论tab是以字符串的形式还是以虚拟元素的形式存在,都可以直接在<li>
标签中渲染出来。
网友评论