React中的无状态和有状态组件
组件的概念在Web中应用的场景已经相当广泛了。而React是专注于View层的,组件也是React核心理念之一,一个完整的应用将由一个个独立的组件拼装而成。组件也是React最基础的一部分,欲想征服React,那么了解和编写组件就显得尤为重要。
在上一篇文章,咱们就写了一个最简单的React组件,而且在文章末尾,咱们留了一个问题,**怎么创建无状态和有状态的React组件?**接下来,就一起来了解React中的无状态和有状态的组件。
React中创建组件的方式
在了解React中的无状态和有状态的组件之前,先来了解在React中创建组件的姿势。简单的说,在React中创建组件有三种方式:
- ES5写法:
React.createClass
- ES6写法:
React.Component
- 无状态的函数写法,又称为纯组件SFC
React.createClass
React.createClass
是React刚开始推荐的创建组件的方式。这是ES5的原生的JavaScript来实现的React组件。React.createClass
这个方法构建一个组件**“类”**,它接受一个对象为参数,对象中必须声明一个render()
方法,render()
方法将返回一个组件实例。
先来看一个React.createClass
创建组件的形式:
import React from 'react'
import ReactDOM from 'react-dom'
const SwitchButton = React.createClass({
getDefaultProp:function() {
return { open: false }
},
getInitialState: function() {
return { open: this.props.open };
},
handleClick: function(event) {
this.setState({ open: !this.state.open });
},
render: function() {
var open = this.state.open,
className = open ? 'switch-button open' : 'btn-switch';
return (
<label className={className} onClick={this.handleClick.bind(this)}>
<input type="checkbox" checked={open}/>男
</label>
);
}
});
ReactDOM.render(
<SwitchButton />,
document.getElementById('root')
);
React.createClass
是用来创建有状态的组件,这些组件是要被实例化的,并且可以访问组件的生命周期方法。不过React.createClass
创建React组件有其自身的问题存在:
React.createClass
会自动绑定函数方法,导致不必要的性能开销,增加代发过时的可能性React.createClass
的mixins
不够自然、直观
React.Component
React.Component
是以ES6的形式来创建React组件,也是现在React官方推荐的创建组件的方式,其和React.createClass
创建的组件一样,也是创建有状态的组件。而且React.Component
最终会取代React.createClass
。
把上面的例子,用React.Component
来修改:
import React from 'react'
import ReactDOM from 'react-dom'
class SwitchButton extends React.Component {
constructor(props) {
super(props)
this.state = {
open: this.props.open
}
this.handleClick = this.handleClick.bind(this)
}
handleClick(event) {
this.setState({ open: !this.state.open })
}
render() {
let open = this.state.open,
className = open ? 'switch-button open' : 'btn-switch'
return (
<label className={className} onClick={this.handleClick}>
<input type="checkbox" checked={open}/> 男
</label>
)
}
}
SwitchButton.defaultProps = {
open: false
}
ReactDOM.render(
<SwitchButton />,
document.getElementById('root')
)
React.Component
和React.createClass
创建组件有蛮多不同之处,有关于这两者的区别,@toddmotto去年就写过一篇《React.createClass
versus extends React.Component
》,文章对两者之间做过详细的阐述。
无状态的函数写法
无状态的函数创建的组件是无状态组件,它是一种只负责展示的纯组件:
function HelloComponent(props) {
return <div>Hello {props.name}</div>
}
ReactDOM.render(<HelloComponent name="marlon" />, mountNode)
对于这种无状态的组件,使用函数式的方式声明,会使得代码的可读性更好,并能大大减少代码量,箭头函数则是函数式写法的最佳搭档:
const Todo = (props) => (
<li
onClick={props.onClick}
style={{textDecoration: props.complete ? "line-through" : "none"}}
>
{props.text}
</li>
)
上面定义的 Todo
组件,输入输出数据完全由props
决定,而且不会产生任何副作用。对于props
为 Object
类型时,我们还可以使用 ES6 的解构赋值:
const Todo = ({ onClick, complete, text, ...props }) => (
<li
onClick={onClick}
style={{textDecoration: complete ? "line-through" : "none"}}
{...props}
>
{props.text}
</li>
)
无状态组件一般会搭配高阶组件(简称:HOC)一起使用,高阶组件用来托管state
,Redux 框架就是通过 store
管理数据源和所有状态,其中所有负责展示的组件都使用无状态函数式的写法。
这种模式被鼓励在大型项目中尽可能以简单的写法 来分割原本庞大的组件,而未来 React 也会面向这种无状态的组件进行一些专门的优化,比如避免无意义的检查或内存分配。所以建议大家尽可能在项目中使用无状态组件。
无状态组件内部其实是可以使用ref
功能的,虽然不能通过this.refs
访问到,但是可以通过将ref
内容保存到无状态组件内部的一个本地变量中获取到。
例如下面这段代码可以使用ref
来获取组件挂载到DOM中后所指向的DOM元素:
function TestComp(props){
let ref;
return (
<div ref={(node) => ref = node}></div>
)
}
如何选择创建组件的方式
Facebook 官方早就声明 ES6 React.Component
将取代React.createClass
。随着 React 不断发展,React.createClass
暴露出一些问题:
- 相比
React.Component
可以有选择性的绑定需要的函数,React.createClass
会自动绑定函数,这样会导致不必要的性能开销。 React.createClass
亲生的 mixin,React.Component
不再支持,事实上 mixin 不够优雅直观,替代方案是使用更流行的高阶组件-HOC,如果你的项目还离不开 也可以使用 react-mixin
总的来说:无状态函数式写法 优于React.createClass
,而React.Component
优于React.createClass
。能用React.Component
创建的组件的就尽量不用React.createClass
形式创建组件。
如何选择创建组件的方式,可以阅读@James K Nelson写的《Should I use React.createClass, ES6 Classes or stateless functional components?》一文。
React.createClass
对决 React.Component
特别声明,这一切的内容来自于@toddmotto去年分享《
React.createClass
versus extendsReact.Component
》一文。
语法区别
// React.createClass
// 新创建的 class 赋给一个常量,并添上 render 函数以完成最基本的组件定义
import React from 'react';
const Contacts = React.createClass({
render() {
return (
<div></div>
);
}
});
export default Contacts;
// React.Component
// 采用ES6
import React from 'react';
class Contacts extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div></div>
);
}
}
export default Contacts;
从 JavaScript 语言层面来看,我们已经在使用 ES6 中的类了,通常这些 ES6 代码需要使用类似 Babel 的工具转换为 ES5 代码之后才能在浏览器中正常执行。这里我们引入了一个叫 constructor
的东西,因为我们需要在这里调用 super()
函数来为 React.Component
传递属性。
在这次代码转换中,我们通过继承 React.Component
代替直接调用 React.createClass
的方式,创建了一个叫做Contacts
的类,使得这段代码中 JavaScript 的味道变得更浓郁了。在整个语法转换的过程中,这一步具有革命性的意义。
propType 和 getDefaultProps
这是个关乎如何使用、声明默认属性和类型,以及如何设置给类初始化状态的重要变化。
// React.createClass
import React from 'react';
const Contacts = React.createClass({
propTypes: {
},
getDefaultProps() {
return {
};
},
render() {
return (
<div></div>
);
}
});
export default Contacts;
// React.Component
import React from 'react';
class Contacts extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div></div>
);
}
}
Contacts.propTypes = {
};
Contacts.defaultProps = {
};
export default Contacts;
在调用 React.createClass
时,我们添加了一个叫做 propTypes
的对象,只要给它的属性进行赋值就能声明对应属性的类型。 getDefaultProps
这个函数返回了一个对象,这个对象的所有属性将会作为组件的初始化属性。
转换语法之后,我们通过给 Contacts
类添加一个名为 propTypes
属性的方式来达到和上面同样的效果。我认为这种方式比之前更加干净简洁了。而 getDefaultProps
函数也变成了一个名为 defaultProps
的属性,注意它仅仅是一个对象而不是get
函数。我更喜欢这种语法,因为它跳出了 React 的语法规则,变成了原生 JavaScript。
State 的区别
// React.createClass
import React from 'react';
const Contacts = React.createClass({
getInitialState () {
return {
};
},
render() {
return (
<div></div>
);
}
});
export default Contacts;
// React.Component
import React from 'react';
class Contacts extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
return (
<div></div>
);
}
}
export default Contacts;
React.createClass
创建了一个叫做 getInitialState
的函数,它只做一件事,那就是返回一个包含初始化状态的对象。
使用React.Component
后,getInitialState
函数被抛弃了,我们在 constructor
中像创建初始化属性一样声明了所有状态,我认为这样更加像 JavaScript 并且更少地驱动了“API”。
也就是说,React.createClass
创建的组件,其状态state
是通过getInitialState
方法来配置组件相关的状态;React.Component
创建的组件,其状态state
是在constructor
中像初始化组件属性一样声明的。
this
的区别
使用 React.createClass
时 React 会自动帮我们处理函数中的 this
指针,但使用 ES6 的话 this
将会失效。
React.createClass
注意,我们在
onClick
属性上绑定了this.handleClick
。当点击事件被触发时,React 会切换到正确的上下文中去执行handleClick
。
import React from 'react';
const Contacts = React.createClass({
handleClick() {
console.log(this); // React Component instance
},
render() {
return (
<div onClick={this.handleClick}></div>
);
}
});
export default Contacts;
React.Component
由于使用了 ES6,这里会有些微不同,属性并不会自动绑定到 React 类的实例上。
import React from 'react';
class Contacts extends React.Component {
constructor(props) {
super(props);
}
handleClick() {
console.log(this); // null
}
render() {
return (
<div onClick={this.handleClick}></div>
);
}
}
我们可以像下面这样在行内代码中绑定正确的执行上下文:
import React from 'react';
class Contacts extends React.Component {
constructor(props) {
super(props);
}
handleClick() {
console.log(this); // React Component instance
}
render() {
return (
<div onClick={this.handleClick.bind(this)}></div>
);
}
}
export default Contacts;
除此之外,我们也可以在 constructor
中来改变 this.handleClick
执行的上下文,相对于上一种来说这显然是更加优雅的解决办法,万一将来我们需要改变语法结构,这种方式完全不需要去改动 JSX 的部分:
import React from 'react';
class Contacts extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this); // React Component instance
}
render() {
return (
<div onClick={this.handleClick}></div>
);
}
}
export default Contacts;
Mixins
Mixins(混入)是面向对象编程OOP的一种实现,其作用是为了复用共有的代码,将共有的代码通过抽取为一个对象,然后通过Mixins进该对象来达到代码复用。如果我们使用 ES6 的方式来创建组件,那么 React mixins 的特性将不能被使用了。
React.createClass
使用 React.createClass
的话,我们可以在创建组件时添加一个叫做 mixins
的属性,并将可供混合的类的集合以数组的形式赋给 mixins
。
import React from 'react';
var SomeMixin = {
doSomething() {
}
};
const Contacts = React.createClass({
mixins: [SomeMixin],
handleClick() {
this.doSomething(); // use mixin
},
render() {
return (
<div onClick={this.handleClick}></div>
);
}
});
export default Contacts;
React.Component
但在 ES6 中,mixins
特性不被支持。但是React开发者社区提供一个全新的方式来取代Mixins,那就是Higher-Order Components(高阶组件)。那么什么是高阶组件呢?其实它和高阶函数的概念类似,就是一个会返回组件的组件。或者更确切地说,它其实是一个会返回组件的函数。就像这样:
const HigherOrderComponent = (WrappedComponent) => {
return class WrapperComponent extends Component {
render() {
//do something with WrappedComponent
}
}
}
做为一个高阶组件,可以在原有组件的基础上,对其增加新的功能和行为。我们一般希望编写的组件尽量纯净或者说其中的业务逻辑尽量单一。但是如果各种组件间又需要增加新功能,如打印日志,获取数据和校验数据等和展示无关的逻辑的时候,这些公共的代码就会被重复写很多遍。因此,我们可以抽象出一个高阶组件,用以给基础的组件增加这些功能,类似于插件的效果。具体细节可以参考这篇文章。
无状态组件 vs 有状态组件
**无状态组件:**无状态组件(Stateless Component)是最基础的组件形式,由于没有状态的影响所以就是纯静态展示的作用。一般来说,各种UI库里也是最开始会开发的组件类别。如按钮、标签、输入框等。它的基本组成结构就是属性(props
)加上一个渲染函数(render
)。由于不涉及到状态的更新,所以这种组件的复用性也最强。
**有状态组件:**在无状态组件的基础上,如果组件内部包含状态(state
)且状态随着事件或者外部的消息而发生改变的时候,这就构成了有状态组件(Stateful Component)。有状态组件通常会带有生命周期(lifecycle),用以在不同的时刻触发状态的更新。这种组件也是通常在写业务逻辑中最经常使用到的,根据不同的业务场景组件的状态数量以及生命周期机制也不尽相同。
而在React中,我们通常通过props
和state
来处理两种类型的数据。props
是只读的,只能由父组件设置。state
在组件内定义,在组件的生命周期中可以更改。基本上,无状态组件(也称为哑组件)使用props
来存储数据,而有状态组件(也称为智能组件)使用state
来存储数据。为了能更好的理解,我们通过下面的示例来展示。
回到《写第一个React组件》一文中,也就是文章中咱们使用create-react-app
创建的example-app
项目中。在src
目录中创建一个文件夹,并且将命名为messages
。然后进入messages
目录中创建一个message-view.js
文件。并且输入下面这段代码,创建一个无状态组件:
import React, { Component } from 'react';
class MessageView extends Component {
render() {
return(
<div className="container">
<div className="from">
<span className="label">From: </span>
<span className="value">John Doe</span>
</div>
<div className="status">
<span className="label">Status: </span>
<span className="value"> Unread</span>
</div>
<div className="message">
<span className="label">Message: </span>
<span className="value">Have a great day!</span>
</div>
</div>
)
}
}
export default MessageView;
为了让组件好看一点,在src/App.css
文件中添加样式代码:
.container {
margin-left: 40px;
}
.label {
font-weight: bold;
font-size: 1.2rem;
}
.value {
color: #474747;
position: absolute;
left: 200px;
}
.message .value {
font-style: italic;
}
最后在src/App.js
引入刚创建的组件:
import React, { Component } from 'react';
import './App.css';
import MessageView from './messages/message-view';
class App extends Component {
render(){
return (
<MessageView />
)
}
}
export default App;
别忘了修改src/index.js
下的代码:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
完成代码之后。在命令终端执行npm start
,并在浏览器打开:http://localhost:3000/
。这个时候,在浏览器中你能看到如下的效果:
对于无状态组件而言,没有必要使用面向对象的语法,尤其是在没有定义生命周期函数的情况下。根据这样的说法,我们重新修改MessageView
组件:
import React from 'react';
import PropTypes from 'prop-types';
export default function MessageView({message}) {
return(
<div className="container">
<div className="from">
<span className="label">From: </span>
<span className="value">{message.from}</span>
</div>
<div className="status">
<span className="label">Status: </span>
<span className="value"> {message.status}</span>
</div>
<div className="message">
<span className="label">Message: </span>
<span className="value">{message.content}</span>
</div>
</div>
)
}
MessageView.PropTypes = {
message: PropTypes.object.isRequired
}
注意,代码中已经不再导入Component
,因为在函数中这个已经不是必须的了。这种风格可能刚开始会让你感到困惑,但很快你就会发现这种方式编写React组件更快。
修改完MessageView
组件之后,记得在src/App.js
中声明一个message
对象,不然会报错:
在src/App.js
中添加下面代码:
import React, { Component } from 'react';
import './App.css';
import MessageView from './messages/message-view';
class App extends Component {
render(){
const message = {
from: '江西',
status: '工作中',
content: '想请个假去旅行'
}
return (
<MessageView message={ message } />
)
}
}
export default App;
这个时候看到的效果如下:
到这一步,咱们就成功的创建了一个无状态的React组件。但这并不是我们所需要的,因为我们需要做更多的事情才能和有状态的组件或容器适合的集成。目前,MessageView
只显示静态数据。我们需要对其进行改造,以便它能够接受输入的参数。这个时候就需要使用this.props
。使用这个将会给变量message
分配一个props
。这个时候也需要引入prop-types
,用来标记消息变量。也是为了使用我们的项目在增长时能更容易调试。
我们来更新message-view.js
文件的代码:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
class MessageView extends Component {
render() {
const message = this.props.message
return(
<div className="container">
<div className="from">
<span className="label">From: </span>
<span className="value">{message.from}</span>
</div>
<div className="status">
<span className="label">Status: </span>
<span className="value"> {message.status}</span>
</div>
<div className="message">
<span className="label">Message: </span>
<span className="value">{message.content}</span>
</div>
</div>
)
}
}
MessageView.propTypes = {
message: PropTypes.object.isRequired
}
export default MessageView
接下来,还需要创建一个有状态组件,作为MessageView
组件的父组件。我们将使用state
存储一个message
,并且传给MessageView
。这样一来,需要在src/messages
下创建message-list.js
文件,并将下面的代码复制到这个文件当中:
import React, { Component } from 'react'
import MessageView from './message-view'
class MessageList extends Component {
state = {
message: {
from: 'Martha',
status: 'read',
content: 'I will be traveling soon'
}
}
render() {
return(
<div>
<h1>List of Messages</h1>
<MessageView message={this.state.message} />
</div>
)
}
}
export default MessageList
接下来更新src/App.js
文件,把MessageList
替代当初的MessageView
:
import React, { Component } from 'react';
import './App.css';
import MessageList from './messages/message-list';
class App extends Component {
render(){
return (
<MessageList />
)
}
}
export default App;
保存文件,浏览器看到的效果如下:
从最终的效果来看,并无太大差异,也是仅显示了一条信息。如果我们想让MessageView
实例显示多条消息。怎么破?首先,我们将改变state.messages
,使用一个messages
数组来存储信息列表。然后通过map
函数来生成每个对应state.messages
,从而产生每个对应的MessageView
实例。我们还需要给state.messages
数组填充一个名为key
的特殊属性,它有一个独特的值,比如index
。为了跟踪列表中的哪个项目被更改、添加或删除,我们需要更新MessageList
组件:
import React, { Component } from 'react'
import MessageView from './message-view'
class MessageList extends Component {
state = {
messages: [
{
from: 'John',
content: 'The event will start next week',
status: 'unread'
},
{
from: 'Martha',
content: 'I will be traveling soon',
status: 'read'
},
{
from: 'Jacob',
content: 'Talk later. Have a great day!',
status: 'read'
}
]
}
render() {
const messageViews = this.state.messages.map(function(message, index){
return(
<MessageView key={ index } message={ message } />
)
})
return(
<div>
<h1>List of Messages</h1>
{ messageViews }
</div>
)
}
}
export default MessageList
这个时候,在你的浏览器中看到的效果如下:
这样我们创建了一个有状态的React的组件。是不是觉得很有意思。不过很多时候感觉还是晕晕的。还需要继续深入了解,希望随着后面的学习能慢慢的更清楚其中的奥秘和之关的关系。
特别声明,上面的示例来自于@Michael Wanyoike写的《Getting Started with React: A Beginner’s Guide》教程中。
总结
首先介绍了在React中创建组件的三种姿势以及它们之间的对比。总的来说:无状态函数式写法 优于React.createClass
,而React.Component
优于React.createClass
。能用React.Component
创建的组件的就尽量不用React.createClass
形式创建组件。
另外深入对比了React.createClass
和React.Component
两者之间的差异。
最后通过一个实例,展示了React中的无状态和有状态组件的创建方式。
篇幅略长,也很零乱。如果文中有不对之处,还请大婶指正。
参考资料
- Getting Started with React: A Beginner’s Guide
- React.createClass versus extends React.Component
- Stateful vs Stateless Components
- React: ES5 (createClass) or ES6 (class)?
- React创建组件的三种方式及其区别
- 总结 React 组件的三种写法 及最佳实践
- 更优雅的编写 React 组件
- 九个你忽略的React 无状态组件的优势
如需转载,烦请注明出处:https://www.fedev.cn/react/stateful-vs-stateless-components.htmlnike air max 2019 china