標籤:
React最初來自Facebook內部的廣告系統項目,項目實施過程中前端開發遇到了巨大挑戰,代碼變得越來越臃腫且混亂不堪,難以維護。於是痛定思痛,他們決定拋開很多所謂的“最佳實務”,重新思考前端介面的構建方式,於是就有了React。
React帶來了很多開創性的思路來構建前端介面,雖然選擇React的最重要原因之一是效能,但是相關技術背後的設計思想更值得我們去思考。之前我也曾寫過一篇React的入門文章,並提供了範例程式碼,大家可以結合參考。
上個月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卻認為這破壞了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 = <div class=“hello” onClick={someFunc}> <span>Hello World</span> </div>;React.renderComponent(message, document.body);
將HTML直接嵌入到JavaScript代碼中看上去確實是一件足夠瘋狂的事情。人們花了多年時間總結出的介面和商務邏輯相互分離的“最佳實務”就這麼被徹底打破。那麼React為何要如此另類?
模板出現的初衷是讓非開發人員也能對介面做一定的修改。但這個初衷在當前Web程式裡已完全不適用,每個模板背後的代碼邏輯嚴重依賴模板中的內容和DOM結構,兩者是緊密耦合的。即使做到檔案位置的分離,實際上兩者還是一體的,並且為了兩者之間的協作而不得不引入很多機制和概念。以Angularjs的首頁範例程式碼為例:
<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去寫)。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/
React的設計哲學 - 簡單之美