本系列在《The Road to learn React》指导下,记录如何写出 Hacker News (译者注:一个著名黑客论坛)应用,以及更多相关知识的梳理,能够更好的教会你 React。
知识回顾
在react入门教程01中,介绍了React的简介,并搭建了开发环境、最后创建了第一个React应用;在react入门教程02中,简单地介绍JSX和ES6,同时实现了列表展示,最后接触调试神器 - 模块热替换方式。
本章将指导你了解 React 的基础知识,并创建自己的组件。
组件内部状态
组件内部状态也被称为局部状态,允许你保存、修改和删除存储在组件内部的属性。使用 ES6 类组件可以在构造函数中初始化组件的状态。构造函数只会在组件初始化时调用一次。
下面,构造函数方式实现组件的初始状态,初始状态为一个列表。并将render()
方法中的静态列表改成 使用state里的list。
...
class App extends Component {
constructor(props) {
super(props);
this.state = {
list: list,
};
}
render() {
return (
{this.state.list.map(item => {
return (
);
})}
);
}
}
...
现在list是组件的一部分,它在组件内部状态(state)中。你可以从list中添加、修改或者删除列表项。每次修改组件的内部状态,组件的render()
方法会再次运行。这样,可以简单地实现数据修改后,组件重新渲染并展示正确的数据。
关于上述的构造函数,更多的介绍下:
constructor方法是一个特殊的方法,其用于创建和初始化使用class创建的一个对象。一个类只能拥有一个名为 “constructor”的特殊方法。如果类包含多个constructor的方法,则将抛出 一个SyntaxError 。
一个构造函数可以使用 super 关键字来调用一个父类的构造函数。
单向数据流
使用React的单向数据流,来实现Dismiss
按钮 将从列表中删除数据项:
class App extends Component {
constructor(props) {
super(props);
this.state = {
list: list,
};
this.onDismiss = this.onDismiss.bind(this);
}
onDismiss(id) {
const updatedList = this.state.list.filter(item => item.objectID !== id);
this.setState({ list: updatedList });
}
render() {
return (
{this.state.list.map(item => {
return (
);
})}
);
}
}
单向数据流方式的内部状态更新:
这里,我们为列表中的每一项增加一个按钮。通过该按钮可以删除你不感兴趣的数据项。
几个知识点:
- onDismiss() 是类方法,需要在构造函数中绑定它。
- state更新,会再次运行
render()
方法渲染,最后这个删除项就不再显示了。
深入理解 React的状态与生命周期
原文
这里以一个时钟为例,下面是以手动调用ReactDOM.render()
来重新渲染的方式实现:
function tick() {
const element = (
Hello, world!
It is {new Date().toLocaleTimeString()}.
);
ReactDOM.render(
element,
document.getElementById('root')
);
}
setInterval(tick, 1000);
在codepen的实现案例
下面,我们将实现可复用的Clock
组件,该组件自维护地实现更新和渲染。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
在codepen的实现案例
以上就是这个Clock
组件的实现。从手动渲染方式转换到组件方式的三步骤如下:
第一步,将Function
转换到ES6 Class
。先实现继承React.Component
的同名类,新增render()
方法,将function的方法内容移至render()
方法,使用this.props
替换props
,最后删除空的function定义。
class Clock extends React.Component {
render() {
return (
Hello, world!
It is {this.props.date.toLocaleTimeString()}.
);
}
}
在codepen的实现案例
第二步,使用内部状态。在render()
方法中用this.state.date
替换this.props.date
,使用构造函数初始化状态this.state
,注意需将props
传给父级构造函数,现在渲染
不用再传入date
参数了。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
在codepen的实现案例
第三步,给组件增加生命周期类的方法,我们在第一次渲染组件的时候开启定时器,在组件被移除的时候删除定时器,这里使用componentDidMount
和componentWillUnmount
,这类方法又叫做生命周期钩子
。componentDidMount
在组件被渲染到DOM的时候运行,这里适合开启定时器:
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
在componentWillUnmount()
方法中下架定时器:
componentWillUnmount() {
clearInterval(this.timerID);
}
最后定义tick()
方法来更新状态中的date
数据:
tick() {
this.setState({
date: new Date()
});
}
现在,一个时钟组件就实现好了。
使用状态state
的三点注意事项:
- 不能直接修改
state
,必须通过setState()
方法
// Wrong
this.state.comment = 'Hello';
// Correct
this.setState({comment: 'Hello'});
- 状态更新是一个异步操作,React会因为性能问题,选择批量执行
setState()
调用。所以不能依赖当前状态的数据来计算下一个状态数据。
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
this.setState((prevState, props) => ({
counter: prevState.counter + props.increment
}));
-
setState()
是一个Merge
操作,不会更新或覆盖其他不相干的属性
JavaScript类的绑定
在Dismiss
按钮的实现中,你已经在构造函数中绑定了onDismiss方法,这一步是为了做什么?JavaScript类的绑定是非常重要的,因为类方法不会自动绑定this到实例上。让我们通过代码来验证。
class ExplainBindingsComponent extends React.Component {
onClickMe() {
console.log(this.state);
}
render() {
return (
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
在codepen的实现案例
按钮正确的渲染了,但是当你点击按钮的时候,在开发调试控制台中得到undefined。由于this是undefined所以并不能被检索到,所有为了确保this在类方法中可访问的,需要将this绑定到类方法上。
在构造行数中正确绑定:
class ExplainBindingsComponent extends React.Component {
constructor() {
super();
this.state = {};
this.onClickMe = this.onClickMe.bind(this);
}
onClickMe() {
console.log(this.state);
}
render() {
return (
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
再次尝试点击按钮,这个this对象就指向了类的实例。
最后值得一提的是类方法可以通过ES6的箭头函数做到自动地绑定。
class ExplainBindingsComponent extends React.Component {
constructor() {
super();
this.state = {};
}
onClickMe = () => {
console.log(this.state);
}
render() {
return (
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
如果在构造函数中的重复绑定对你有所困扰,你可以使用这种方式代替。React 的官方文 档中坚持在构造函数中绑定类方法,所以本书也会采用同样的方式。
和表单交互
让我们在程序中加入表单来体验 React 和表单事件的交互,我们将在程序中加入搜索功能, 列表会根据输入框的内容对标题进行过滤。
这里直接给出最终代码实现:
...
// ES 5
// function isSearched(searchTerm) {
// return function(item) {
// return item.title.toLowerCase().includes(searchTerm.toLowerCase());
// }
// }
// ES 6
const isSearched = searchTerm => item =>
item.title.toLowerCase().includes(searchTerm.toLowerCase());
class App extends Component {
constructor(props) {
super(props);
this.state = {
list: list,
searchTerm: '',
};
this.onDismiss = this.onDismiss.bind(this);
this.onSearchChange = this.onSearchChange.bind(this);
}
onDismiss(id) {
const updatedList = this.state.list.filter(item => item.objectID !== id);
this.setState({ list: updatedList });
}
onSearchChange(event) {
this.setState({ searchTerm: event.target.value });
}
render() {
return (
{this.state.list.filter(isSearched(this.state.searchTerm)).map(item => {
return (
);
})}
);
}
}
...
这里,首先在本地内部状态中记录了搜索词searchTerm
,并在输入框中定义了一个onChange处理程序,来更新本地的搜索词的状态。过滤器的功能使用到了isSearched
的高阶函数,该函数返回了另一个函数,因为filter函数接受一个函数作为它的输入,返回的函数可以访问列表项目对象,将会根据函数定义的条件对列表进行过滤。
更多高阶函数
相关内容 中文/English
ES6小知识补充
对象初始化
在 ES6 中,你可以通过简写属性更加简洁地初始化对象。想象下面的对象初始化:
var o = {};
var o = {a: 'foo', b: 42, c: {}};
var a = 'foo', b = 42, c = {};
var o = {a: a, b: b, c: c};
var o = {
property: function ([parameters]) {},
get property() {},
set property(value) {}
};
对象初始化是一个描述对象初始化过程的表达式。对象初始化是由一组描述对象的属性组成。
属性的值可以是原始类型,也可以是其他对象。
没有属性的空对象可以用以下方式创建:
let obj = {};
不过,字面 和初始化 标记的优势在于,可以用内含属性的花括号快速创建对象。简单地编写一个逗号分隔的键值对的类别。如下代码创建了一个含三个属性的对象,键分别为 "foo", "age" 和 "baz"。这些键对应的值,分别是字符串“bar”,数字42和另一个对象。
let obj = {
foo: "bar",
age: 42,
baz: { myProp: 12 },
}
创建对象后,可以读取或者修改它。对象属性可以用下标小圆点标记或者方括号标记访问。
object.foo; // "bar"
object["age"]; // 42
object.foo = "baz";
对象属性也可以是一个函数
// ES5
var userService = {
getUserName: function (user) {
return user.firstname + ' ' + user.lastname;
},
};
// ES6
const userService = {
getUserName(user) {
return user.firstname + ' ' + user.lastname;
},
};
最后值得一提的是你可以在 ES6 中使用计算属性名。
// Computed property names (ES6)
var i = 0;
var a = {
["foo" + ++i]: i,
["foo" + ++i]: i,
["foo" + ++i]: i
};
console.log(a.foo1); // 1
console.log(a.foo2); // 2
console.log(a.foo3); // 3
var param = 'size';
var config = {
[param]: 12,
["mobile" + param.charAt(0).toUpperCase() + param.slice(1)]: 4
};
console.log(config); // { size: 12, mobileSize: 4 }
更多ES6 对象初始化相关内容阅读
ES6 解构
在 JavaScript ES6 中有一种更方便的方法来访问对象和数组的属性,叫做解构。比较下面 JavaScript ES5 和 ES6 的代码片段。
const user = {
firstname: 'Robin', lastname: 'Wieruch',
};
// ES5
var firstname = user.firstname;
var lastname = user.lastname;
console.log(firstname + ' ' + lastname); // output: Robin Wieruch
// ES6
const { firstname, lastname } = user;
console.log(firstname + ' ' + lastname);
// output: Robin Wieruch
使用解构方式简化 map和filter部分的代码:
...
render() {
const { searchTerm, list } = this.state;
return (
{list.filter(isSearched(searchTerm)).map(item => {
return (
...
组件拆分
现在,你有一个大型的 App 组件。它在不停地扩展,最 可能会变得混乱。你可以开始将 它拆分成若干个更小的组件。
让我们开始使用一个用于搜索的输入组件和一个用于展示的列表组件。
第一个是 Search 组件。
class Search extends Component {
render() {
const { value, onChange } = this.props;
return (
);
}
}
第二个是 Table 组件。
class Table extends Component {
render() {
const {list, pattern, onDismiss } = this.props;
return (
{list.filter(isSearched(pattern)).map(item => {
})}
)
}
}
让我们开始使用上述的搜索输入组件和用于展示的列表组件。
...
render() {
const { searchTerm, list } = this.state;
return (
);
}
}
现在你有了三个 ES6 类组件。你可能已经注意到,props 对象可以通过这个类实例的 this 来访问。props 是 properties 的简写,当你在 App 组件里面使用它时,它有你传递给这些组 件的所有值。这样,组件可以沿着组件树向下传递属性。
从 App 组件中提取这些组件之后,你就可以在别的地方去重用它们了。因为组件是通过 props 对象来获取它们的值,所以当你在别的地方重用它时,你可以每一次都传递不同的 props,这些组件就变得可复用了。
可组合的组件
在 props 对象中还有一个小小的属性可供使用: children 属性。通过它你可以将元素从上 层传递到你的组件中,这些元素对你的组件来说是未知的,但是却为组件相互组合提供了 可能性。让我们来看一看,当你只将一个文本(字符串)作为子元素传递到 Search 组件中 会怎样。
render() {
const { searchTerm, list } = this.state;
return (
Search
现在 Search 组件可以从 props 对象中解构出 children 属性。然后它就可以指定这个 children 应该显示在哪里。
class Search extends Component {
render() {
const { value, onChange, children } = this.props;
return ;
}
}
现在,你应该可以在输入框旁边看到这个 “Search” 文本了。当你在别的地方使用 Search 组 件时,如果你喜欢,你可以选择一个不同的文本。总之,它不仅可以把文本作为子元素传 递,还可以将一个元素或者元素树(它还可以再次封装成组件)作为子元素传递。children 属性让组件相互组合到一起成为可能。
可复用组件-Button
可复用和可组合组件让你能够思考合理的组件分层,它们是 React 视图层的基础。前面几 章提到了可重用性的术语。现在你可以复用 Search 和 Table 组件了。甚至 App 组件都是可 复用的了,因为你可以在别的地方重新实例化它。
让我们再来定义一个可复用组件 Button,最 会被更频繁地复用。
class Button extends Component {
render() {
const { onClick, className='', children } = this.props;
return (
)
}
}
替换Table组件中的button元素:
class Table extends Component {
render() {
const { list, pattern, onDismiss } = this.props;
return (
{list.filter(isSearched(pattern)).map(item => (
))}
);
}
}
函数式无状态组件
函数式无状态组件: 这类组件就是函数,它们接收一个输入并返回一个输出。输入是 props,输出就是一个普通的 JSX 组件实例。到这里,它和 ES6 类组件非常的相似。然而,函数式无状态组件是函数(函数式的),并且它们没有本地状态(无状态的)。你不能通过 this.state 或者 this.setState() 来访问或者更新状态,因为这里没有 this 对象。此外,它也没有生命周期方法。虽然你还没有学过生命周期方法,但是你已经用到了其中两个:constructor() and render()。constructor 在一个组件的生命周期中只执行一次,而 render() 方法会在最开始执行一次,并且每次组件更新时都会执行。当你阅读到后面关于生命周期方法的章节时,要记得函数式无状态组件是没有生命周期方法的。
什么时候更适合使用函数式无状态组件而非 ES6 类组件?一个经验法则就是当你不需要本地状态或者组件生命周期方法时,你就应该 使用函数式无状态组件。
把 Search 组件重构成一个函数式无状态组件:
const Search = ({ value, onChange, children }) => {
// do something
return (
);
};
给组件声明样式
整个应用声明样式 -- src/index.js 文件:
body {
color: #222;
background: #f4f4f4;
font: 400 14px CoreSans, Arial, sans-serif;
}
a {
color: #222;
}
a:hover {
text-decoration: underline;
}
ul,
li {
list-style: none;
padding: 0;
margin: 0;
}
input {
padding: 10px;
border-radius: 5px;
outline: none;
margin-right: 10px;
border: 1px solid #dddddd;
}
button {
padding: 10px;
border-radius: 5px;
border: 1px solid #dddddd;
background: transparent;
color: #808080;
cursor: pointer;
}
button:hover {
color: #222;
}
*:focus {
outline: none;
}
src/App.css - App 文件中给你的组件声明样式
.page {
margin: 20px;
}
.interactions {
text-align: center;
}
.table {
margin: 20px 0;
}
.table-header {
display: flex;
line-height: 24px;
font-size: 16px;
padding: 0 10px;
justify-content: space-between;
}
.table-empty {
margin: 200px;
text-align: center;
font-size: 16px;
}
.table-row {
display: flex;
line-height: 24px;
white-space: nowrap;
margin: 10px 0;
padding: 10px;
background: #ffffff;
border: 1px solid #e3e3e3;
}
.table-header > span {
overflow: hidden;
text-overflow: ellipsis;
padding: 0 5px;
}
.table-row > span {
overflow: hidden;
text-overflow: ellipsis;
padding: 0 5px;
}
.button-inline {
border-width: 0;
background: transparent;
color: inherit;
text-align: inherit;
-webkit-font-smoothing: inherit;
padding: 0;
font-size: inherit;
cursor: pointer;
}
.button-active {
border-radius: 0;
border-bottom: 1px solid #38bb6c;
}
现在可以在一些组件中使用这些样式。
APP:
...
render() {
const { searchTerm, list } = this.state;
return (
Search
);
}
}
Table组件:
class Table extends Component {
render() {
const { list, pattern, onDismiss } = this.props;
return (
{list.filter(isSearched(pattern)).map(item => (
))}
);
}
}
小结
你已经学习了编写一个 React 应用所需要的基础知识了!
React
- 使用this.state和setState()来管理你的内部组件状态
- 将函数或者类方法传递到你的元素处理器
- 在 React 中使用表单或者事件来添加交互
- 在 React 中单向数据流是一个非常重要的概念
- 拥抱 controlled components
- 通过 children 和可复用组件来组合组件
- ES6类组件和函数式无状态组件的使用方法和实现
- 给你的组件声明样式的方法
ES6
- 绑定到一个类的函数叫作类方法
- 对象初始化
- 解构对象和数组
- 默认参数
你可以进一步阅读官方文档。
你还可以在这里找到源码。