標籤:

最近的一次討論記錄

上周 D2,遇到了天翔,聊了一會,談了一些看法,在這裡記錄一下大致的討論。

問:聽叔叔安利了 RxJS 做狀態管理之後,嘗試用了一段時間,總的感覺還是比較好的,但有時候還是會覺得比較彆扭,這是為什麼呢?

答:可能的原因有這麼兩點:

1. 強行把不同方向的管道流融合在一起

2. 太強調把一次性的非同步操作與管道流統一在一起

問:什麼叫不同方向的管道流?

答:打個比方,如果說有一個遠程查詢的請求,視圖要訂閱這個結果,步驟是這樣的:

視圖 -> 發起請求 -> |n 遠程n渲染 <- 得到結果 <- |n

從上面這個示意圖中,我們可以明顯看到存在兩個方向,如果不考慮其他更新的匯入來源,這個表達式是比較容易寫出來的。但是,考慮到視圖渲染可能是多個流訂閱來的,匯入過程很可能在兩個不同的地方:

- 遠程請求得到結果之前

- 遠程請求得到結果之後

注意這裡,得到結果只是一個請求,得到數據之後不就更新視圖了嗎,那麼,這個「之前」和「之後」是什麼含義?

我們考慮的是這個數據,它可能是單一業務數據,可能是複合數據;可能是藉助這個結果只渲染一個地方,也可能是多個地方,所以,你可能遇到的比較彆扭的地方在於:

- 從視圖往遠程請求的這個方向,訂閱關係是一條直線

- 從遠程請求回來的這個方向,訂閱關係可能是個網

如果在這個情況下,非要把它們鏈式寫成一個流,往往帶來可維護性的負擔。可以考慮把它斷成兩節,上一半只管寫,下一半只管讀。從寫的方向看,一條線到底,拿到結果,無論正常、異常,是否有 pending,都一起寫入下面這些流的起始點;從讀的方向看,對於任意一個起點,都可能看得到它往視圖方向的整個樹,從視圖方向看,任何一個視圖的組合訂閱,又都可以看得到往數據層的整個訂閱樹。雖然這是個網,但看上去也是非常清晰的,可維護性也比較好。

問:那麼,一次性和管道流的含義又分別是什麼?

所謂一次性,是指非可重複的事情,比如單次的網路請求,通常,你會用 Promise 表達,但對於一個掌握了 RxJS 基本用法的人來說,身懷利器,殺心自起,你會傾向於把原先用 Promise 表達的東西全部換成 RxJS,比如一個簡單的 xxx.then,你就要想把它變成 mergeMap,其實這是不必要的。

Future 和 Stream,沒有必要在中間步驟上做融合。如果你有一個多級的不涉及外部狀態變更的連續請求,應當先用 Promise 搞成串,然後整體與流結合,不需要把串裡面的每一段都用流來表達。

有一個簡單的辦法控制內心慾望,那就是不讓代碼中出現 Promise,儘可能不要 then,而是用 await 表達這些東西,封裝成 async 函數,這樣,整體與 Stream 結合就不會有違和感。

總的來說,如果你的目的只是用 RxJS 來做數據流管理,那其實可以只用裡面幾個比較常用的操作符,適當克制自己的慾望,這樣整個代碼的可維護性會比較好。

問:這個讀寫分離的機制,有些像 Redux 了,那為什麼叔叔一直對 Redux 這麼深惡痛絕呢?

答:幾個原因:

1. 代碼信噪比太低

2. 非同步中間件的實現機制是比較彆扭的

3. 與 React 的結合機制,其實破壞了 React 本身的一些東西。

問:前兩點很好理解,上次吉姆的知乎回答里也提到了,第三點怎麼理解呢?

答:我們需要注意到,在不存在 Redux 的時候,React 本身是有兩個特點的:

1. 整個體系是以視圖為主體的,一切以視圖為重,數據邏輯其實依附在組件生命周期中

2. 架構是分形的:從組件樹上取下任意一個分支,都還是可以運行的,而如果把頂級組件再次包裝,它整個又成為了別人的分支。

但是在 React-Redux 體系中,這兩點都被破壞了。

外置的數據層,尤其是單一 Store,其實對 React 的架構破壞性非常大,因為這是一個很明顯的 MDV 結構了,之前的 M 不明顯,現在可就不一樣了。當你在一個明顯存在 M 的場景下,你潛意識就會對 M 精心維護,然後會發現,數據不再是整個架構中的從屬部分,而是一個源頭,或者說主體,視圖反而變成從屬了。

當然,我不是說這種架構不正確,不合理,而是說,有沒有 Redux,對整個 React 應用的結構是有較大影響的。對於 React 這麼一個本身是重視圖的初始設計而言,這種做法很容易搞得頭重腳輕。比如說,你會經常看到有人希望把一切狀態、包括組件內部狀態都整合到 Redux 去,把視圖極度輕量化,這就是另外一個極端了。

此外,這個架構變成不分形了,也是一個彆扭的地方,一個樹對一個平級結構的依賴,除非這個平級結構隱含了樹形關係,否則就必然是不分形的。不分形,當我們遇到架構調整,比如說多應用整合,每個都要下沉一級,然後搞一個總的菜單,類似阿里雲多控制台的統一入口那樣,就會比較尷尬。

問:那麼,理想的框架應該怎麼做才比較合適呢?

答:如果你理解了 CycleJS,並且入門了 RxJS 或者 xstream 這類流式庫,那麼,你很大概率會不認同 React-Redux 體系。

首先,無論出現什麼情況,保持架構的分形都是一個比較好的方式。

其次,如果你期望把數據與視圖徹底隔離,最好一開始就把數據提到比較重要的地位去,把視圖降低為附庸。

我們來看 CycleJS 的一個組件,它的數據輸入、事件定義都完全隔離在視圖之外,視圖只是最基本渲染,這個分離就非常自然,相比來說,React 的這些問題,都會使得社區存在非常多的流派,並且,遜尼派和什葉派之間的互不認同感可能比對其他框架還強。所以,如果說真的存在面向未來的框架,那也至少是 CycleJS 這樣的,而不是 React。

問:那麼,其他框架也會有分形的問題嗎?

答:是的,會有。

比如說 Vue,它的情況跟 React 類似,都是最初只面向組件化的視圖層框架,然而,後期也引入了 VueX 這樣的東西,我個人從未評價過 VueX,但實際上認為這個東西的存在,能解決不少工程問題,但也讓整個架構有點不那麼和諧。

反倒是 Angular,甚至 AngularJS,這方面還稍好。

問:為什麼 AngularJS 這方面還稍好呢?它不是有一些什麼 Service 之類的東西嗎?

答:我作這個論斷,主要是兩個依據:

1. 組件生命周期方法未以約定方式存在於組件主體中,導致組件的主體是比較純凈的,如果你只把模板當作視圖,那這時候,其實要比另外一些框架里,組件聲明周期那麼明確地表露出來要自然一些。

比如說:

<div ng-init="xxx()">

而不是在 controller 中,弄一個默認的約定:

onInit() {

}

我個人認為,組件中生命周期方法與普通的業務方法混在同一個 Class 中,是不太合適的,最好能分開。

2. 在 AngularJS 中的 Service,其實並未提倡把「狀態」分離出來,大部分情況下只是作為類似公共函數那樣的東西,並不持有狀態,狀態的持有是在 Controller 中的。所以它並不破壞視圖的分形。

問:可是,AngularJS 並未那麼明顯地凸顯組件,它的 Controller 應該如何理解呢?

答:我們把它轉換一種形式:

@tpl(tplStr)

class SomeController {

foo() {}

bar() {}

}

你再看看?

問:這不是 Angular 的寫法嗎,AngularJS 好像不是這麼寫的啊?

答:寫法只是形式,實際上它是可以轉換成這種寫法的,比如這個代碼:github.com/teambition/t

如果你注意看最下面那句,你就不會懷疑它是不是 AngularJS。

問:這樣看來,好像是理解了,幾個框架深層次居然有這麼多的一致性。

答:沒錯,如果一個人深刻理解了這些,他是有機會在不同框架之間保留儘可能多的邏輯代碼,從而使得遷移代價盡量小的。AngularJS 的主要問題在另外一些方面,設計是一個問題,另外還有一些東西,比如工具鏈路的重視程度就不如 React 體系,不太敢於在官方實踐中引入這類 Decorator 的便利寫法,或者 TypeScript 這類語言支持。

不過,當時那個時候,大家也不容易理解和支持這些,你現在可能覺得 Decorator 挺好的語言特性啊,但那時候,不知道有多少人吐槽 Angular 2 的這個設計了。

問:為什麼不認同組件的生命周期方法跟其他業務方法混合的方式,期望把它們分開呢?

答:我舉個例子吧。前幾天,公司群里有個討論很有意思:

React 代碼中,有人寫:

async componentWillMount () {}

這麼一句代碼引發了一段討論,有人就認為這個 async 會影響這個生命周期方法的結果,而實際上,這個 async 只是為了讓你能在這個方法體內寫 await 語句,並沒有別的什麼作用。

組件的生命周期方法往往帶有很多隱藏含義,業務方法跟他混合的時候,有時候比較難區分,如果生命周期方法的名字過於平凡,不是一個明顯能區分的方法名,很可能就在某些時候造成業務開發人員的困擾了。

問:可不可以這麼理解,基於 RxJS 這樣的庫,可以直接實現一個框架?為什麼 CycleJS 不把 Stream 直接拆到小粒度數據,而是要整體扔給 vdom?為什麼 Vue 底層也構建了一層訂閱關係,但是用了 getter, setter 的方式,沒有用看起來更優雅的 Stream,Stream 不是還能兼容非同步嗎?

答:不這麼做的主要原因在於性能,恰恰就因為 Stream 要兼容非同步。注意,並不是因為非同步導致性能不好,而是你的每個東西,因為要同時兼容同步和非同步,都會要用非同步的方式來處理,這個包裝過程是會明顯變慢的,當在框架底層這麼去實現的時候,就可能把這種變慢的東西放大了很多倍,達到不可接受的地步了。

所以,剛才例子中的那個 componentWillMount,它的實現就不太可能兼容非同步。同理,為什麼 React 的 setState 寧可接受回調作第二個參數,也不變成 Promise 方法,就是這樣的考慮。所以,Vue 對數據處理的方式,是在外層推,組件內部拉,這是一個折中方案,也是比較合適的。

注意:我們不能接受組件框架原生非同步,但是,是可以接受數據流原生非同步的,因為後者承載的東西,很難出現很多倍的放大,你做網路請求之類的事情,本身就不會期望它立刻返回,適當損失有些性能,心裡是有所預期的,在這個部分,更關注的會是代碼組織的優雅。

問:那麼,前端業務邏輯這塊,函數式不能特別普及的原因是什麼?

答:主要是幾點:

1. 函數式的抽象代價比較大

2. 中國的大學教育基本上很少有灌輸 SICP 這些概念的

3. JavaScript 語言本身不是函數式語言,只是支持部分函數式特性

所以,很函數式的業務代碼做不到「無腦寫,無腦讀」,門檻就必然高,也正是因為這個,會影響一些框架的流行度。像 React 這樣,能這麼流行已經很不錯了,主要是佔了工程體系的光,不然還達不到這樣。而在國內這麼環境下,不那麼函數式的東西自然會更流行,人民群眾有他喜聞樂見的東西,各有各的好。

問:那如何看待 TypeScript 的逐漸流行呢?

答: 實際上,這是幾個不相關方面導致的吧:

- 一些從 Java 等方面轉來的人,誤以為這是一種跟 Java 差不多的 OOP 語言,感到親切

- 一些函數式的人,因為期望類型的推導

- 一些人希望能夠在工程角度增加一些穩定性

可以看到,對它的誤解還是比較多,比如說,有些聲稱喜歡函數式的人,說自己厭惡 TS 是因為它的 OOP 傾向,實際上類型在函數式裡面才是更重要的東西,而類型跟 OOP 根本就是毫無關係的事情。我也見過一些只用過 JS 就聲稱自己喜歡函數式編程的人,或者認識不到 Stream 也是一種重要的函數式理念,其實沒必要黑,畢竟一山還有一山高,我自己也還徘徊在門口,不知道入門沒有呢。


推薦閱讀:

Nuxt.js 實戰
前端測試框架 Jest

TAG:前端框架 |