EasyReact的簡單試用及和RAC的對比

HarrisonXi_發表於2018-08-21

原文地址:蘋果梨的部落格

美團開源了船新的響應式框架 EasyReact,GayHub地址:github.com/meituan/Eas…

作為熱愛響應式的程式猿,一定是要試用評測一下這傳說中又快又好用的新框架的,事不宜遲我們開始。(雖然這框架已經開源一個月了?)

使用 EasyReact 的 MvvmDemo

評測的具體方案是用我以前的 MvvmDemo 改造一下,舊 demo 的程式碼參照 GitHub。使用這個改造的方案,可以更方便的進行 EasyReact 和 RAC 的對比。

首先進行 EasyReact 的安裝,不得不說支援 CocoaPods 的庫安裝起來還是方便。但是 EasyReact 是沒有提供打包好的 Framework 或者對應的 Framework 工程的,這就不太方便進行一次打包多處直接使用二進位制包了。

EasyReact 優缺點
✅ 支援 CocoaPods
❌ 沒有提供二進位制 Framework

為了方便對比,我把使用 EasyReact 和 RAC 的對比做成了一個獨立的 commit f95ed50。可以看到其實從語法上來說,它們的常規使用方法十分的相似。然後我們來一點點比較細節的差異。

EZRNode vs RACSignal

RACSignal 的設計概念是表示一個可以被訂閱的訊號流,最主要的意義是表示其內部的值是變化的。而 RACSubject 是表示一個熱訊號流,熱訊號和冷訊號的內容後面再說,當前主要先要說的 RACSubject 的特徵是可以手動傳送訊號。

EZRNode 從設計上看上去更像是一個存著 value 的 model,這個使得初學者很容易理解它的用途。而 EZRMutableNode 使得 node 存著的 value 可以被修改,然後修改這個 value 的時候就會對外發出訊號。說起來我個人覺得這種設計的確可以讓程式式程式設計的開發者更容易理解和過渡到響應式程式設計中,但是有點略二不像的設計也會帶來對應的困擾。

1. 到底 EZRNode 的 value 是不是可變的

如果我們認為 EZRNode 的 value 是不可變的,那麼 EZRNode 提供 listenedBy: 就會很奇怪,一個不可變的值我們監聽它幹什麼呢?

如果我們認為 EZRNode 的 value 是可變的,那麼有些介面的設計又看上去很怪,典型的代表就是響應式程式設計最常用到的巨集定義 EZR_PATH 的實現類 EZRPathTrampoline,在其內部都預設認為 EZRMutableNode 才可以進行繫結。

我覺得從總體設計上來看,其實應該認為 EZRNode 的 value 值是可變的,EZRNode+Operation.h 中的變換都是基於 EZRNode 來實現的可以證明這一點。另一種理解是哪怕是不可變的值,其實也可以變換和監聽的嘛,這樣看起來 EZRNode 的意義和 RACSignal 其實是十分接近的。

另外 EZRPathTrampoline.m 裡面有個小細節:

- (void)setObject:(EZRNode *)node forKeyedSubscript:(NSString *)keyPath {
    NSParameterAssert(node);
    NSParameterAssert(keyPath);
    
    EZRMutableNode *keyPathNode = self[keyPath];
    [_cancelBag addCancelable:[keyPathNode syncWith:node]];
}
複製程式碼

可以看到在標頭檔案裡定義的 node 引數是 EZRMutableNode,但是類實現裡其實用的是 EZRNode,讓我不禁懷疑是不是標頭檔案裡的型別寫錯了……?

得出的第一個結論:姑且認為 EZRNode 的意義和 RACSignal 相同,是訊號的最基礎單元。

2. EZRNode 還是 EZRMutableNode

這個問題和問題1其實有點重疊,主要原因的根源還是巨集定義 EZR_PATH 的實現類 EZRPathTrampoline

對外暴露 EZRNode 類似於對外暴露一個 readonly 的屬性,使用者表面上可以感知到不可以修改其內部的 value。但是面臨的一個問題是,使用者想要使用 EZR_PATH 巨集進行繫結時還是要進行一次 mutablify 的轉換。

對外直接暴露 EZRMutableNode 的話相當於暴露了一個 readwrite 的屬性,使用者不僅可以監聽它,同時也具備了可以修改其 value 的能力,這對於維持一個 ViewModel 的封裝性來說可是個災難。

還有一點是,EZRNode 轉成 EZRMutableNode 時,複用了原先的記憶體地址。

EZRNode *node = [EZRNode new];
EZRMutableNode *mutableNode = [node mutablify];
複製程式碼

上面程式碼裡 nodemutableNode 的指標是完全相等的,當然它們的 class 也都會是 EZRMutableNode。這樣的好處就是轉換前後,它們的邏輯都是連續的;壞處是型別原地轉換的邏輯會導致使用方比較混亂(可能前一秒還是 EZRNode 的例項,下一秒就被別人變成 EZRMutableNode 了),另外 mutablify 的轉換也是不可逆的。

這樣設計應該也是沒有辦法:雖然說起來它們和 NSString & NSMutableString 組合有很多相似的地方,但是要支援 copy 協議是很麻煩的。比如想要維持監聽的鏈路不被打斷,訊號源這種東西在支援 copy 時是很容易出大問題的,要複製要維持的狀態多得難以想象。

綜上所訴,我們設計介面時到底是暴露 EZRNode 還是 EZRMutableNode 型別會有很大的困擾。相比較而言,RAC 就沒有這個困擾,不想讓別人知道這是個可以手動發訊號的 RACSubject,包裝成 RACSignal 暴露出去就好。其實我還是覺得 EasyReact 去修改下 EZRPathTrampoline 應該也可以達成類似的效果?。

不過關於 Node 可變狀態的轉換的確也沒有想到什麼好的辦法,現在的這個設計模式,即使用 readonly 式的 EZRNode 暴露介面給外界也是形同虛設,畢竟外界拿到這個 EZRNode 之後手動 mutablify 一下,然後想怎麼改就怎麼改。

3. 冷訊號熱訊號

冷訊號一直是 RAC 裡面一個讓響應式程式設計新手懵逼的概念,詳細的概念我在《RAC中的冷訊號與熱訊號》中介紹過。

既然容易讓新手懵逼,那麼 EasyReact 是怎麼處理的呢?EasyReact 裡好像就壓根沒有提供冷訊號的概念?。

這樣倒是也挺好的,讓使用者自己基於 block 和各種事件倒是也能完成類似的邏輯,省得新手在理解上有錯誤而導致寫出的程式碼有嚴重問題。

EasyReact 優缺點
✅ 易理解,拋棄了大量對初學者很晦澀的響應式概念
❌ 框架內部介面的設計對 EZRNode & EZRMutableNode 的理解貌似本身就不一致
❌ 不可變和可變 node 的無縫轉換過程可能引發其它業務方的邏輯混亂
❌ EZRNode 完全做不到 readonly 的效果,形同虛設
⚠️ 拋棄了冷訊號的概念,這個優劣參半吧

巨集定義 EZR_PATH

EZR_PATH 巨集是和 RAC 中的 RAC & RACObserve 兩個巨集相同地位的核心巨集方法,最大的不同點是它把 RAC 中的兩個巨集合併成了一個巨集。

這是個好事兒還是壞事兒呢?我個人覺得兩面都有。

// RAC
RAC(self.loginButton, enabled) = RACObserve(self.viewModel, loginEnabled);
// EasyReact
EZR_PATH(self.loginButton, enabled) = EZR_PATH(self.viewModel, loginEnabled);
複製程式碼

參照上面的程式碼示例還有 EZRPathTrampoline 的實現:

  1. 首先我覺得用一個類同時實現監聽和被監聽兩件事,從內聚性上來說講的過去,可能的確是利大於弊的。
  2. 從程式碼的閱讀和書寫上來說,書寫只要記住一個巨集,寫起來會略方便一點點,閱讀時也沒什麼障礙,畢竟一眼就可以看出來是等號左邊的表示式監聽了等號右邊的表示式。
  3. 從工程維護的大角度來說,只用一個巨集,很難區分這個巨集出現的地方是實現監聽者還是被監聽者。

第三點我們擴充開來舉個例子,用之前 MvvmDemo 裡的程式碼來看,我想要知道哪些人監聽過 ViewModel 的 username 屬性,哪些人讓 ViewModel 的 username 屬性監聽過其它訊號:

EasyReact的簡單試用及和RAC的對比

EasyReact的簡單試用及和RAC的對比

快速定位,精準無誤有木有!只用一個 EZR_PATH 巨集的話這些就無法簡單精準定位了,寫複雜的正則或許能搞定,但是也會麻煩很多。這個需要自行體會,基礎架構實現的底層模組的屬性,被監聽和監聽其它屬性的訊號流多如牛毛,能讓定位的複雜度降低是提高工作效率的重要保證。

EasyReact 優缺點
⚠️ EZR_PATH 巨集易用、二合一,但是也導致難以區分是實現監聽者還是被監聽者

對系統類的擴充套件

基於剛剛提到的 commit f95ed50 的 MvvmDemo 是不完整的,一個很重要的原因就是 UITextField 這類 UI 控制元件,是不可以通過監聽它的 text 屬性就能簡單實現響應式的。所以我們必須要一個新的 commit fff9624,來把 UITextField 依然通過 delegate 的方式連結到 ViewModel 上,說起來就是還是拋棄不了過程式的開發方法。

這點我相信美團內部應該還是有對應的一些封裝吧,日後或許也會漸漸開源出來。畢竟如果一套響應式框架如果沒有辦法很便捷的應用到業務層的 UI 上,實用性就會大打折扣。

相比較來說沉澱了多年的 RAC 強大的多,不光連 UI 控制元件的擴充套件封裝很完備,還為了具體的場景需要實現了 RACCommand 和 RACChannel 等類,甚至於連 UserDefaults 都做了對應的擴充套件封裝。

EasyReact 優缺點
❌ 沒有對系統類的擴充套件,易用性大打折扣

效能對比

美團官博寫著的 EasyReact 還有一個最大的亮點就是效能起飛!不過當然要實踐出真知,不能盲目的相信當事人自己的資料。基於上面的 MvvmDemo,我來自己做一個簡單的效能對比試驗一下:

- (void)testPerformance {
    [self measureBlock:^{
        TestObject *object = [TestObject new];
        ViewModel *viewModel = [ViewModel new];
        NSArray *array = @[@"0", @"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9"];
        EZR_PATH(viewModel, username) = EZR_PATH(object, username);
        EZR_PATH(viewModel, password) = EZR_PATH(object, password);
        EZR_PATH(object, usernameColor) = ConvertInputStateToColor(EZR_PATH(viewModel, usernameInputState));
        EZR_PATH(object, passwordColor) = ConvertInputStateToColor(EZR_PATH(viewModel, passwordInputState));
        EZR_PATH(object, loginEnabled) = EZR_PATH(viewModel, loginEnabled);
        for (int i = 0; i < 1000; i++) {
            object.username = array[i % 10];
            object.password = array[i % 10];
        }
    }];
}
複製程式碼

如上單元測試,在 RAC 的分支和 EasyReact 的分支各實現一次,執行完了之後對比總耗時:

EasyReact的簡單試用及和RAC的對比

可以看到,在綜合了 combine、listen、map 等操作的實驗下,EasyReact 的效率在 RAC 的三倍以上。

EasyReact 優缺點
✅✅ EasyReact 的效率在 RAC 的三倍以上

除錯複雜度

EasyReact 的 EZRNode 概念比起 RACSignal 來說,是確確實實的持有了一個 value 的,所以除錯起來有相當大的優勢。舉幾個例子:

  1. 可以給 setValue: 加個斷點,設定進來的新值和之前的舊值都可以輕鬆獲得。
  2. 任何時機可以方便的直接用 .value 拿到現在的節點值,按照文件描述這個值是執行緒安全的,放心使用。
  3. 堆疊的深度上及呼叫的邏輯上看上去可能會更簡單。

還有很多其它的可能性,不過這裡先展開說一下第3點。下面是同一個單元測試,EasyReact 下的堆疊狀態:

EasyReact的簡單試用及和RAC的對比

對應的 RAC 下的堆疊狀態:

EasyReact的簡單試用及和RAC的對比

……

EasyReact的簡單試用及和RAC的對比

堆疊長度從22激增到了52(這也是 RAC 效率低一些的重要原因吧?)。

倒是如果把程式碼隱藏起來(非程式碼展開,直接使用打包好的 Framework),其實 RAC 的堆疊也會比較清晰:

EasyReact的簡單試用及和RAC的對比

可以看到雖然堆疊長度還是很大,但是層級上只展示了幾個關鍵層。

這種訊號流除錯起來,說起來誰更方便些真的沒有定論,因為畢竟都很麻煩?。加上跨執行緒呼叫的情況,更是難上加難,所以我在這裡也就不硬比個高低了。倒是總體說起來 EasyReact 概念簡單,設計也簡單,應該除錯難度肯定會更低一些的。

EasyReact 優缺點
✅ EasyReact 除錯難度更低

文件 & 社群

文件也是重要的一點,這裡長話短說了。

RAC 的文件一直是比較差的,這麼難的框架還只能靠自己還有零散的博文來啃,的確有些吃力。看 RAC 各種複雜高階變換時,很多時候是藉助 ReactiveX 框架的示意圖(例如這個 zip 的示意圖)來理解的,這些示意圖很好很強大,學習響應式的朋友也可以去觀摩學習下。

EasyReact 的設計比較簡單,文件相對來說就好理解些,而且官方中文文件這點對於國人開發者來說太友好了!

社群的活躍度這點目前就不清楚會怎樣了,國內的社群氛圍一直比較差,還不清楚遇到具體的問題時,美團官方的跟進及各大社群的討論會如何,只能說不抱很大期望。

EasyReact 優缺點
✅ 文件齊全,官方中文

總結

老實說光效能這一點,EasyReact 就值得推薦。對於學習響應式框架的初學者來說,EasyReact 是可以嘗試的,整體來說它的概念更簡單。但是就完備程度來說,EasyReact 還有一段很長的路要走,對 RAC 熟悉程度比較高的程式猿,開發效率肯定還是更高的。所以說從開發效率、執行效能和學習成本等各方面考慮,選擇適合你們自己團隊的響應式框架吧。

相關文章