组件声明

返回目录

React 组件声明与作用域绑定

目前组件中关于布局与数据绑定主要是基于 JSX 语法进行编写,很类似于 HTML 标签的布局过程, React 的核心魅力即在于其灵活的组件,其组件与其他传统的譬如 Angular 1 这样的框架相比,具有以下特点:

  • Compositional Components

    组件的可随意组合性是其灵魂特性,笔者也有专门的章节来介绍组件的组合策略与最佳实践。通过组件的组合,你可以以较好地方式进行代码复用与分发。

  • Pure Components React 中提倡的函数式组件不会有任何的副作用,并且下文中提及的 Dumb Component 与 Smart Component 的分隔与 HOC 模式保证了组件的可测试性。

  • Basic LifeCycle React 为其组件提供了一个基本的生命周期,这保证了我们对于组件更好地控制性,并且变相地也为我们提供了命名空间等分隔。

React 提供了和以往不一样的方式来看待视图,它以组件开发为基础。组件是 React 的核心概念,React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。譬如早期的 React.createClass 方法或者继承自 React.Component 的 ES6 Class 就用于生成一个组件类。对 React 应用而言,你需要分割你的页面,使其成为一个个的组件。也就是说,你的应用是由这些组件组合而成的。你可以通过分割组件的方式去开发复杂的页面或某个功能区块,并且组件是可以被复用的。这个过程大概类似于用乐高积木去瓶装不同的物体。我们称这种编程方式称为组件驱动开发。本部分我们会详细讨论基于 JSX 的 React 组件的声明方式。当然,在部分情况下你也可以选择不使用 JSX 来声明组件,我们在 JSX 章节中提到很多次 JSX 实际上会被解析为对于createElement函数的调用,因此我们也可以直接以如下方式声明组件:

class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}

不过这种方式会增加整个代码的复杂度,也失去了 React 简单灵活的灵魂,因此不是很提倡。

ES6 Class

笔者推荐是全部使用 ES6 的语法进行组件的声明,其基本样式如下所示:

import React from 'react';
/**
* Rendering <HelloMessage text='Hello Sarah' /> results in this HTML:
* <div>Hello Sarah</div>
*/
class HelloMessage extends Component {
render() {
return <div>{ this.props.text }</div>
}
}

Component、Element 与 Instance

函数式组件

React 0.14 版本引入了所谓的无状态函数式组件的概念(Stateless Functional Components),允许开发者以更加简单的,纯粹的 JavaScript 函数的方式来定义组件。实际上无状态特性是函数式的自有特性,函数式组件因为其声明方式注定就为无状态组件(状态组件与无状态组件的对比详见下文)。我们首先看下 ES6 类方式声明组件与函数式声明组件之间的区别,首先是我们上文提及的类方式:

export default class RelatedSearch extends React.Component {
constructor(props) {
super(props);
this._handleClick = this._handleClick.bind(this);
}
_handleClick(suggestedUrl, event) {
event.preventDefault();
this.props.onClick(suggestedUrl);
}
render() {
return (
<section className="related-search-container">
<h1 className="related-search-title">Related Searches:</h1>
<Layout x-small={2} small={3} medium={4} padded={true}>
{this.props.relatedQueries.map((query, index) =>
<Link
className="related-search-link"
onClick={(event) =>
this._handleClick(query.searchQuery, event)}
key={index}>
{query.searchText}
</Link>
)}
</Layout>
</section>
);
}
}

而使用 SFC 模式的话,大概可以省下 29%的代码:

const _handleClick(suggestedUrl, onClick, event) => {
event.preventDefault();
onClick(suggestedUrl);
};
const RelatedSearch = ({ relatedQueries, onClick }) =>
<section className="related-search-container">
<h1 className="related-search-title">Related Searches:</h1>
<Layout x-small={2} small={3} medium={4} padded={true}>
{relatedQueries.map((query, index) =>
<Link
className="related-search-link"
onClick={(event) =>
_handleClick(query.searchQuery, onClick, event)}
key={index}>
{query.searchText}
</Link>
)}
</Layout>
</section>
export default RelatedSearch;

当我们比较这两种不同的实现时,最简单的可观测的差异在于

  • 没有构造函数(5 行)

  • 以 Arrow Function 的方式替代 Render 语句(4 行)

单文件中的代码差异可能不太明显,不过这区区几行代码减少带来的意义却是巨大的。首先,函数式组件顾名思义即可知其不需要引入 Class 关键字,并且其内部也没有this的困扰,JavaScript 语言中所有关于this的令人头大的部分都可以被忽略。这一点在我们进行事件绑定的时候很有作用:

onClick={this.sayHi.bind(this)}>Say Hi</a>
onClick={sayHi}>Say Hi</a>

在无状态组件中我们不需要再显式地绑定this关键字。除此之外,函数式组件只是简单的根据输入的 Props 返回响应的 HTML,并不包含冗余的标签与内嵌的函数,其具有更好的可读性,并且相对应的其可测试性与性能也会更好。因为其没有内部状态以及对生命周期的管理,React 团队计划会在未来面对函数式组件进行深度优化,避免不需要的脏检测以及内存分配。不过虽然我们很推荐使用函数式无状态组件,但也绝对不能滥用,一般来说,有以下特征的组件式绝对不适合使用 SFC 的:

  • 需要自定义整个组件的生命周期管理

  • 需要使用到 ref

this 绑定

JavaScript 中的this一直是令人困惑与头疼的东西,不同于其他有明确类模型定义的语言,JavaScript 中的this会很飘忽不定,特别是在包含回调函数的逻辑中,往往错误就发生在你没有正确的绑定this。而 React 中默认的this指针是指向当前组件的上下文,不过当我们写一些异步代码的时候,this指针就有可能被重定向:

this.setState({ loading: true });
fetch('/').then(function loaded() {
this.setState({ loading: false });
});

上述代码执行时会报TypeError的异常,这是因为在回调函数中this指针被重定向之后不能再找到setState函数。在传统的 JavaScript 中我们可以使用闭包的特性来记录当前的this指针:

var component = this;
component.setState({ loading: true });
fetch('/').then(function loaded() {
component.setState({ loading: false });
});

这种方式简单易用,也适合于初学者理解,不过感觉是以粗劣的方式来解决,也不符合语言特性,下面我们会讨论其他几种解决方案。

类成员方法

React 允许当我们使用React.createClass来声明某个组件时,类中定义的成员方法的上下文会被自动地绑定当前组件对象,这样我们就可以将类成员方法作为回调函数传入异步方法:

React.createClass({
componentWillMount: function() {
this.setState({ loading: true });
fetch('/').then(this.loaded);
},
loaded: function loaded() {
this.setState({ loading: false });
}
});

这种方式相对于前者就优雅了很多,我们并不需要在组件中添加太多额外的代码。不过如果我们使用的 ES2015 类语法来声明某个组件,其中声明的成员函数并不会被自动绑定到当前上下文,这一点需要注意下。

Bind

JavaScript 中所有函数都拥有bind函数,可以来强制绑定该函数的this指针。一旦某个函数的上下文被绑定之后,就不会受到调用者的影响。简单的调用方式如下:

this.setState({ loading: true });
fetch('/').then(function loaded() {
this.setState({ loading: false });
}.bind(this));

这种方式对于其他语言转换而来的开发者而言可能难于理解,另外这种直接在调用时绑定的方式也容易被粗心的开发者所忽略。ES2016(ES7) 中介绍了新的绑定语法,即引入了::双冒号操作符来表示绑定操作,即默认的将左值绑定到右侧的表达式。譬如我们如果自定义了如下的map函数:

function map(f) {
var mapped = new Array(this.length);
for(var i = 0; i < this.length; i++) {
mapped[i] = f(this[i], i);
}
return mapped;
}

不同于 Lodash 中的实现,我们并不需要将数据以参数形式传入,而是直接以类似于调用成员方法的方式来处理:

// [2, 4, 6]

换言之,传统的我们以call或者封装的调用方式都可以转换为新的绑定表达式语法:

[].map.call(someNodeList, myFn);
// or
Array.from(someNodeList).map(myFn);

对于这种类似于数组的数据结构我们可以使用如下语法:

someNodeList::map(myFn);

而这种语法在 React 中的具体使用方式为:

this.setState({ loading: true });
fetch('/').then(this::() => {
this.setState({ loading: false });
});

Arrow Function

ES2015 标准中介绍了所谓的箭头函数(Arrow Function)语法来声明函数表达式,箭头函数最大的特点在于其默认使用了当前闭包中的this指针,即this指针在创建时就被强制绑定而不会受到调用者的影响。

this.setState({ loading: true });
fetch('/').then(() => {
this.setState({ loading: false });
});

无论你嵌套定义了多少层函数,所有的箭头函数都会保存最初的上下文信息。不过这种方式的缺点在于我们无法再使用命名函数,这样在调试时候就不太方便了,匿名函数中抛出的异常都会被标识为anonymous function。如果你是使用了 Babel 这样的转换工具将 ES2015 的代码转换到 ES5,其默认是使用上文介绍的别名方式来固定this的指向:

const loaded = () => {
this.setState({ loading: false });
};
// will be compiled to
var _this = this;
var loaded = function loaded() {
_this.setState({ loading: false });
};