編者按:自2013年Facebook發布以來,React吸引了越來越多的開發者,基於它的衍生技術,如React Native、React Canvas等也層出不窮。InfoQ精心策划“深入淺出React”系列文章,為讀者剖析React開發的技術細節。
React最初來自Facebook內部的廣告系統項目,項目實施過程中前端開發遇到了巨大挑戰,代碼變得越來越臃腫且混亂不堪,難以維護。於是痛定思痛,他們決定拋開很多所謂的“最佳實踐”,重新思考前端界面的構建方式,於是就有了React。
React帶來了很多開創性的思路來構建前端界面,雖然選擇React的最重要原因之一是性能,但是相關技術背后的設計思想更值得我們去思考。之前我也曾寫過一篇React的入門文章,並提供了示例代碼,大家可以結合參考。
上個月React發布了最新的0.13版,並提供了對ES6的支持。在新版本中,一個小小的改變是React取消了函數的自動綁定,也就是說,以前可以這樣去綁定一個事件:
而在以ES6語法定義的組件中,必須寫為:
了解前端開發和JavaScript的同學都知道,做事件綁定時我們需要通過bind(或類似函數)來實現一個閉包以讓事件處理函數自帶上下文信息,這是由JavaScript語言特性決定的。而在0.13版本之前,React會自動在初始化時對組件的每一個方法做一次這樣的綁定,類似於this.func = this.func.bind(this),這樣在JSX的事件綁定中就可以直接寫為onClick={this.handleSubmit}。
表面上看自動綁定給開發帶來了便利,而Facebook卻認為這破壞了JavaScript的語言習慣,其背后的神奇(Magic)邏輯或許會給初學者帶來困惑,甚至開發者如果從React再轉到其它庫也可能會無所適從。基於同樣的理由,React還取消了對mixin的支持,基於ES6的React組件不再能夠以mixin的形式進行代碼復用或者擴展。盡管這帶來了很大不便,但Facebook認為mixin增加了代碼的不可預測性,無法直觀的去理解。關於mixin的思考,還可以參考這篇文章。
以簡單直觀、符合習慣的(idiomatic)方式去編程,讓代碼更容易被理解,從而易於維護和不斷演進。這正是React的設計哲學。
編寫可預測,符合習慣的代碼
所謂可預測(predictable),即容易理解的代碼。在年初的React開發者大會上,React項目經理Tom Occhino進一步闡述React誕生的初衷,在演講中提到,React最大的價值究竟是什么?是高性能虛擬DOM、服務器端Render、封裝過的事件機制、還是完善的錯誤提示信息?盡管每一點都足以重要。但他指出,其實React最有價值的是聲明式的,直觀的編程方式。
軟件工程向來不提倡用高深莫測的技巧去編程,相反,如何寫出可理解可維護的代碼才是質量和效率的關鍵。試想,一個月之后你回頭看你寫的代碼,是否一眼就明白某個變量,某個if判斷的含義;一個新加入的同事想去增加一個小小的新功能或是修復某個Bug,他是否對自己的代碼有足夠的信心不引入任何副作用?隨着功能的增加,代碼很容易變得越來越復雜,這些問題也將越來越嚴重,最終導致一份難以維護的代碼。而React號稱,新同事甚至在加入的第一天就能開始開發新功能。
那么React是如何做的呢?
使用JSX直觀的定義用戶界面
JSX是React的核心組成部分,它使用XML標記的方式去直接聲明界面,界面組件之間可以互相嵌套。但是JSX給人的第一印象卻是相當“丑陋”。當下面這樣的例子被第一次展示的時候,甚至很多人稱之為“巨大的退步(Huge Step Backwards)”:
var React = require(‘React’);
var message =
模板出現的初衷是讓非開發人員也能對界面做一定的修改。但這個初衷在當前Web程序里已完全不適用,每個模板背后的代碼邏輯嚴重依賴模板中的內容和DOM結構,兩者是緊密耦合的。即使做到文件位置的分離,實際上兩者還是一體的,並且為了兩者之間的協作而不得不引入很多機制和概念。以Angularjs的首頁示例代碼為例:
- {{todo.text}}
現在來看React怎么寫這段邏輯:
//...
render: function () {
var lis = this.todoList.todos.map(function (todo) {
return (
{todo.text}
});
return (
{lis}
);
}
//...
可以看到,JSX中除了另類的HTML標記之外,並沒有引入其它任何新的概念(事實上HTML標記也可以完全用JavaScript去寫)。Angular中的repeat在這里被一個簡單的數組方法map所替代。在這里你可以利用熟悉的JavaScript語法去定義界面,在你的思維過程中其實已經不需要存在模板的概念,需要考慮的僅僅是如何用代碼構建整個界面。這種自然而直觀的方式直接降低了React的學習門檻並且讓代碼更容易理解。
簡化的組件模型:所謂組件,其實就是狀態機器
組件並不是一個新的概念,它意味着某個獨立功能或界面的封裝,達到復用、或是業務邏輯分離的目的。而React卻這樣理解界面組件:
所謂組件,就是狀態機器
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的原理我在去年底的文章有過比較詳細的介紹,這里不再重復。簡而言之就是,UI界面是一棵DOM樹,對應的我們創建一個全局唯一的數據模型,每次數據模型有任何變化,都將整個數據模型應用到UI DOM樹上,由React來負責去更新需要更新的界面部分。事實證明,這種方式不但簡化了開發邏輯並且極大的提高了性能。
以這種思路出發,我們在考慮不斷變化的UI界面時,僅僅需要整體考慮UI的構成。編程模型的簡化帶來的是代碼的精簡和易於理解,也即React不斷提到的可預測(Predictable)的代碼,代碼的功能一目了然易於理解。Tom Occhino在2015 React開發者大會上也分享了React在Facebook內部的應用案例,隨着新功能被不斷的添加到系統中,開發進度非但沒有變慢,甚至越來越快。
單向數據流動:Flux
既然已經有了組件機制去定義界面,那么還需要一定的機制來定義組件之間,以及組件和數據模型之間如何通信。為此,Facebook提出了Flux框架用於管理數據流。Flux是一個相當寬松的概念框架,同樣符合React簡單直觀的原則。不同於其它大多數MVC框架的雙向數據綁定,Flux提倡的是單向數據流動,即永遠只有從模型到視圖的數據流動。
Flux引入了Dispatcher和Action的概念:Dispatcher是一個全局的分發器負責接收Action,而Store可以在Dispatcher上監聽到Action並做出相應的操作。簡單的理解可以認為類似於全局的消息發布訂閱模型。Action可以來自於用戶的某個界面操作,比如點擊提交按鈕;也可以來自服務器端的某個數據更新。當數據模型發生變化時,就觸發刷新整個界面。
Flux的定義非常寬松,除了Facebook自己的實現之外,社區中還出現了很多Flux的不同實現,各有特點,比較流行的包括Flexible, Reflux, 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,將只讀數據的概念引入JavaScript,並且在github開源。如果不希望一開始就引入這樣一個較大的框架,React還提供了一個工具類插件,幫助管理和操作只讀數據:React.addons.update。
React思想的衍生:React Native, React Canvas等等
在前幾天的Facebook F8開發者大會上,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是Flipboard出品的一套前端框架,所有的界面元素都通過Canvas來繪制,infoQ之前也有文章對其進行了介紹。Flipboard追求極致的性能和用戶體驗,因此對瀏覽器的緩慢DOM操作深惡痛絕,不惜大刀闊斧徹底舍棄了DOM,而完全用Canvas實現了整套UI控件。有興趣的同學不妨一試。
小結
React並不是突然從哪里蹦出來,而是為了解決前端開發中的痛點而生。以簡單為原則設計也決定了React具有極其平緩的學習曲線,開發者可以快速上手並應用到實際項目中。本文總結分析了其相關技術背后的設計思想,希望通過這個角度能讓大家對React有一個總體的認識,從而在React的實際項目開發中,遵循簡單直觀的原則,進行高效率高質量的產品開發。
參考資料
React官方網站:http://facebook.github.io/react/
React博客:http://facebook.github.io/react/blog/
React入門:http://ryanclark.me/getting-started-with-react/
顛覆式前端UI框架:React:http://www.infoq.com/cn/articles/subversion-front-end-ui-development-framework-react
Immutable.js: http://facebook.github.io/immutable-js/
React Native: http://facebook.github.io/react-native/
Flux: https://facebook.github.io/flux/
Flux框架對比:https://github.com/voronianski/flux-comparison
React開發者大會網站:http://conf.reactjs.com/index.html
React在Slack上的聊天社區:http://reactiflux.com/