目前,react组件有三种写法,分别是es5的createClass写法,es6的class写法,以及stateless(无状态组件)写法。
下面由浅入深来细说这三种写法。涵盖了生命周期,反向数据流,es6/7等知识。
一,原始的createClass写法
对于写react组件,很多人第一印象往往是createClass,这是因为createClass是react组件最原始的写法,基本每个学react的人都是接触这种写法过来的。
createClass写法是基于es5,它实际上是React对象的一个顶层API,它只接受一个配置对象作为参数,如下:
var React= require('react');var ReactDOM= require('react-dom');var AppComponent= React.createClass( {这是一个react组件配置对象} );
1,下面来说说组件配置对象。
首先,这个配置对象必须有一个render()方法,并且这个render()方法必须返回由闭合标签包裹的html片段。
render()方法的作用在于渲染DOM。
如下:
varAppComponent = React.createClass({render:function(){return(
返回值最外层必须是闭合标签
) }})
一个最为简单react组件就这样完成了。这个组件只配置了一个render函数,你还可以有更多的配置。
react组件的调用也很简单:
// 调用react组件
最终结果如下:
返回值最外层必须是闭合标签
2,添加组件生命周期方法
react组件本身除了render()方法以外,还有诸多生命周期方法。这些生命周期方法如下:
# 初始化阶段getDefaultProps()// 组件类创建的时候调用getInitialState()// 组件挂载之前调用, 定义state初始值componentWillMount()// 组件即将被装载、渲染到页面上componentDidMount()// 组件挂载后调用# 组件运行阶段componentWillReceiveProps()// 组件将要接收到新属性的时候调用shouldComponentUpdate()// 组件接受到新属性或者新状态的时候调用componentWillUpdate()// 新属性或state 更新前调用componentDidUpdate()// 更新完成后调用# 组件销毁阶段componentWillUnmount()// 组件销毁之前调用
那么,这些生命周期方法是不是必须的呢?
没有一个是必须的,也就是说是否使用,使用哪些生命周期方法,全凭你自身需求来决定。
但通常你会用到下面这几个:
getInitialState()// 定义你的初始statecomponentDidMount()// 在render()方法之后调用,也就是执行这个方法前DOM已经渲染完成,// 此时可以使用这个方法来载入数据,也就是发送ajax请求数据
那么,这些方法是如何应用的呢?
很简单,使用方法和render()方法一样,在组件配置对象中定义即可。组件配置对象实际上就是一个组件属性和方法的集合。
接着上面的例子:
varAppComponent = React.createClass({getDefaultProps:function(){// 一些逻辑},getInitialState:function(){// 一些逻辑},componentWillMount:function(){// 一些逻辑},componentDidMount:function(){// 一些逻辑},componentWillReceiveProps:function(){// 一些逻辑},shouldComponentUpdate:function(){// 一些逻辑}, …… render:function(){return(
返回值最外层必须是闭合标签
) }})
至此,完成了一个略完整的组件。
为什么说是略完整?因为还没有添加组件验证器,也就是propTypes。
3,添加组件验证器propTypes
propTypes是一个对象,主要用于验证传入组件的props属性是否符合你在propTypes对象中设定的类型 ,如果不符合,组件会报错。通常在开发环境中使用。
下面我们来给组件添加验证器,在组件内配置propTypes属性即可。以上面的例子为例。
varAppComponent = React.createClass({propTypes: {data: React.PropTypes.array,// 验证传入的data是不是数组,如果不是则会报错loadding: React.PropTypes.bool,// 验证loadding是否为trueloadData: React.PropTypes.func,// 验证loadData是否为函数类型},getDefaultProps:function(){// 一些逻辑},getInitialState:function(){// 一些逻辑}, …… render:function(){return(
返回值最外层必须是闭合标签
) }})
于是,组件验证器添加完成。
props属性是如何传入呢?如下:
所有传入的属性都可以在组件内通过this.props对象访问到。
4, 自定义方法
上面讲解了react组件自带的一些方法和属性。那么除了组件自带的,我们可不可以给组件自定义我们需要的方法呢?
答案必然是肯定的。光自带的方法是无法满足需求的,往往我们需要自定义一些方法来处理很多的逻辑。
自定义方法和组件自带方法一样,都是在组件配置对象内定义。以上面的粟子,再来举个粟子:
varAppComponent = React.createClass({propTypes: {data: React.PropTypes.array,// 验证传入的data是不是数组,如果不是则会报错loadding: React.PropTypes.bool,// 验证loadding是否为trueloadData: React.PropTypes.func,// 验证loadData是否为函数类型},getInitialState:function(){// 一些逻辑},componentDidMount:function(){// ajax请求},renderSubmit:function(e){// 自定义的方法,用于处理表单提交e. stopPropagation();// 一些表单提交处理逻辑} …… render:function(){return(
// 调用自定义方法
) }})
至此,一个完整的组件基本完成。
createClass方法的劣势
现阶段已不推荐使用createClass方法来创建组件,准确地说它已经过时了。
基于es5,终究是要被淘汰的
过于臃肿,组件性能略差
二,class组件写法,也就是使用es6的写法
1,创建class组件
class写法是目前比较推荐的写法,另一种最为推荐的写法是stateless组件,后面会讲到。
这里所说的class是es6的class特性。
react巧妙地运用了这一特性。因为它有一个非常棒的API,那就是extends(继承)。
我们先来回顾下如何使用class定义一个类,如下:
classAppComponent{ }
这样就定义了一个AppComponent类。
再来回顾下,如何继承另一个类,如下:
classAppComponentextendsAPP{ }
这样AppComponent类就继承了APP类,AppComponent就可以访问到APP类的属性和方法。
众所周知,react有一个顶层组件,也就是React.Component,它本身封装了诸多属性和方法,那么,我们可以使用class来继承它,从而使它的属性和方法能够共享给我们自定义的一个类,从而形成一个新的组件。如下:
importReactfrom'react';classAppComponentextendsReact.Component{// 定义一个继承于react顶层Component的新组件AppComponentconstructor(props){super(props)// super,调用父类构造函数改变this指向}//这是一个基于es6的react组件render(){return
返回值最外层必须是闭合标签
}}或者importReact,{Component} from'react';// 解构classAppComponentextendsComponent{// 定义一个继承于react顶层Component的新组件AppComponentconstructor(props){super(props)// super,调用父类构造函数改变this指向}//这是一个基于es6的react组件render(){return
返回值最外层必须是闭合标签
}}# 上面两种写法同理,只是第二种使用了es6的另一个特性,对象的解构,将Component从React对象中解构出来。
至此,一个基于es6的react组件诞生了。
2,class组件与createClass组件的比较
class组件的用法和createClass组件的用法基本一致,它同样有生命周期,有验证器,同样可以自定义方法等等。
createClass组件到class组件的变迁,实际上是es5到es6的变迁,基本上是语法上的变迁和功能上的拓展。组件的本质是没有改变的,只是写法稍有不同。
下面来说说这些不同点。
创建组件的方式不同
一个是使用es5封装的createClass方法来创建,一个是使用es6的class特性来创建。
上面已经详细阐述过,这点就不过多阐述了。
定义初始state的方式不同
createClass组件是使用getInitialState方法来定义初始state的,如下:
varAppComponent = React.createClass({ getInitialState:function(){return{ loadding:false, isshow:false, data:null…… } } })
class组件定义初始state则大不相同,可分为两种,如下:
# 第一种,构造函数内定义,这是es6的实现classAppComponentextendsReact.Component{ constructor(props){super(props);// 调用父类的构造函数,改变this指向this.state = { loadding:false, isshow:false, data:null, …… } } ……}# 第二种,直接定义静态属性,这是es7的实现,更为简单,实用classAppComponentextendsReact.Component{ state = { loadding:false, isshow:false, data:null…… } ……}# es6规定是只有静态方法,没有静态属性的。但是es7可以定义静态属性,可通过babel转换实现。# 要使用es7也很简单,通常安装和配置babel-preset-stage-0即可使用所有的es6/7新特性
react组件内es5和es6的不同写法
直接举个粟子说明,以class组件为例:
# es5写法classAppComponentextendsReact.Component{ componentDidMount: function(){ } renderSubmit: function(){ } …… render: function(){return
hello
}}# es6写法,有2种// 写法一classAppComponentextendsReact.Component{ …… componentDidMount(){ } renderSubmit(){ } …… render(){return
hello
}}/*
提示:
componentDidMount(){
}
这种写法相当于es5的:
componentDidMount: function(){
}
*/# 写法二,使用箭头函数,似乎高逼格一些classAppComponentextendsReact.Component{ …… componentDidMount=()=>{ } renderSubmit=()=>{ } …… render=()=>{return
hello
}}# 提示:class严格来讲不是一个对象,class内定义的属性和方法并不需要用逗号','隔开。
三, stateless组件写法
前言
所谓stateless组件,也就是无状态组件。
这种react组件有一个特点,它没有生命周期方法,没有render方法,连state也没有,this也没有,也不需要实例化。
为什么需要这样的组件?
很多时候,从业务上考虑,我们的某些组件只用于纯UI展示,并没有涉及到生命周期,也不需要setState,
但是react组件本身依然存在生命周期方法等一大堆组件本身的设定,然而这些设定我们根本不需要用到的,它们的存在造成了资源浪费,多余,和臃肿。
另外组件实例化是需要占用内存,消耗性能的。
因此,考虑到不同业务需求,后来react增加了stateless组件的支持。
stateless组件本质是一个函数,它没有生命周期,也不需要实例化,没有this指向, 更轻盈,性能更加好。
这种组件是所有react组件中性能最好的组件类型。官方也推荐多用这种组件。
stateless组件的定义
stateless组件本质是一个带有返回值的函数,而且必须是使用闭合标签包裹的返回值。
下面来举个粟子:
constAppComponent =(props) =>{// 一些逻辑return
这是一个干净纯洁的stateless组件
}
这样就定义了一个常见的stateless组件。发现没,这就是一个函数。
有没有觉得非常干净,整洁,高逼格了许多?
stateless组件的使用和前面两种是一样的,直接来个粟子:
同样,你还可以给它加验证器:
AppComponent.propTypes= { data: React.PropTypes.array,// 验证传入的data是不是数组,如果不符合则会报错loadding: React.PropTypes.bool,// 验证传入的loadding是否为trueloadData: React.PropTypes.func,// 验证传入的loadData是否为函数类型}
如何获取props属性?
首先来看看如何在调用组件时传入props属性:
上面组件调用时传入了data, loadding, loadData三个props属性。这些传入的属性最终都会被收集到组件的props对象里面。
需要注意的是,stateless组件的props是通过传参传进去的,因为它本身是一个函数,没有this
而获取props属性的方式也五花八门,下面直接看个粟子:
# 第一种,在内部展开获取constAppComponent= (props) =>{const{ data, loadding, loadData } = props; // 通过es6的对象解构赋值的写法,直接从props获取到 /* 上面等同于:constdata = props.data;constloadding = props.loadding;constloadData = props.loadData; */ ……return
这是一个干净纯洁的stateless组件
}# 第二种,高逼格一些,直接从参数里解构出来,如下:constAppComponent= ({ data, loadding, loadData }) =>{ ……return
这是一个干净纯洁的stateless组件
} 或者,如果参数较多,可以写得优雅一些:constAppComponent= ({ data, loadding, loadData }) =>{ ……return
这是一个干净纯洁的stateless组件
}
使用反向数据流来实现setState功能
stateless组件本身没有生命周期,也无法设置state,那是否意味着stateless组件不能使用setState功能呢?
答案是可以实现,不过是间接使用。实现的方式是——反向数据流。
有两个前提条件,一个是必须依赖一个父组件,二是这个父组件不是stateless组件,它有生命周期。
在实践之前,我们先来解决一个疑问,react明明是单向绑定,何来反向数据流?
的确react本身是单向绑定,是自上而下传递信息的,也就是从父组件传递给子组件,逐级向下传递。
如果要实现反向数据流,那么就意味着要实现自下而上传递信息,也就是要实现子组件向父组件传递信息。
那么,如何做到子组件向父组件传递信息?
下面是实现原理:
在父组件中定义一个方法,把这个方法传递给子组件,由子组件去触发这个方法,并且以函数传参的方式,把需要的信息传递给父组件。
这样就实现了自下而上传递的反向数据流。
那么,我们可以使用这个原理来实现stateless组件的setState功能。
下面来个例子:
# 父组件import ChildComponent from'./ChildComponent';// 引入子组件classAppComponentextendsReact.Component{ state = { msg:null, content:null// 初始状态} changeLoad(content){// 这个方法是传递给子组件调用的,并且子组件会传递content过来this.setState({ msg:'反向传递成功', content// { content } 等同于 { content: content }}) } render(){return
{this.state.content }
} }# 子组件const ChildComponent =({ msg, changeLoad })=>{ conststr="我是一段文字,子组件把我这段文字传给了它的父组件,并在父组件中展示我";return
{ msg?msg:null} 点我传递str给父组件 // 调用父组件的changeLoad方法并传递str给父组件
}
这个例子很简单。父组件定义了msg和content两个初始state, 并且定义了一个changeLoad函数,这个函数是传递给子组件,由子组件去触发的。
子组件触发的时候传递新的content给父组件, changeLoad函数接受到新的content就会通过setState功能去更新旧的content,以及msg提示。
此时父组件就会展示子组件传递过来的content,并传递msg通知子组件。msg就是被改变的props属性。
这样不仅实现了正向传递,也实现了反向传递。
四,不同组件的最佳应用方式
写react组件,推荐使用class组件和stateless组件。createClass组件尽量少用或者不用。
下面来说说这三种组件的应用场景。
createClass
已不推荐使用,这里不再多讲。但你仍需要了解它,因为你可能会接触到一些旧项目,或者一些旧的开源项目,这些项目大都采用createClass写法。
再加上es6受限于兼容性问题而尚未普及,所以你可能接触到较多的createClass组件,你有必要去了解它。
class组件和stateless组件互为替补
这两种组件通常要搭配使用,互为替补,这是react组件最好的应用方式。
在实践前,先来讲讲两个概念,分别是——容器组件,展示组件。
也就是Container Component和 Presentational Component
所谓Container Component(容器组件),简单来讲就是它通常是作为一个父组件,它底下领着一群子组件。容器组件(父组件)的作用在于,给子组件传递数据,并且定义逻辑方法传递给子组件。
子组件不参与或少参与业务逻辑处理,它主要负责接收和展示容器组件传递过来的数据,以及调用传递过来的逻辑方法。
而这里所说的子组件,通常就是展示型组件,也就是Presentational Component。
这里为什么说class组件和stateless组件搭配使用,互为替补,是react组件的最好应用方式呢?
首先,stateless组件没有生命周期,无实例化,性能最好。而展示组件通常只需要做数据展示,和逻辑方法调用,它并不需要使用到生命周期方法。
这不正和stateless组件最为契合吗?
于是,stateless组件,通常用作于展示组件。
再来说说class组件,它有生命周期,再搭配es6/7语法,它可以处理众多复杂的业务逻辑。
而容器组件通常只专注于处理业务逻辑,需要使用生命周期,对于数据的展示则交给展示组件。这正和class组件最为契合。于是,通常class组件作为容器组件。
这两种组件,各自分工,互为替补,是react组件最好的应用方式。
下面是一个例子:
# 容器组件// /containers/index.jsimport ListTableComponent from'/components/ListTable';import ItemTableComponent from'/components/ItemTable';classAppComponentextendsReact.Component{ state = { loadding:false}render =()=>{const{list, item } = this.props;consthanddleLoad = ()=>{// ToDo}constlistProps = {list, handdleLoad };constitemProps = { item, handdleLoad };return
}}# 展示组件一// /components/ListTable.jsconstListTable = ({list,handdleLoad })=>{return
…… {list.coutry} {list.address} …… 做一些事情
}# 展示组件二// /components/ItemTable.jsconstItemTable = ({ item,handdleLoad })=>{return
…… {item.name} {item.age} …… 做一些事情
}
这个例子中,容器组件index.js载入了两个子组件,这两个是展示组件,容器组件定义了handdleLoad方法,并从props拿到数据list和item两个数据源,再组织成listProps和itemProps两个props属性对象,并把他们分别传给ListTable和ItemTable两个展示组件。
这两个展示组件从props中拿到传过来的数据,并在render方法中展示出来,并不需要处理过多业务逻辑。
总结
react组件的演进,依赖于js语法的演进,随着es6/7的到来,react组件的写法变得多样,且更为高效便捷。
因此,多结合es6/7语法,多使用stateless组件,可使你的react应用更上一层楼。