本文将从一个简单的例子开始,逐步深入 React 的编写细节。

React Native 主张用 React 的开发思维来编写 UI 层。因此在学习 React-Native 之前,了解基本的 React 的语法和存在的坑会对今后 React Native 的开发大有裨益。

本文将从一个简单的例子开始,逐步完善我们的程序。在这个过程中,我们将一步步探讨如何用 React 来开发网页应用,以及需要注意的陷阱。与其他教程不同,本文将采用类似 Zed A. Shaw《Learn Code the Hard Way》 系列的案例驱动的形式,从例子开始着手。我相信,掌握一门新技术最好的方法就是自己动手。因此,我并不打算面面俱到的列举所有关于 React 的内容,而更倾向于担任一个引路人的角色:把读者们带到 React 花园的门前,然后让读者们在 React 花园里来一场自助游。为了让这个旅途更加有收获,我会在每节内容的最后安排几个练习,并在最后分享一些值得深入学习的文章和教程。

练习0:准备工作

下载 React 的 Starter Kit 0.14.0 并解压。得到的目录结构长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
react-0.14.0/       # React 根目录
|
+-- build/ # React 的 js 代码
|
+-- examples/ # 官方提供的例子
| |
| +---- basic/
| |
| +---- basic-click-counter/
| |
| +---- ...
|
+-- README.md # 说明文档
  • build 目录存放的是 React 的 js 文件,我们编写的所有例子都会包含这个目录下的若干文件。
  • examples 目录包含了官方提供的例子。

接下来我们需要启动一个简单的 HTTP 服务器方便我们本地预览我们的应用:

1
2
$ cd react-0.14.0
$ python -m SimpleHTTPServer

接下来可以用浏览器访问 http://localhost:8000/examples/basic/ ,你将看到这样的页面:

该页面会统计用户自打开这个页面开始经过的时间。

用 Atom 载入整个目录。启动 Atom ,点击 【File】-> 【Add Project Folder】 菜单项,选择 react-0.14.0 目录所在文件夹。

在根目录下创建一个新的文件夹 test ,在 test 目录下新建页面文件 index.html 。

本文后面的大部分练习都只涉及对这个文件进行修改。

扩展练习

  1. 访问 examples 目录里的每个例子,感受下用 React 写的网页应用。
  2. Atom + Nuclide 是 Facebook 推荐的 React IDE 。本系列也将一直使用它学习 React / React Native。熟悉下 Atom 的使用,并选择安装我在上篇博文中推荐的一些插件。
  3. 试试使用 browser-plus 插件在 Atom 中直接预览页面。

练习1:Hello World

按照惯例,让我们先来实现一个简单的 Hello World 程序。在 index.html 里敲入下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
ReactDOM.render(
React.createElement("h1", null, "Hello World!"),
document.getElementById('container')
);
</script>
</body>
</html>

我们先看看这个页面的效果。访问 http://localhost:8000/test/ ,你将看到这样的界面:

如果您的 build 文件夹中没有 react-dom.js 文件,您可能下载的是 0.13 或者更早的版本,建议下载使用 Starter Kit 0.14.0

代码解读

  • 程序的第 4 行和第 5 行引用了 build 目录下的 react.js 和 react-dom.js 文件。其中,react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能。
1
2
3
4
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
</head>
  • 第 10 行调用了 ReactDOM.render 函数:
1
2
3
4
5
ReactComponent render(
ReactElement element,
DOMElement container,
[function callback]
)

这个函数用来将一个 React 元素 element 渲染到 container 指定的 DOM 中。最后的一个参数 callback 是可选的,用于指定该组件绘制或更新完成后需要执行的回调。

某些教程会使用 React.render 来渲染页面,这个函数已经过时。建议使用新的 ReactDOM.render 函数。

在我们的例子中,我们用 React.createElement 创建了一个内容为 “Hello World!” 的一级标题。当页面启动时,这个一级标题会被插入到 id 为 container 的 div 容器中。

1
2
3
4
5
ReactElement createElement(
string/ReactClass type,
[object props],
[children ...]
)

React.createElement 函数的第一个参数是元素类型,可以是 h1div 等 HTML 元素,也可以是 ReactClass 类型(后面会提到),接下来是两个可选参数 propschildren ,分别表示要赋予的属性和子元素。

打开浏览器的调试工具(例如 Chrome 的审查工具),可以看到带有 “Hello World!” 文字信息的一级标题被插入到了 container 这个 div 容器中:

拓展训练

  1. 试试将 “Hello World!” 这句话改成其他内容,刷新下页面,看看内容有没有变。
  2. React.DOM 是对 React.createElement 的封装和简化。查下 React.DOM文档,试试将代码用 React.createDOM 重写。

练习2:JSX

在练习1中我们使用 React 提供的 render() 函数实现了向指定 DOM 中插入内容的简单功能。但这段文本内容是 Hard-Code 的,没有数据绑定的过程,不利于数据和页面模板的分离。

另一个很糟糕的问题是,像 React.createElement 这类创建元素的方法不如直接编写 HTML 直观。举个例子,假设现在我们需要在 “Hello World!” 标题和 container 容器中增加一层:把 “Hello World!” 放入一个名为 greeting 的 div 容器,再把这个 greeting 容器放入 container 容器里。从页面层级来看,关系应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
+-------------------------+
| container |
| +-------------------+ |
| | | |
| | greeting | |
| | +-------------+ | |
| | |Hello World! | | |
| | +-------------+ | |
| | | |
| +-------------------+ |
| |
+-------------------------+

如果用 React.createElement 来实现 greeting 和 “Hello World!” 标题的动态创建,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/javascript">
ReactDOM.render(
React.createElement("div", { id: "greeting" },
React.createElement("h1", null, "Hello World!")
), document.getElementById('container'));
</script>
</body>
</html>

可以看到,仅仅是增加一层嵌套,就需要再写一层 React.createElement 。想象一下,当日后我们的项目变得越来越复杂时,我们的代码里可能会有一堆的 Reacte.createElement 嵌套,代码的可读性越来越差,甚至难以继续维护。

JSX 就是为了解决上面的问题而设计出来的一套扩展语法,它的特点是在 JavaScript 中加入了类 XML 语法特性。我们在开发网页应用的时候,不再需要调用无趣的 Reacte.createElement 来创建页面元素,而可以写 HTML 页面一样完成页面的编写。

JSX 的取名含义应该就是 JS + XML 。

要使用 JSX ,我们需要对我们的代码做一些改造。将 ReactDOM.render 的内容改成:

1
2
3
4
5
6
ReactDOM.render(
<div id="greeting">
<h1>Hello World!</h1>
</div>,
document.getElementById('container')
);

不过这段代码并不能直接被浏览器渲染,我们需要将它保存到另一个文件 main.jsx 中:

完成后使用 babel 命令将 main.jsx 转成浏览器支持的 JavaScript 代码:

1
2
3
4
$ npm install --save-dev babel-cli babel-present-react  # 安装 babel
$ echo '{ "presets": [ "react" ], "plugins": []}' > ~/.babelrc # 将 react 插件添加进 .babelrc
$ cd test
$ babel main.jsx -o main.js

完成后会在当前目录下生成 main.js 文件,我们打开它看看里面的内容:

可以和我们在上一节写的JavaScript代码比较下,是不是一模一样?现在可以在我们页面代码中把个脚本文件引用进来:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
</head>
<body>
<div id="container"></div>
<script src="main.js"></script>
</body>
</html>

代码解读

如前面所说,JSX 其实就是在 JS 的基础上加入了类 XML 的语法。HTML 的标签直接写在 JavaScript 代码中,不加任何引号,这就是 JSX 的语法。它允许 HTML 与 JavaScript 的混写。纯 JS 的代码很难看出页面的逻辑,而加入了 HTML 的标签支持后,程序的可读性就大大提高了。

为了更详细的说明 JSX 语法的特点,我们对 main.jsx 的代码做点修改,将 “Hello World!” 字符串提取出来作为一个变量 greeting

1
2
3
4
5
6
7
var greeting = "Hello World!";
ReactDOM.render(
<div id="greeting">
<h1>{greeting}</h1>
</div>,
document.getElementById('container')
);

上面代码体现了 JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。我们再次用 babel 转换成 JS 代码,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";

var greeting = "Hello World!";
ReactDOM.render(React.createElement(
"div",
{ id: "greeting" },
React.createElement(
"h1",
null,
greeting
)
), document.getElementById('container'));

即时渲染 JSX

由于是一门扩展语言,JSX 的代码并不能直接被浏览器渲染,所以我们不能直接在代码中引用 JSX 代码,而应该先用 babel 工具转换成 JavaScript 再引用。为了方便调试,我们可以使用 babel 中的 browser.js 来让浏览器支持渲染 JSX 。browser.js 属于 babel-core ,先安装 babel-core 。要注意的是 Babel 从 6.0 开始不再提供 browser.js ,因此我们需要安装版本 5 的 babel-core :

1
$ npm install babel-core@5

然后将 index.html 修改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../node_modules/babel-core/browser.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/babel">
var greeting = "Hello World!";
ReactDOM.render(
<div id="greeting">
<h1>{greeting}</h1>
</div>,
document.getElementById('container')
);
</script>
</body>
</html>

程序的第 6 行添加了对 browser.js 的引用,第 10 行开始直接加入 JSX 代码。需要注意的是脚本的类型需要为 text/babel ,用于告诉浏览器这段代码是 JSX 代码,需要使用 browser.js 渲染。

browser.js 的原理其实是在页面运行时动态将 JSX 转成 JavaScript 再渲染,这个过程比较耗时。实际发布项目时依然建议使用 babel 将 JSX 预转换成 JavaScript 。

扩展练习

  1. 试试修改 JSX 代码中 HTML 中的部分,看看会有什么变化;
  2. 试试修改 JSX 代码中 JavaScript 的部分,看看 JavaScript 的一些常见语法特性是否能够被支持。例如将第 14 行改为 <h1>{"Hello " + "World!"}</h1>
  3. 试试在 JSX 代码中 JavaScript 的部分写一个 if-else ,看看能否像期望的那样工作。如果不能,需要怎么修改使它工作?(提示:参考 If Else in JSX

练习3:组件和属性

为了更好的将页面模块化,React 使用组件来表示每个页面模块。组件可以像其他 HTML 标签一样使用 ReactDOM.render 直接绘制。组件可以包含属性和状态。

  • 属性(props):类似 HTML 中的属性,在绘制的时候可以直接在标签中添加属性,然后在组件中通过 this.props.属性名 获取。
  • 状态(state):维护组件内部的状态。一个组件就是一个状态机。React 把用户界面当作简单状态机,把用户界面想像成拥有不同状态然后渲染这些状态。在 React 中,一旦组件的 state 发生变化,用户界面有改动的部分就会被重绘。组件的状态通常在组件的内部函数 getInitialState() 中声明,使用 setState() 函数更新值,并通过 this.state.状态名 来获取值。

我们将在下一个练习了解状态的使用。现在先让我们把焦点放在属性上。将 main.html 改写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../node_modules/babel-core/browser.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/babel">
var Greeting = React.createClass({
render: function() {
return (
<div id="greeting">
<h1>{this.props.word}</h1>
</div>
);
}
});

ReactDOM.render(
<Greeting word="Hello World!"/>,
document.getElementById('container')
);
</script>
</body>
</html>

代码解读

在上面的代码中,我们使用 React.createClass() 来创建一个组件实例。JSX 里约定分别使用首字母大、小写来区分本地组件的类和 HTML 标签。每个组件通常都会有一个 render() 函数,用于指定当调用 ReactDOM.render() 渲染该组件时的方式。该函数会使用 return 语句返回一个页面节点。在我们的例子中,我们将问候语作为一个 word 属性,在 Greeting 组件中通过 this.props.word 来获取,并放入一个一级标题中,再在外层用一个 id 为 “greeting” 的 div 包含。

  • 官方建议组件的取名以大写字母开头,以区分 HTML 标签。
  • 目前, 一个 component 的 render,只能返回一个节点。如果你需要返回一堆 div , 那你必须将你的组件用 一个div 或 span 或任何其他的组件包裹。

ReactDOM.render() 函数中,我们可以像使用其他 HTML 标签一样使用自定义的组件,并传入一个自定义属性 word

经过这么修改,我们把原本 Hard-Code 的 “Hello World!” 字符串改成通过组件属性来传递,这个过程就完成了视图和数据的 绑定

现在我们使用 react-devtool 来调试 React 程序,看看属性是如何被传入到组件里的。如果你的浏览器还没有装这个插件,现在就装上它(Chrome 版 | Firefox 版)。

打开浏览器的调试工具,点击 React 选项卡,如图所示:

调试工具左侧的窗口展示了 Greeting 组件完成数据绑定后的结果,右边的窗口展示了 Greeting 组件的所有属性,目前只有一个 word 属性。我们在左边窗口的代码首行单击鼠标右键,可以打开一个菜单。选择 【Show Source】 可以跳进 Greeting 的源码,选择 【Show in Elements Pane】 可以跳进 HTML 元素面板中,如下图所示:

扩展练习

  1. 试试在组件的 render 函数中返回多个根节点,看看会不会报错。
  2. 阅读官方文档有关属性验证的内容,编写对 word 属性的类型验证,并尝试将 word 的值修改为数值或者其他类型看看能否通过验证。
  3. 阅读官方文档有关属性默认值 的内容,为 word 属性增加一个默认值 “Hello World” 。
  4. 阅读官方文档有关扩展属性(Spread Attributes)的内容,为 Greeting 添加一个新属性 date ,并使用 {..props} 传入这两个属性的值。

练习4:展示一组数据

我们继续完善我们的例子。现在我们希望能够传入一组人的名字,然后让 Greeting 组件向这些人问好。

将 index.html 改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../node_modules/babel-core/browser.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/babel">
var Greeting = React.createClass({
render: function() {
return (
<ol id="greeting">
{
this.props.names.map(function (name) {
return <li>Hello, {name}!</li>;
})
}
</ol>
);
}
});

var names = ['Alice', 'Bob', 'Cindy'];
ReactDOM.render(
<Greeting names={names}/>,
document.getElementById('container')
);
</script>
</body>
</html>

刷新下浏览器,效果如下:

代码解读

让我们先看看 ReactDOM.render() 部分:

1
2
3
4
5
var names = ['Alice', 'Bob', 'Cindy'];
ReactDOM.render(
<Greeting names={names}/>,
document.getElementById('container')
);

这一部分的内容和之前的区别不大,唯一的区别就是 names 属性的取值通过传入一个变量 names 来完成,由于是一个 JavaScript 的列表型变量,因此,names 的两端需要用 {} 包围 。

我们再看看 Greeting 组件的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Greeting = React.createClass({
render: function() {
return (
<ol id="greeting">
{
this.props.names.map(function (name) {
return <li>Hello, {name}!</li>;
})
}
</ol>
);
}
});

在程序的第 6 行,我们使用 JavaScript 的 Array.prototype.map() 操作将 names 数组的每个值 name 一个个使用 <li>Hello, {name}</li> 的形式重新创建,得到一个新的数组再返回给 ReactDOM.render() 函数绘制。注意 Array.prototype.map() 操作是一个 JavaScript 操作,所以必须使用 {} 包围。

打开 React 调试工具,可以看到 names 属性变成了一个列表:

注意到调试工具的终端窗口出现了一个警告:

为了解释这个问题,我们先来了解一下虚拟 DOM 。

HTML 或 XML 文档是使用 DOM (Document Object Model,文档对象模型)来表示和处理的。DOM 技术使得用户页面可以动态地变化,如可以动态地显示或隐藏一个元素,改变它们的属性,增加一个元素等,使得页面的交互性大大地增强。

然而,DOM 有一个致命的缺点——慢。举个例子,假如我们需要在某个节点动态插入一个元素,那就需要先定位到那个节点再进行插入。假如要插入多个元素,那么节点的定位和插入的时间就要成倍增加。对于一个复杂的页面,整个过程可能非常耗时。

为了提高页面元素操纵的效率,React 提出了虚拟 DOM 的技术:组件在插入文档之前,并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,因此称为虚拟 DOM 。与 DOM 相比,虚拟 DOM 放弃了定位和修改节点的过程,而是通过一种称为 DOM diff 的算法找出中这个虚拟 DOM 中发生改动的部分,然后对这些部分进行整体刷新。这样,多次的节点定位和修改就合并成了一次组件的整体刷新。这就是为什么虚拟 DOM 的速度要比 DOM 快的重要原因。

由上也可看出,虚拟 DOM 技术依赖于 DOM diff 算法的效率和准确性。而这个算法依赖于以下两个假设:

  1. 组件的 DOM 是相对稳定的。虚拟 DOM 在任何一个时刻的快照,和短时间内另一时刻的快照并不会有太大的变化,这样就很容易通过比较找出发生改动的部分。
  2. 类型相同的兄弟节点可以被唯一的标识。如果同类型的兄弟节点没有唯一的标识,那么不同时刻的虚拟 DOM 在同一级的 Diff 结果可能会不稳定。React 允许使用 key 属性来标识节点。

列表的每个子元素就是类型相同的兄弟节点,如果列表的子元素不加上 key 属性标识,当列表的元素发生改变(例如有个新元素插入到头部),有可能会影响 DOM diff 的判断,从而影响算法的效率和准确性。

拓展训练

  1. 对于我们这个例子,如何修改代码来消除这个警告?
  2. 阅读官方文档有关this.props.children的内容,尝试使用 this.props.children 取代例子中的 this.props.names 展示数据。

练习5:增加交互

到目前为止 Greeting 组件的 name 属性的值都是在代码中事先写好的,程序运行的过程中没法再改变。现在我们对这个例子做些修改,让它在运行时接受我们的输入,并生成问候语。

修改 index.html 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../node_modules/babel-core/browser.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/babel">
var Greeting = React.createClass({
getInitialState: function() {
return {
name_list : []
};
},
render: function() {
return (
<div>
<ol>
{
this.state.name_list.map(function (name) {
return <li key={name}>Hello {name}!</li>;
})
}
</ol>
<input ref="name_input" placeholder="Input a name here" type="text"/>
<input type="submit" onClick={this.handleClick} />
</div>
);
},
handleClick: function(event) {
var names = this.state.name_list;
var input_name = this.refs.name_input.value;
names.push(input_name);
this.setState({name_list: names});
this.refs.name_input.value = "";
}
});

ReactDOM.render(
<Greeting/>,
document.getElementById('container')
);
</script>
</body>
</html>

刷新下浏览器,此时页面初始化时只有一个文本输入框和一个提交按钮:

此时注意到调试工具中出现了一个新的 State 对象,该对象包含一个 0 元素的 name_list 列表。

往文本框中输入名字并点击提交按钮后,页面就会出现相应的问候语:

此时调试工具中的 State 对象也发生了相应变化,name_list 中的元素会记录下用户输入的所有名字。

代码解读

在练习 3 中我们简单提过状态(state)。React 把用户界面当作简单状态机,把用户界面想像成拥有不同状态然后渲染这些状态。对于在代码中需要动态改变的数据,例如需要对用户输入、服务器请求或者时间变化等作出响应,这时就需要使用 state 。在我们的例子中,此时 Greeting 组件所需要渲染的名字列表是由用户输入的,所以应该将其改写成 state 。

  • 程序的第 12 ~ 16 行声明了一个 name_list 状态并初始化为一个 0 元素的空列表([])。
1
2
3
4
5
getInitialState: function() {
return {
name_list : []
};
},

在使用状态的组件中,这个函数通常是必须编写的。否则会报 “Cannot read property ‘name_list’ of null” 错误。

  • 程序的第 27 ~ 28 行增加了两个页面表单元素,用于接收用户输入和设置响应按钮点击事件为实例的 handleClick() 函数。
1
2
<input ref="name_input" placeholder="Input a name here" type="text"/>
<input type="submit" onClick={this.handleClick} />
  • 程序的第 32 ~ 37 行是对 handleClick() 函数的实现。需要格外注意的一点是获取输入框的内容的方式。

我们前面已经说到,组件在插入页面前其实是在虚拟 DOM 中的表示,因此,在渲染成最终实际的 DOM 前,你不能通过直接访问组件内的元素来试图获取它的属性。对于我们的代码,Greeting 组件的子节点有一个文本输入框,用于获取用户的输入。这时就必须获取真实的 DOM 节点,虚拟 DOM 是拿不到用户输入的。为了做到这一点,我们在文本输入框添加了一个 ref 属性 name_input,然后通过 this.refs.name_input 就指向这个虚拟 DOM 的子节点。

1
2
3
4
5
6
7
handleClick: function(event) {
var names = this.state.name_list;
var input_name = this.refs.name_input.value;
names.push(input_name);
this.setState({name_list: names});
this.refs.name_input.value = "";
}

如果需要获取这个元素自身的真实 DOM 节点,可以使用 ReactDOM.findDOMNode 方法。该方法将在虚拟 DOM 插入文档以后才返回该元素实际的 DOM 节点。

扩展练习

  1. 阅读官方文档有关 state 与 props 的选择,了解什么时候要用 state ,什么时候要用 props 。
  2. 阅读官方文档有关 React 支持的事件 ,为文本框增加一个按键事件:当按下回车键时触发提交。
  3. 这个页面有一个bug:当用户什么都不输入,直接点 sumbit 按钮时,页面将把空文本当成 name 的 state 传入给 Greeting 组件渲染。如下图所示:
    怎么对用户的输入进行验证?
  4. 利用 ReactDOM.findDOMNode 函数,增加一个按钮,当点击该按钮时,让输入框获得焦点。
  5. 为了给用户一个输入示例,我们可以给 input 增加一个 value="Alice" 属性,让它在页面初始时给出一个示例。如下:

但这引来了一个 bug :输入框变成了不可变。怎么解决这个问题?(留意终端的错误警告信息)

练习6:复合组件

通过观察我们上一节的程序,我们可以看到 Greeting 组件其实包含了两个部分:一个用来展示问候语的列表,以及一个输入名字的表单。从功能上看,这两个部分可以各自作为一个独立的组件 NameList 和 NameForm ,然后再组合成一个复合组件 GreetingWidget 。画图示意如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
+-----------------------------+
| GreetingWidget |
| |
| +-----------------------+ |
| | NameList | |
| | | |
| +-----------------------+ |
| |
| +-----------------------+ |
| | NameForm | |
| | | |
| +-----------------------+ |
+-----------------------------+

这样的设计看起来好像很合理,然而在 React 中实现可能会遇到问题。在 React 里面,数据流是一个方向的:从拥有者到子节点。这是因为根据 the Von Neumann model of computing ,数据仅向一个方向传递。你可以认为它是单向数据绑定。因此, NameList 里头展示的数据必须由 GreetingWidget 以属性的方式传入,而这些属性又必须从 NameForm 获取。试图从子节点获取数据就违反了 React 单向数据绑定的原则。为了解决这个问题,我们可以以属性的形式传递一个回调函数 onNameSubmit() 给 NameForm 。当点击 NameForm 里的 submit 按钮时,就调用这个回调函数并将 name 数据作为参数交给回调函数处理。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!DOCTYPE html>
<html>
<head>
<script src="../build/react.js"></script>
<script src="../build/react-dom.js"></script>
<script src="../node_modules/babel-core/browser.js"></script>
</head>
<body>
<div id="container"></div>
<script type="text/babel">
var GreetingWidget = React.createClass({
getInitialState: function() {
return {
name_list : []
};
},
render: function() {
return (
<div>
<NameList name_list={this.state.name_list} />
<NameForm onNameSubmit={this.handleNameSubmit} />
</div>
);
},
handleNameSubmit: function(name) {
var names = this.state.name_list;
names.push(name);
this.setState({name_list: names});
}
});

var NameForm = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
var name = this.refs.name_input.value;
if (!name) {
return;
}
this.props.onNameSubmit(name);
this.refs.name_input.value = "";
return;
},
render: function() {
return (
<form onSubmit={this.handleSubmit}>
<input ref="name_input" placeholder="Input a name here" type="text" />
<input type="submit" />
</form>
);
}
});

var NameList = React.createClass({
render: function() {
return (
<div>
<ol>
{
this.props.name_list.map(function (name) {
return <li key={name}>Hello {name}!</li>;
})
}
</ol>
</div>
);
}
});

ReactDOM.render(
<GreetingWidget />,
document.getElementById('container')
);
</script>
</body>
</html>

代码解读

  • 代码的第 11 ~ 30 行是 GreetingWidget 组件的实现,第 32 ~ 51 行是 NameForm 组件的实现。第 53 ~ 68 行是 NameList 的组件。在第 20 行和第 21 行, Greeting 组件分别包含了 NameForm 组件和 NameList 组件:
1
2
<NameList name_list={this.state.name_list} />
<NameForm onNameSubmit={this.handleNameSubmit} />

其中,Greeting 组件将 handleNameSubmit() 函数作为一个属性传递给 NameForm 当做回调函数。

在上图所示的调试工具中也可以清楚的看到 GreetingWidget 在虚拟 DOM 中的内部结构。

  • 在 NameForm 的实现中,我们将表单的 onSubmit 事件指定使用该组件实例的 handleSubmit() 函数处理:
1
2
3
4
5
6
7
8
render: function() {
return (
<form onSubmit={this.handleSubmit}>
<input ref="name_input" placeholder="Input a name here" type="text" />
<input type="submit" />
</form>
);
}

handleSubmit() 函数调用了父节点 GreetingWidget 传进来的回调函数 onNameSubmit() 函数,并传入本节点的输入框控件的值作为 name 参数:

1
2
3
4
5
6
7
8
9
10
handleSubmit: function(e) {
e.preventDefault();
var name = this.refs.name_input.value;
if (!name) {
return;
}
this.props.onNameSubmit(name);
this.refs.name_input.value = "";
return;
},

注意在这里我们调用了 preventDefault() 来避免使用浏览器默认的行为提交表单。

  • GreetingWidget 的 onNameSubmit() 回调函数指定使用 handleNameSubmit() 函数来处理,该函数接收子节点回传的 name 参数,并通过 setState() 方法追加到当前 name_list 列表中:
1
2
3
4
5
handleNameSubmit: function(name) {
var names = this.state.name_list;
names.push(name);
this.setState({name_list: names});
}
  • 在 GreetingWidget 中,由于要处理用户输入,数据被定义为 State 。在调试工具中,点击根节点 GreetingWidget ,注意右侧数据区中的 name_list 是以 State 定义的:

而传给 NameList 的数据只用来展示,所以可以定义为 Props 。在调试工具中,点击 NameList 子节点,注意右侧数据区中的 name_list 是以 Prop 定义的:

扩展练习

  1. 试试移除第 34 行 e.preventDefault(); 重新提交下数据,看看有什么变化;
  2. 给我们的页面元素添加样式,注意在 JSX 中指定页面元素 css 属性应该使用 className 属性。详见 Supported Attributes
  3. 使用单向数据绑定是 React 保持简单的一个重要体现。如果确实需要双向数据绑定,从子节点返回数据给父节点,可以考虑使用 ReactLink 。阅读官方文档关于 ReactLink 的介绍,并尝试使用 ReactLink 取代回调的方式重新实现本节的例子。

补遗

本文从例子入手,一步步介绍了 JSX 、组件、属性、状态、数据展示、表单处理、复合组件等 React 开发中的基础概念,在其中存在的一些坑和值得深究的东西也尽量以扩展练习的形式交给读者主动去学习掌握。

受限于篇幅关系,本文所介绍的内容主要是为了后续学习 React Native 做准备,而不足以囊括 React 开发基础的所有方面。例如:

  1. 没有深入探讨组件的生存周期
  2. 没有介绍 Mixins 和如何用它来编写可复用组件;
  3. 没有引入与 Ajax 结合的网络编程;
  4. 没有介绍 Flux/Reflux/GraphQL/Relay 等数据处理库。

如果希望继续深入学习 React 开发,在学习完本文之后,您可以继续阅读下面列举的资料:

  1. 阅读 Starter Kit 中自带的所有官方例子的代码;
  2. 阅读 官方教程 ,了解如何使用 React 和 Ajax 进行网络编程。
  3. 阅读上面提及的链接,补充学习本文所遗漏的内容。

其它推荐的学习材料:

  1. Awesome-React 系列
  2. React 入门实例教程 - 阮一峰
  3. QCon上海2015 - React 实战 - 王沛
  4. QCon上海2015 - 探索 React 生态圈 - 郭达锋
  5. jQuery versus React.js thinking - zigomir

Comments