从零学习React技术栈:组件类型

发布于 余博伦

这一节的内容会比较多,如果是刚刚入门的新同学一时半会儿可能会接受不了,而且基本都属于理论知识。如果你在阅读时发现理解有困难也不需要灰心,可以把本篇教程当作随时可供查阅的文档,等到你在实践中积累了一定的代码量之后再回过头来阅读文章就会感觉非常轻松啦。

  • 元素与组件 Element & Component
  • 函数定义与类定义组件 Functional & Class
  • 展示与容器组件 Presentational & Container
  • 有状态与无状态组件 Stateful & Stateless
  • 受控与非受控组件 Controlled & Uncontrolled
  • 组合与继承 Composition & Inheritance

元素与组件 Element & Component

元素

元素是构建React应用的最小单位。元素所描述的也就是你在浏览器中能够看到的东西。根据我们在上节课中讲到的内容,我们在编写React代码时一般用JSX来描述React元素。

在作用上,我们可以把React元素理解为DOM元素;但实际上,React元素只是JS当中普通的对象。React内部实现了一套叫做React DOM的东西,或者我们称之为Virtual DOM也就是虚拟DOM。通过一个树状结构的JS对象来模拟DOM树。

说到这里我们可以稍微讲一下,React为什么会有这一层虚拟DOM呢?在课程介绍中我们曾经提到过,React很快、很轻。它之所以快就是因为这一套虚拟DOM的存在,React内部还实现了一个低复杂度高效率的Diff算法,不同于以往框架,例如Angular使用的脏检查。在应用的数据改变之后,React会尽力少地比较,然后根据虚拟DOM只改变真实DOM中需要被改变的部分。React也藉此实现了它的高效率,高性能。

当然这不是虚拟DOM唯一的意义,通过这一层单独抽象的逻辑让React有了无限的可能,就比如React Native的实现,可以让你只掌握JS的知识也能在其他平台系统上开发应用,而不只是写网页,甚至是之后会出现的React VR或者React物联网等等别的实现。

话说回来,元素也就是React DOM之中描述UI界面的最小单位。刚才我们说到了,元素其实就是普通的JS对象。不过我们用JSX来描述React元素在理解上可能有些困难,事实上,我们也可以不使用JSX来描述:

const element = <h1>Hello, world</h1>;
// 用JSX描述就相当于是调用React的方法创建了一个对象
const element = React.createElement('h1', null, 'Hello, world');

组件

要注意到,在React当中元素和组件是两个不同的概念,之所以在前面讲了这么多,就是担心大家不小心会混淆这两个概念。首先我们需要明确的是,组件是构建在元素的基础之上的。

React官方对组件的定义呢,是指在UI界面中,可以被独立划分的、可复用的、独立的模块。其实就类似于JS当中对function函数的定义,它一般会接收一个名为props的输入,然后返回相应的React元素,再交给ReactDOM,最后渲染到屏幕上。

函数定义与类定义组件 Functional & Class

新版本的React里提供了两种定义组件的方法。当然之前的React.createClass也可以继续用,不过我们在这里先不纳入我们讨论的范围。

第一种函数定义组件,非常简单啦,我们只需要定义一个接收props传值,返回React元素的方法即可:

function Title(props) {
    return <h1>Hello, {props.name}</h1>
}

甚至使用ES6的箭头函数简写之后可以变成这样:

const Title = props => <h1>Hello, {props.name}</h1>

第二种是类定义组件,也就是使用ES6中新引入的类的概念来定义React组件:

class Title extends React.Component {
    render() {
        return <h1>Hello, {this.props.name}</h1>
    }
}

之后呢,根据我们在上一节课中了解到的,组件在定义好之后,可以通过JSX描述的方式被引用,组件之间也可以相互嵌套和组合。

展示与容器组件 Presentational & Container

接下来我们还会介绍一些更深入的关于组件概念,现在听起来可能会比较抽象枯燥,不过接下来要介绍的这几个概念在之后的课程中都是会被应用到的,同学们也可以根据自己的实际情况,在阅读完后续的课程之后,再返回来看,相信一定会对你理解React有所帮助。

首先是最重要的一组概念:展示组件与容器组件。同样,在课程介绍中我们提到的,React并不是传统的MVVM框架,它只是在V层,视图层上下功夫。同学们应该对MVVM或MVC都有所了解,那么既然我们的框架现在只有V层的话,在实际开发中应该如何处理数据与视图的关系呢?

为了解决React只有V层的这个问题,更好地区分我们的代码逻辑,展示组件与容器组件这一对概念就被引入了。这同样也是我们在开发React应用时的最佳实践。

我们还是先看一个具体的例子来解释这两个概念:

class CommentList extends React.Component {
    constructor(props) {
        super(props)
        this.state = { comments: [] }
    }

    componentDidMount() {
        $.ajax({
            url: "/my-comments.json",
            dataType: 'json',
            success: function(comments) {
                this.setState({comments: comments})
            }.bind(this)
        })
    }

    renderComment({body, author}) {
        return <li>{body}—{author}</li>
    }
    render() {
        return <ul> {this.state.comments.map(this.renderComment)} </ul>
    }
}

这是一个回复列表组件,乍看上去很正常也很合理。但实际上在开发React应用时,我们应该避免写出这样的组件,因为这类组件担负的功能太多了。它只是一个单一的组件,但需要同时负责初始化state,通过ajax获取服务器数据,渲染列表内容,在实际应用中,可能还会有更多的功能依赖。这样,在后续维护的时候,不管是我们要修改服务器数据交互还是列表样式内容,都需要去修改同一个组件,逻辑严重耦合,多个功能在同一个组件中维护也不利于团队协作。

通过应用展示组件与容器组件的概念,我们可以把上述的单一组件重构为一个展示回复列表组件和回复列表容器:

// 展示组件
class CommentList extends React.Component {
    constructor(props) {
        super(props)
    }
    renderComment({body, author}) {
        return <li>{body}—{author}</li>
    }
    
    render() { 
        return <ul> {this.props.comments.map(this.renderComment)} </ul>
    } 
}

// 容器组件
class CommentListContainer extends React.Component {
    constructor() {
        super()
        this.state = { comments: [] }
    }
    
    componentDidMount() {
        $.ajax({
        url: "/my-comments.json",
        dataType: 'json',
        success: function(comments) {
            this.setState({comments: comments})
        }.bind(this)
        })
    }
    
    render() {
        return <CommentList comments={this.state.comments} />
    }
}

像这样回复列表如何展示与如何获取回复数据的逻辑就被分离到两个组件当中了。我们再来明确一下展示组件和容器组件的概念:

展示组件

  • 主要负责组件内容如何展示
  • props接收父组件传递来的数据
  • 大多数情况可以通过函数定义组件声明

容器组件

  • 主要关注组件数据如何交互
  • 拥有自身的state,从服务器获取数据,或与Redux等其他数据处理模块协作
  • 需要通过类定义组件声明,并包含生命周期函数和其他附加方法

那么这样写具体有什么好处呢?

  • 解耦了界面和数据的逻辑
  • 更好的可复用性,比如同一个回复列表展示组件可以套用不同数据源的容器组件
  • 利于团队协作,一个人负责界面结构,一个人负责数据交互

有状态与无状态组件 Stateful & Stateless

有状态组件

意思是这个组件能够获取储存改变应用或组件本身的状态数据,在React当中也就是state,一些比较明显的特征是我们可以在这样的组件当中看到对this.state的初始化,或this.setState方法的调用等等。

无状态组件

这样的组件一般只接收来自其他组件的数据。一般这样的组件中只能看到对this.props的调用,通常可以用函数定义组件的方式声明。它本身不会掌握应用的状态数据,即使触发事件,也是通过事件处理函数传递到其他有状态组件当中再对state进行操作。

我们还是来看具体的例子比较能清楚地说明问题,与此同时,我们已经介绍了三组概念,为了防止混淆,我这里特意使用了两个展示组件来做示例,其中一个是有状态组件,另一个是无状态组件,也是为了证明,并不是所有的展示组件都是无状态组件,所有的容器组件都是有状态组件。再次强调一下,这是两组不同的概念,以及对组件不同角度的划分方式。

// 有状态组件
class StatefulLink extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            active: false
        }
    }
    handleClick() {
        this.setState({
            active: !this.state.active
        })
    }
    render() {
        return <a 
            style={{ color: this.state.active ? 'red' : 'black' }}
            onClick={this.handleClick.bind(this)}
            >
            Stateful Link
            </a>
    }
}

// 无状态组件
class StatelessLink extends React.Component {
    constructor(props) {
        super(props)
    }
    handleClick() {
        this.props.handleClick(this.props.router)
    }
    render() {
        const active = this.props.activeRouter === this.props.router
        return (
            <li>
                <a 
                style={{ color: active ? 'red' : 'black' }}
                onClick={this.handleClick.bind(this)}
                >
                    Stateless Link
                </a>
            </li>
        )
    }
}
class Nav extends React.Component {
    constructor() {
        super()
        this.state={activeRouter: 'home'}
    }
    handleSwitch(router) {
        this.setState({activeRouter: router})
    }
    render() {
        return (
            <ul>
                <StatelessLink activeRouter={this.state.activeRouter} router='home' handleClick={this.handleSwitch.bind(this)} />
                <StatelessLink activeRouter={this.state.activeRouter} router='blog' handleClick={this.handleSwitch.bind(this)} />
                <StatelessLink activeRouter={this.state.activeRouter} router='about' handleClick={this.handleSwitch.bind(this)} />
            </ul>
        )
    }
}

上述的例子可能稍有些复杂,事实上,在React的实际开发当中,我们编写的组件大部分都是无状态组件。毕竟React的主要作用是编写用户界面。再加上ES6的新特性,绝大多数的无状态组件都可以通过箭头函数简写成类似下面这样:

// function SimpleButton(props) {
//    return <button>{props.text}</button>
// }
const SimpleButton = props => <button>{props.text}</button>

受控与非受控组件 Controlled & Uncontrolled

受控组件

一般涉及到表单元素时我们才会使用这种分类方法,在后面一节课程表单及事件处理中我们还会再次谈论到这个话题。受控组件的值由propsstate传入,用户在元素上交互或输入内容会引起应用state的改变。在state改变之后重新渲染组件,我们才能在页面中看到元素中值的变化,假如组件没有绑定事件处理函数改变state,用户的输入是不会起到任何效果的,这也就是**“受控”**的含义所在。

非受控组件

类似于传统的DOM表单控件,用户输入不会直接引起应用state的变化,我们也不会直接为非受控组件传入值。想要获取非受控组件,我们需要使用一个特殊的ref属性,同样也可以使用defaultValue属性来为其指定一次性的默认值。

我们还是来看具体的例子:

class ControlledInput extends React.Component {
    constructor() {
        super()
        this.state = {value: 'Please type here...'}
    }
    handleChange(event) {
        console.log('Controlled change:',event.target.value)
        this.setState({value: event.target.value})
    }
    render() {
        return (
            <label>
                Controlled Component:
                <input type="text"
                    value={this.state.value}
                    onChange={(e) => this.handleChange(e)}
                />
            </label>
        )
    }
}
class UncontrolledInput extends React.Component {
    constructor() {
        super()
    }
    handleChange() {
        console.log('Uncontrolled change:',this.input.value)
    }
    render() {
        return (
            <label>
                Uncontrolled Component:
                <input type="text"
                        defaultValue='Please type here...'
                        ref={(input) => this.input = input}
                        onChange={() =>this.handleChange()}
                />
            </label>
        )
    }
}

通常情况下,React当中所有的表单控件都需要是受控组件。但正如我们对受控组件的定义,想让受控组件正常工作,每一个受控组件我们都需要为其编写事件处理函数,有的时候确实会很烦人,比方说一个注册表单你需要写出所有验证姓名电话邮箱验证码的逻辑,当然也有一些小技巧可以让同一个事件处理函数应用在多个表单组件上,但生产开发中并没有多大实际意义。更有可能我们是在对已有的项目进行重构,除了React之外还有一些别的库需要和表单交互,这时候使用非受控组件可能会更方便一些。

组合与继承 Composition & Inheritance

前面我们已经提到了,React当中的组件是通过嵌套或组合的方式实现组件代码复用的。通过props传值和组合使用组件几乎可以满足所有场景下的需求。这样也更符合组件化的理念,就好像使用互相嵌套的DOM元素一样使用React的组件,并不需要引入继承的概念。

当然也不是说我们的代码不能这么写,来看下面这个例子:

// Inheritance
class InheritedButton extends React.Component {
    constructor() {
        super()
        this.state = {
            color: 'red'
        }
    }
    render() {
        return (
            <button style={{backgroundColor: this.state.color}}>Inherited Button</button>
        )
    }
}
class BlueButton extends InheritedButton {
    constructor() {
        super()
        this.state = {
            color: '#0078e7'
        }
    }
}

// Composition
const CompositedButton = props => <button style={{backgroundColor:props.color}}>Composited Button</button>
const YellowButton = () => <CompositedButton color='#ffeb3b' />

但继承的写法并不符合React的理念。在React当中props其实是非常强大的,props几乎可以传入任何东西,变量、函数、甚至是组件本身:

function SplitPane(props) {
    return (
        <div className="SplitPane">
            <div className="SplitPane-left">
                {props.left}
            </div>
            <div className="SplitPane-right">
                {props.right}
            </div>
        </div>
    )
}
function App() {
    return (
        <SplitPane
            left={
                <Contacts />
            }
            right={
                <Chat />
        } />
    )
}

React官方也希望我们通过组合的方式来使用组件,如果你想实现一些非界面类型函数的复用,可以单独写在其他的模块当中在引入组件进行使用。

余博伦

知乎专栏【从零学习前端开发】作者,FreeCodeCamp中国发起人,React中文社区布道师。致力于为前端初学者提供良心的干货、暖心的教程、真心的分享。微信公众号 yunote。

如需转载,烦请注明出处:https://www.fedev.cn/react/react-learn-2-2.htmlNike Roshe Two Release Date