深入淺出React(一):React的設計哲學 – 簡單之美

  • 2019 年 12 月 4 日
  • 筆記

本文作者:IMWeb 劉起 原文出處:IMWeb社區 未經同意,禁止轉載

編者按:自2013年Facebook發布以來,React吸引了越來越多的開發者,基於它的衍生技術,如React Native、React Canvas等也層出不窮。InfoQ精心策劃「深入淺出React」系列文章,為讀者剖析React開發的技術細節。

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(&#x2018;React&#x2019;);  var message =    <div class=&#x201C;hello&#x201D; 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: &#x2018;John&#x2019;,    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的實際項目開發中,遵循簡單直觀的原則,進行高效率高品質的產品開發。