组件声明
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 中的实现,我们并不需要将数据以参数形式传入,而是直接以类似于调用成员方法的方式来处理:
[1, 2, 3]::map(x => x * 2)
// [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 });
};