> 原文:http://www.infoq.com/cn/articles/react-art-of-simplity
> 作者:王沛
> **編者按**:自2013年Facebook發布以來,React吸引了越來越多的開發者,基于它的衍生技術,如React Native、React Canvas等也層出不窮。InfoQ精心策劃“深入淺出React”系列文章,為讀者剖析React開發的技術細節。
React最初來自Facebook內部的廣告系統項目,項目實施過程中前端開發遇到了巨大挑戰,代碼變得越來越臃腫且混亂不堪,難以維護。于是痛定思痛,他們決定拋開很多所謂的“最佳實踐”,重新思考前端界面的構建方式,于是就有了React。
React帶來了很多開創性的思路來構建前端界面,雖然選擇React的最重要原因之一是性能,但是相關技術背后的設計思想更值得我們去思考。之前我也曾寫過一篇React的[入門文章](http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react),并提供了示例代碼,大家可以結合參考。
**目錄**
[TOC]
上個月React發布了最新的0.13版,并提供了對ES6的支持。在新版本中,一個小小的改變是React取消了函數的自動綁定,也就是說,以前可以這樣去綁定一個事件:
~~~
<button onClick={this.handleSubmit}>Submit</button>
~~~
而在以ES6語法定義的組件中,必須寫為:
~~~
<button onClick={this.handleSubmit.bind(this)}>Submit</button>
~~~
了解前端開發和JavaScript的同學都知道,做事件綁定時我們需要通過bind(或類似函數)來實現一個閉包以讓事件處理函數自帶上下文信息,這是由JavaScript語言特性決定的。而在0.13版本之前,React會自動在初始化時對組件的每一個方法做一次這樣的綁定,類似于`this.func = this.func.bind(this)`,這樣在JSX的事件綁定中就可以直接寫為`onClick={this.handleSubmit}`。
表面上看自動綁定給開發帶來了便利,而Facebook卻[認為](http://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html)這破壞了JavaScript的語言習慣,其背后的神奇(Magic)邏輯或許會給初學者帶來困惑,甚至開發者如果從React再轉到其它庫也可能會無所適從。基于同樣的理由,React還取消了對mixin的支持,基于ES6的React組件不再能夠以mixin的形式進行代碼復用或者擴展。盡管這帶來了很大不便,但Facebook認為mixin增加了代碼的不可預測性,無法直觀的去理解。關于mixin的思考,還可以參考[這篇文章](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750)。
以簡單直觀、符合習慣的(idiomatic)方式去編程,讓代碼更容易被理解,從而易于維護和不斷演進。這正是React的設計哲學。
## 編寫可預測,符合習慣的代碼
所謂可預測(predictable),即容易理解的代碼。在年初的React開發者大會上,React項目經理Tom Occhino進一步闡述React誕生的[初衷](http://facebook.github.io/react/docs/conferences.html),在演講中提到,React最大的價值究竟是什么?是高性能虛擬DOM、服務器端Render、封裝過的事件機制、還是完善的錯誤提示信息?盡管每一點都足以重要。但他指出,其實React最有價值的是聲明式的,直觀的編程方式。
軟件工程向來不提倡用高深莫測的技巧去編程,相反,如何寫出可理解可維護的代碼才是質量和效率的關鍵。試想,一個月之后你回頭看你寫的代碼,是否一眼就明白某個變量,某個if判斷的含義;一個新加入的同事想去增加一個小小的新功能或是修復某個Bug,他是否對自己的代碼有足夠的信心不引入任何副作用?隨著功能的增加,代碼很容易變得越來越復雜,這些問題也將越來越嚴重,最終導致一份難以維護的代碼。而React號稱,新同事甚至在加入的第一天就能開始開發新功能。
那么React是如何做的呢?
## 使用JSX直觀的定義用戶界面
JSX是React的核心組成部分,它使用XML標記的方式去直接聲明界面,界面組件之間可以互相嵌套。但是JSX給人的第一印象卻是相當“丑陋”。當下面這樣的例子被第一次展示的時候,甚至很多人稱之為“巨大的退步(Huge Step Backwards)”:
~~~
var React = require('React');
var message =
<div class="hello" onClick={someFunc}>
<span>Hello World</span>
</div>;
React.renderComponent(message, document.body);
~~~
將HTML直接嵌入到JavaScript代碼中看上去確實是一件足夠瘋狂的事情。人們花了多年時間總結出的界面和業務邏輯相互分離的“最佳實踐”就這么被徹底打破。那么React為何要如此另類?
模板出現的初衷是讓非開發人員也能對界面做一定的修改。但這個初衷在當前Web程序里已完全不適用,每個模板背后的代碼邏輯嚴重依賴模板中的內容和DOM結構,兩者是緊密耦合的。即使做到文件位置的分離,實際上兩者還是一體的,并且為了兩者之間的協作而不得不引入很多機制和概念。以[Angularjs](https://angularjs.org/)的首頁示例代碼為例:
~~~
<ul class="unstyled">
<li ng-repeat="todo in todoList.todos">
<input type="checkbox" ng-model="todo.done">
<span class="done-{{todo.done}}">{{todo.text}}</span>
</li>
</ul>
~~~
盡管我們很容易看懂這一小段模板的含義,但你卻無法開始寫這樣的代碼,因為你需要學習這一整套語法。比如說,你得知道有ng-repeat這樣的標記的準確含義,其中的”todo in todoList.todos”看上去是repeat語法的一部分,或許還有其它語法存在;可以看到有{{todo.text}}這樣的數據綁定,那么如果要對這段文本格式化(加一個formatter)該怎么做;另外,ng-model背后又需要什么樣的數據結構?
現在來看React怎么寫這段邏輯:
~~~
//...
render: function () {
var lis = this.todoList.todos.map(function (todo) {
return (
<li>
<input type="checkbox" checked={todo.done}>
<span className="done-{todo.done}">{todo.text}</span>
</li>);
});
return (
<ul class="unstyled">
{lis}
</ul>
);
}
//...
~~~
可以看到,JSX中除了另類的HTML標記之外,并沒有引入其它任何新的概念(事實上HTML標記也可以[完全用JavaScript去寫](http://jsfiddle.net/reactjs/5vjqabv3/))。Angular中的repeat在這里被一個簡單的數組方法map所替代。在這里你可以利用熟悉的JavaScript語法去定義界面,在你的思維過程中其實已經不需要存在模板的概念,需要考慮的僅僅是如何用代碼構建整個界面。這種自然而直觀的方式直接降低了React的學習門檻并且讓代碼更容易理解。
## 簡化的組件模型:所謂組件,其實就是狀態機器
組件并不是一個新的概念,它意味著某個獨立功能或界面的封裝,達到復用、或是業務邏輯分離的目的。而React卻[這樣理解界面組件](http://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html):
> **所謂組件,就是狀態機器**
>
> React將用戶界面看做簡單的狀態機器。當組件處于某個狀態時,那么就輸出這個狀態對應的界面。通過這種方式,就很容易去保證界面的一致性。
>
> 在React中,你簡單的去更新某個組件的狀態,然后輸出基于新狀態的整個界面。React負責以最高效的方式去比較兩個界面并更新DOM樹。
這種組件模型簡化了我們思考的方式:對組件的管理就是對狀態的管理。不同于其它框架模型,React組件很少需要暴露組件方法和外部交互。例如,某個組件有只讀和編輯兩個狀態。一般的思路可能是提供`beginEditing()`和`endEditing()`這樣的方法來實現切換;而在React中,需要做的是`setState({editing: true/false})`。在組件的輸出邏輯中負責正確展現當前狀態。這種方式,你不需要考慮beginEditing和endEditing中應該怎樣更新UI,而只需要考慮在某個狀態下,UI是怎樣的。顯然后者更加自然和直觀。
組件是React中構建用戶界面的基本單位。它們和外界的交互除了狀態(state)之外,還有就是屬性(props)。事實上,狀態更多的是一個組件內部去自己維護,而屬性則由外部在初始化這個組件時傳遞進來(一般是組件需要管理的數據)。React認為屬性應該是只讀的,一旦賦值過去后就不應該變化。關于狀態和屬性的使用在后續文章中還會深入探討。
## 每一次界面變化都是整體刷新
數據模型驅動UI界面的兩層編程模型從概念角度看上去是直觀的,而在實際開發中卻困難重重。一個數據模型的變化可能導致分散在界面多個角落的UI同時發生變化。界面越復雜,這種數據和界面的一致性越難維護。在Facebook內部他們稱之為“Cascading Updates”,即層疊式更新,意味著UI界面之間會有一種互相依賴的關系。開發者為了維護這種依賴更新,有時不得不觸發大范圍的界面刷新,而其中很多并不真的需要。React的初衷之一就是,既然整體刷新一定能解決層疊更新的問題,那我們為什么不索性就每次都這么做呢?讓框架自身去解決哪些局部UI需要更新的問題。這聽上去非常有挑戰,但React卻做到了,實現途徑就是通過虛擬DOM(Virtual DOM)。
關于虛擬DOM的原理我在去年底的[文章](http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react)有過比較詳細的介紹,這里不再重復。簡而言之就是,UI界面是一棵DOM樹,對應的我們創建一個全局唯一的數據模型,每次數據模型有任何變化,都將整個數據模型應用到UI DOM樹上,由React來負責去更新需要更新的界面部分。事實證明,這種方式不但簡化了開發邏輯并且極大的提高了性能。
以這種思路出發,我們在考慮不斷變化的UI界面時,僅僅需要整體考慮UI的構成。編程模型的簡化帶來的是代碼的精簡和易于理解,也即React不斷提到的可預測(Predictable)的代碼,代碼的功能一目了然易于理解。Tom Occhino在2015 React開發者大會上也[分享](https://www.youtube.com/watch?v=KVZ-P-ZI6W4#t=533)了React在Facebook內部的應用案例,隨著新功能被不斷的添加到系統中,開發進度非但沒有變慢,甚至越來越快。
## 單向數據流動:Flux
既然已經有了組件機制去定義界面,那么還需要一定的機制來定義組件之間,以及組件和數據模型之間如何通信。為此,Facebook提出了Flux框架用于管理數據流。Flux是一個相當寬松的概念框架,同樣符合React簡單直觀的原則。不同于其它大多數MVC框架的雙向數據綁定,Flux提倡的是單向數據流動,即永遠只有從模型到視圖的數據流動。

Flux引入了Dispatcher和Action的概念:Dispatcher是一個全局的分發器負責接收Action,而Store可以在Dispatcher上監聽到Action并做出相應的操作。簡單的理解可以認為類似于全局的消息發布訂閱模型。Action可以來自于用戶的某個界面操作,比如點擊提交按鈕;也可以來自服務器端的某個數據更新。當數據模型發生變化時,就觸發刷新整個界面。
Flux的定義非常寬松,除了Facebook[自己的實現](https://github.com/facebook/flux)之外,社區中還出現了很多Flux的不同實現,各有特點,比較流行的包括[Flexible](http://fluxible.io/),?[Reflux](https://github.com/spoike/refluxjs),?[Flummox](https://github.com/acdlite/flummox)等等。
## 讓數據模型也變簡單:Immutability
Immutability含義是只讀數據,React提倡使用只讀數據來建立數據模型。這又是一個聽上去相當瘋狂的機制:所有數據都是只讀的,如果需要修改它,那么你只能產生一份包含新的修改的數據。假設有如下數據:
~~~
var employee = {
name: ‘John’,
age: 28
};
~~~
如果要修改年齡,那么你需要產生一份新的數據:
~~~
var updated = {
name: employee.name,
age: 29
};
~~~
這樣,原來的employee對象并沒有發生任何變化,相反,產生了一個新的updated對象,體現了年齡發生了變化。這時候需要把新的updated對象應用到界面組件上來進行界面的更新。
只讀數據并不是Facebook的全新發明,而是起源于Clojure, Scala, Haskell等函數式編程語言。只讀的數據可以讓代碼更加的安全和易于維護,你不再需要擔心數據在某個角落被某段神奇的代碼所修改;也就不必再為了找到修改的地方而苦苦調試。而結合React,只讀數據能夠讓React的組件僅僅通過比較對象引用是否相等來決定自身是否要重新Render。這在復雜的界面上可以極大的提高性能。
針對只讀數據,Facebook開發了一整套框架[immutable.js](http://facebook.github.io/immutable-js/),將只讀數據的概念引入JavaScript,并且在github開源。如果不希望一開始就引入這樣一個較大的框架,React還提供了一個工具類插件,幫助管理和操作只讀數據:[React.addons.update](http://facebook.github.io/react/docs/update.html)。
## React思想的衍生:React Native, React Canvas等等
在前幾天的Facebook F8開發者大會上,[React Native](http://facebook.github.io/react-native/)終于眾望所歸的發布,它將React的思想延伸到了原生移動開發。它的口號是“Learn Once, Write Anywhere”,有React開發經驗的開發人員將可以無縫的進行React Native開發。無論是組件化的思想,調試工具,動態代碼加載等React具有的強大特性都可以應用在React Native。相信這會對以后的移動開發布局產生重要影響。
React對UI層進行了完美的抽象,寫Web界面時甚至能夠做到完全的去DOM化:開發者可以無需進行任何DOM操作。因此,這也讓對UI層進行整體替換成為了可能。React Native正是將瀏覽器基于DOM的UI層換成了iOS或者Android的原生控件。而Flipboard則將UI層換成了Canvas。
[React Canvas](https://github.com/Flipboard/react-canvas)是Flipboard出品的一套前端框架,所有的界面元素都通過Canvas來繪制,infoQ之前也有文章對其進行了介紹。Flipboard追求極致的性能和用戶體驗,因此對瀏覽器的緩慢DOM操作深惡痛絕,不惜大刀闊斧徹底舍棄了DOM,而完全用Canvas實現了整套UI控件。有興趣的同學不妨一試。
## 小結
React并不是突然從哪里蹦出來,而是為了解決前端開發中的痛點而生。以簡單為原則設計也決定了React具有極其平緩的學習曲線,開發者可以快速上手并應用到實際項目中。本文總結分析了其相關技術背后的設計思想,希望通過這個角度能讓大家對React有一個總體的認識,從而在React的實際項目開發中,遵循簡單直觀的原則,進行高效率高質量的產品開發。
## 參考資料
1. React官方網站:[http://facebook.github.io/react/](http://facebook.github.io/react/)
2. React博客:[http://facebook.github.io/react/blog/](http://facebook.github.io/react/blog/)
3. React入門:[http://ryanclark.me/getting-started-with-react/](http://ryanclark.me/getting-started-with-react/)
4. 顛覆式前端UI框架:React:[http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react](http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react)
5. Immutable.js:?[http://facebook.github.io/immutable-js/](http://facebook.github.io/immutable-js/)
6. React Native:?[http://facebook.github.io/react-native/](http://facebook.github.io/react-native/)
7. Flux:?[https://facebook.github.io/flux/](https://facebook.github.io/flux/)
8. Flux框架對比:[https://github.com/voronianski/flux-comparison](https://github.com/voronianski/flux-comparison)
9. React開發者大會網站:[http://conf.reactjs.com/index.html](http://conf.reactjs.com/index.html)
10. React在Slack上的聊天社區:[http://reactiflux.com/](http://reactiflux.com/)
* * *
感謝[徐川](http://www.infoq.com/cn/author/%E5%BE%90%E5%B7%9D)對本文的審校。