React 開胃菜

背景
基于HTML的前端界面開發正變得越來越復雜,其本質問題基本可以歸結于如何將來自服務器端或者用戶輸入的動態數據高效的反應到復雜的用戶界面上。
概述
React 是一個非死book和Instagram用來創建用戶界面的JavaScript庫。那么非死book開發這個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>
效果圖

第一步,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代碼。
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.id 和 this.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}
}
}
}
到這里我們的應用總算的大功告成了,當然服務端需要你自己來實現。
來自:http://youbookee.com/2016/10/09/react-appetizer/