React中的无状态和有状态组件

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

组件的概念在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.createClassmixins不够自然、直观

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.ComponentReact.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决定,而且不会产生任何副作用。对于propsObject 类型时,我们还可以使用 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 extends React.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中,我们通常通过propsstate来处理两种类型的数据。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.createClassReact.Component两者之间的差异。

最后通过一个实例,展示了React中的无状态和有状态组件的创建方式。

篇幅略长,也很零乱。如果文中有不对之处,还请大婶指正。

参考资料

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/react/stateful-vs-stateless-components.html

返回顶部