React 开胃菜

react-appetizer

背景

基于HTML的前端界面开发正变得越来越复杂,其本质问题基本可以归结于如何将来自服务器端或者用户输入的动态数据高效的反应到复杂的用户界面上。

概述

React 是一个Facebook和Instagram用来创建用户界面的JavaScript库。那么Facebook开发这个Js库是为了解决什么问题呢?

基于上面的背景来说, React主要是为了解决一个问题, 构建随着时间而数据不断变化的大规模应用程序。相比传统型的前端开发,React开辟了一个相对另类的途经,实现了前端界面的高效率高性能开发。

原理

在Web开发中,我们总需要将变化的数据实时反应到UI上,这时就需要对DOM进行操作。而复杂或频繁的DOM操作通常是性能呢瓶颈产生的原因(如何进行高性能的复杂DOM操作通常是衡量一个前端开发人员技能的重要指标)。

React为此引入了虚拟DOM(Virtual DOM)的机制:在浏览器端用JavaScript实现一套DOM API。基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行比对,得到DOM结构的变化,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。

而且React能够批处理虚拟DOM的刷新,即多次的数据变化会被合并,比如DOM A => B, B => C, C => A, React会认为UI没有任何变化。

尽管每次都需要构造完整的虚拟DOM树,但是因为虚拟DOM是内存数据,性能是极高的,而对实际DOM进行操作的仅仅是不同DOM部分。

我们开发者需要关心仅仅是数据的变化和在任意一个数据状态下,整个界面是如何Render的, 而不需要关心数据变化后如何更新DOM,这后面的一切React会帮我们搞定。

开胃菜

说那么多,还不如实际演练来的体会深切,下面我们就来写一个TODOList 的DEMO,来体会下React的神奇之处吧。

我们使用boostrap来快速实现我们需要的样式,下面是html代码

<html>
<head>
<title>React TODOList</title>
<link href="./build/bootstrap.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">TODOList - 开胃菜</div>
<ul class="list-group">
<li class="list-group-item">9点 整理购书清单</li>
<li class="list-group-item">11 点外出办事</li>
<li class="list-group-item">明天上医院</li>
<li class="list-group-item">回家</li>
</ul>
</div>
<form role="form">
<input class="form-control" placeholder="你下一步打算做什么?" />
</form>
</div>
</body>
</html>

效果图
todolist

第一步,React 初尝

下一步我们来看看怎么用React来实现上图的TODOList的面板组件。

可以从这里下载入门套件

首先,引入我们需要的React相关js。

<html>
<head>
<title>React TODOList</title>
<link href="./build/bootstrap.min.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="con"></div>
<script src="./build/react.js"></script>
<script src="./build/react-dom.js"></script>
<script src="./build/browser.min.js"></script>
<script type="text/babel" src="./jsx/todolist.jsx"></script>
</body>
</html>

jsx/todolist.jsx

ReactDOM.render(
<h1>Hello, world!</h1>,
document.body
);

如果页面出现了Hello, world, 那么恭喜你,你已经成功的迈出了第一步了。

我们来讲解上面发生了什么。

ReactDOM.render()

ReactDOM.render是React的最基本方法,用于将模板转为HTML语言,并插入到指定的DOM节点。
上面的代码将一个h1标题,插入body中。运行结果如下

JSX语法

我们把HTML语言直接写在JavaScript语言中, 即在JavaScript代码写着XML格式的代码称为JSX, 为了把JSX转成标准的JavaScript, 我们用<script type="text/babel">标签,并引入Babel来完成在浏览器里的代码转换。 更多关于JSX语法,请戳这里

实现

下面是完整的JSX代码。

var TodoListBox = React.createClass({
render: function () {
return (
<div className="col-md-4">
<div className="panel panel-default">
<div className="panel-heading">TODOList - 开胃菜</div>
<ul className="list-group">
<li className="list-group-item">9点 整理购书清单</li>
<li className="list-group-item">11 点外出办事</li>
<li className="list-group-item">明天上医院</li>
<li className="list-group-item">回家</li>
</ul>
</div>
<form role="form">
<input className="form-control" placeholder="你下一步打算做什么?" />
</form>
</div>
);
}
});
ReactDOM.render(
<TodoListBox />,
document.getElementById('con')
);

React中都是关于模块化、可组装的组件,我们在上面的代码构造了一个TodoListBox组件, 所谓组件,即组装起来的具有独立功能的UI部件。React推荐以组件的方式去重新思考UI构成,将UI上每一个功能相对独立的模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成大的组件,最终完成整体UI的构建。

上面的代码中,我们在一个JavaScript对象中传递一些一些方法到React.createClass()来创建一个新的React组件。这些方法中最重要的是render, 该方法返回一颗React组件树,这颗树最终将会渲染成HTML。

关于上面的代码你应该清楚知道:

  • 这个 <div> 标签不是真实的DOM节点;他们是 React div 组件的实例化。你可以把这些看做是React知道如何处理的标记或者是一些数据 。React 是安全的。我们不生成 HTML 字符串,因此XSS防护是默认特性。
  • 你没有必要返回基本的 HTML。你可以返回一个你(或者其他人)创建的组件树。这就使 React 组件化:一个可维护前端的关键原则。
  • ReactDOM.render() 实例化根组件,启动框架,注入标记到原始的 DOM 元素中,作为第二个参数提供。
  • ReactDOM.render 保持在脚本底部是很重要的。ReactDOM.render 应该只在复合组件被定义之后被调用。

第二步, 组件化思想

上面JSX代码中只有一个组件,我们应该利用组件化的思想,把一个大的组件才分开来,然后在组合起来,做到模块化,可拆卸。

var TodoList = React.createClass({
render: function () {
return (
<ul className="list-group">
<li className="list-group-item">9点 整理购书清单</li>
<li className="list-group-item">11 点外出办事</li>
<li className="list-group-item">明天上医院</li>
<li className="list-group-item">回家</li>
</ul>
);
}
});
var TodoForm = React.createClass({
render: function () {
return (
<form role="form">
<input className="form-control" placeholder="你下一步打算做什么?" />
</form>
);
}
});
var TodoBox = React.createClass({
render: function () {
return (
<div className="col-md-4">
<div className="panel panel-default">
<div className="panel-heading">TODOList - 开胃菜</div>
<TodoList />
</div>
<TodoForm />
</div>
);
}
});
ReactDOM.render(
<TodoBox />,
document.getElementById('con')
);

我们的TodoList组件还可以再拆分

var Todo = React.createClass({
render: function () {
return (
<li className="list-group-item">{this.props.children}</li>
);
}
})
var TodoList = React.createClass({
render: function () {
return (
<ul className="list-group">
<Todo id="1">9点 整理购书清单</Todo>
<Todo id="2">11 点外出办事</Todo>
<Todo id="3">明天上医院</Todo>
<Todo id="4">回家</Todo>
</ul>
);
}
});

我们创建Todo组件, 它将依赖从父级传来的数据。从父级传来的数据在子组件里作为属性可供使用。我们通过this.props来访问属性。在JSX中,通过将JavaScript表达式放在大括号中(作为属性或者子节点), 你可以把文本或者组件放置到树中。我们以this.props的keys来访问传递给组件的命名属性, this.props.children可以访问任何嵌套的元素。

例如我们传递1(通过属性id)和9点 整理购书清单给第一个Todo, 如上面提到那样,Todo组件将会通过this.props.idthis.props.children来访问这些属性。

第三步,数据模型

挂钩JSON数据

上面的代码中,我们都是直接插入TODO数据。当然我们应该每次打开页面都从服务端获取数据,作为替代,让我们渲染JSON数据到TodoList列表里。最终数据会来自服务器。

var data = [
{id: 1, text: "9点 整理购书清单"},
{id: 2, text: "11 点外出办事"},
{id: 3, text: "明天上医院"},
{id: 4, text: "回家"},
]

接下来,我们通过一种模块化的方式将这个数据传入到TodoList, 再动态渲染Todo

ReactDOM.render(
<TodoBox data={data} />,
document.getElementById('con')
);

var TodoList = React.createClass({
render: function () {
var todoNodes = this.props.data.map(function (todo) {
return (
// 这里有个问题要注意, React对dom做遍历的时候,会根据data-reactid生成
// 虚拟dom树,如果没有手动添加unique constant key的话,react是无法记录你
// 的dom操作的。它只会在重新渲染的时候,继续使用想用dom数组的序数号
// (即array[index])来对比dom树
<Todo key={todo.id}>{todo.text}</Todo>
);
})
return (
<ul className="list-group">
{todoNodes}
</ul>
);
}
});
var TodoBox = React.createClass({
render: function () {
return (
<div className="col-md-4">
<div className="panel panel-default">
<div className="panel-heading">TODOList - 开胃菜</div>
<TodoList data={this.props.data} />
</div>
<TodoForm />
</div>
);
}
});

Reactive state

迄今为止,基于它自己的props,每个组件都渲染自己一次。但是props是不可变的:它们从父级传来并被父级“拥有”。这意味着什么呢?也就是说基于props的数据只能渲染一次,当props的数据变更后(也不能变更),React并不比重新渲染数据。

为了实现交互,我们可以使用可变的state, this.state是组件私有的,当state更新,组件就重新渲染自己。我们可以通过this.setState()来更新state。

var TodoBox = React.createClass({
getInitialState: function () {
return {data: []};
},
render: function () {
return (
<div className="col-md-4">
<div className="panel panel-default">
<div className="panel-heading">TODOList - 开胃菜</div>
<TodoList data={this.state.data} />
</div>
<TodoForm />
</div>
);
}
});

getInitialState()正如其名,该方法用来设置组件的初始状态,在整个生命周期执行一次。

状态更新

接下来,我们看看如何更新状态。我们移除掉ReactDOM中传入到TodoBox的数据

ReactDOM.render(
<TodoBox />,
document.getElementById('con')
);

现在的效果图应该是这样的:

这篇文章我们并不涉及服务端的知识,所以所有关于服务端获取数据这里都只是进行模拟。我们用一个对象把数据源封装起来。

var Server = {
data: [
{id: 1, text: "9点 整理购书清单"},
{id: 2, text: "11 点外出办事"},
{id: 3, text: "明天上医院"},
{id: 4, text: "回家"},
],
getAllData: function () {
return {
'status': true,
// 这里要注意返回一个新数组, 防止传递Server对象的data属性
'data': this.data.slice()
}
}
}

我们给TodoBox组件添加一个方法

var TodoBox = React.createClass({
...
componentDidMount: function () {
// 这里应该是向服务器请求数据,我仅仅做了个模拟
var data = Server.getAllData();
if (data.status === true) {
this.setState({data: data.data.contat(data.data)});
}
},
...
});

这里, 当组件被渲染时,React会自动调用componentDidMount方法。动态更新的关键是对this.setState()的调用。从服务端获取新数据并替换掉旧的data数据,然后UI便会自动更新自己。

第四步,DOM渲染

终于到了第四步,在这一步我们来看看如何添加新的Todo清单, TodoForm应该询问用户要做的事情并发送一个请求到服务器来保存Todo清单。

事件绑定

我们用this.state来在用户输入时保存输入,因此我们初始一个state, 带有text属性

var TodoForm = React.createClass({
getInitialState: function () {
return {text: ''}
},
handleTextChange: function (e) {
this.setState({text: e.target.value});
},
render: function () {
return (
<form role="form" className="todoForm">
<input
className="form-control"
placeholder="你下一步打算做什么?"
onChange={this.handleTextChange}
/>
</form>
);
}
});

我们把input的onChange事件绑定到handleTextChange上,来实时的同步this.state.text的数据。

下面我们来看看如何处理表单提交:

var TodoForm = React.createClass({
...
handleSubmit: function (e) {
e.preventDefault();
var todo = this.state.text;
if (!todo) {
return;
}
// TODO: send request to the server
this.setState({text: ''});
},
render: function () {
return (
<form role="form" className="todoForm" onSubmit={this.handleSubmit} >
<input
className="form-control"
placeholder="你下一步打算做什么?"
value={this.state.text}
onChange={this.handleTextChange}
/>
</form>
);
}
});

我们给表单绑定了一个onSubmit事件处理器, 它在表单提交了合法数据后清空表单字段。这里还少了一部,我们如何提交我们的Todo清单并重新渲染UI呢?

首先,我们要明确一点的是,重新渲染UI意味着this.state发生了变化, 是哪一个组件的this.state应该发生变化呢?从前面我们知道, TodoBox组件拥有了Todo列表清单的状态,所以我们应当把状态的更新交给TodoBox来完成,而不是TodoList组件,更不会是TodoForm组件。

为此,我们需要提交一个Todo清单后,把数据从子组件传回到父组件,看看应该怎么做:

var TodoBox = React.createClass({
...
handleTodoSumbit: function (todo) {
// TODO: submit to server and refresh todo list
},
render: function () {
return (
<div className="col-md-4">
<div className="panel panel-default">
<div className="panel-heading">TODOList - 开胃菜</div>
<TodoList data={this.state.data} />
</div>
<TodoForm onTodoSubmit={this.handleTodoSumbit} />
</div>
);
}
});

在TodoForm组件中,我们调用通过this.props来调用父组件TodoBox传递过来的回调函数handleTodoSumbit

var TodoForm = React.createClass({
...
handleSubmit: function (e) {
e.preventDefault();
var todo = this.state.text;
if (!todo) {
return;
}
this.props.onTodoSubmit({text: text});
this.setState({text: ''});
},
...
});

提交更新

剩下来的,就剩提交数据并更新状态了, 我们来实现handleTodoSumbit方法。

var TodoBox = React.createClass({
...
handleTodoSumbit: function (todo) {
var result = Server.addOne(todo.text);
if (result.status === true) {
var data = this.state.data.concat([result.data]);
this.setState({'data': data});
}
},
...
});

Server对象也要有一个addOne的模拟操作

var Server = {
...
addOne: function (todo) {
if (!todo) {
return;
}
var id = this.data.length + 1;
this.data.push({id: id, text: todo});
return {
'status': true,
'data': {'id': id, text: todo}
}
}
}

到这里我们的应用总算的大功告成了,当然服务端需要你自己来实现。

点击查看完整DEMO代码, 这里是DEMO效果

拓展阅读

React.js 的介绍 - 针对了解 jQuery 的工程师(译)
为什么使用 React?
Why did we build React?
API

坚持原创技术分享