React Native 從入門到原理

bestswifter發表於2016-06-12

React Native 是最近非常火的一個話題,介紹如何利用 React Native 進行開發的文章和書籍多如牛毛,但面向入門水平並介紹它工作原理的文章卻寥寥無幾。

本文分為兩個部分:上半部分用通俗的語言解釋了相關的名詞,重點介紹 React Native 出現的背景和試圖解決的問題。適合新手對 React Native 形成初步瞭解。(事實證明,女票能看懂這段)

下半部分則通過原始碼(0.27 版本)分析 React Native 的工作原理,適合深入學習理解 React Native 的執行機制。最後則是我個人對 React Native 的分析與前景判斷。

動態配置

由於 AppStore 稽核週期的限制,如何動態的更改 app 成為了永恆的話題。無論採用何種方式,我們的流程總是可以歸結為以下三部曲:“從 Server 獲取配置 –> 解析 –> 執行native程式碼”。

很多時候,我們自覺或者不自覺的利用 JSON 檔案實現動態配置的效果,它的核心流程是:

  1. 通過 HTTP 請求獲取 JSON 格式的配置檔案。
  2. 配置檔案中標記了每一個元素的屬性,比如位置,顏色,圖片 URL 等。
  3. 解析完 JSON 後,我們呼叫 Objective-C 的程式碼,完成 UI 控制元件的渲染。

通過這種方法,我們實現了在後臺配置 app 的展示樣式。從本質上來說,移動端和服務端約定了一套協議,但是協議內容嚴重依賴於應用內要展示的內容,不利於擴充。也就是說,如果業務要求頻繁的增加或修改頁面,這套協議很難應付。

最重要的是,JSON 只是一種資料交換的格式,說白了,我們就是在解析文字資料。這就意味著它只適合提供一些配置資訊,而不方便提供邏輯資訊。舉個例子,我們從後臺可以配置顏色,位置等資訊,但如果想要控制 app 內的業務邏輯,就非常複雜了。

記住,我們只是在解析字串,它完全不具備執行和除錯的能力。

React

不妨暫時拋棄移動端的煩惱,來看看前端的“新玩意”。

背景

作為前端小白,我以前對前端的理解是這樣的:

  • 用 HTML 建立 DOM,構建整個網頁的佈局、結構
  • 用 CSS 控制 DOM 的樣式,比如字型、字號、顏色、居中等
  • 用 JavaScript 接受使用者事件,動態的操控 DOM

在這三者的配合下,幾乎所有頁面上的功能都能實現。但也有比較不爽地方,比如我想動態修改一個按鈕的文字,我需要這樣寫:

然後在 JavaScript 中操作 DOM:

可以看到,在 HTML 和 JavaScript 程式碼中,idonclick 事件觸發的函式必須完全對應,否則就無法正確的響應事件。如果想知道一個 HTML 標籤會如何被響應,我們還得跑去 JavaScript 程式碼中查詢,這種原始的配置方式讓我覺得非常不爽。

初識 React

隨著 FaceBook 推出了 React 框架,這個問題得到了大幅度改善。我們可以把一組相關的 HTML 標籤,也就是 app 內的 UI 控制元件,封裝進一個元件(Component)中,我從阮一峰的 React 教程中摘錄了一段程式碼:

如果你想問:“為什麼 JavaScript 程式碼裡面出現了 HTML 的語法”,那麼恭喜你已經初步體會到 React 的奧妙了。這種語法被稱為 JSX,它是一種 JavaScript 語法擴充。JSX 允許我們寫 HTML 標籤或 React 標籤,它們終將被轉換成原生的 JavaScript 並建立 DOM。

在 React 框架中,除了可以用 JavaScript 寫 HTML 以外,我們甚至可以寫 CSS,這在後面的例子中可以看到。

理解 React

前端界總是喜歡創造新的概念,彷彿誰說的名詞更晦澀,誰的水平就越高。如果你和當時的我一樣,聽到 React 這個概念一臉懵逼的話,只要記住以下定義即可:

React 是一套可以用簡潔的語法高效繪製 DOM 的框架

上文已經解釋過了何謂“簡潔的語法”,因為我們可以暫時放下 HTML 和 CSS,只關心如何用 JavaScript 構造頁面。

所謂的“高效”,是因為 React 獨創了 Virtual DOM 機制。Virtual DOM 是一個存在於記憶體中的 JavaScript 物件,它與 DOM 是一一對應的關係,也就是說只要有 Virtual DOM,我們就能渲染出 DOM。

當介面發生變化時,得益於高效的 DOM Diff 演算法,我們能夠知道 Virtual DOM 的變化,從而高效的改動 DOM,避免了重新繪製 DOM。

當然,React 並不是前端開發的全部。從之前的描述也能看出,它專注於 UI 部分,對應到 MVC 結構中就是 View 層。要想實現完整的 MVC 架構,還需要 Model 和 Controller 的結構。在前端開發時,我們可以採用 Flux 和 Redux 架構,它們並非框架(Library),而是和 MVC 一樣都是一種架構設計(Architecture)。

如果不從事前端開發,就不用深入的掌握 Flux 和 Redux 架構,但理解這一套體系結構對於後面理解 React Native 非常重要

React Native

分別介紹完了移動端和前端的背景知識後,本文的主角——React Native 終於要登場了。

融合

前面我們介紹了移動端通過 JSON 檔案傳遞資訊的不足之處:只能傳遞配置資訊,無法表達邏輯。從本質上講,這是因為 JSON 畢竟只是純文字,它缺乏像程式語言那樣的執行能力。

而 React 在前端取得突破性成功以後,JavaScript 佈道者們開始試圖一統三端。他們利用了移動平臺能夠執行 JavaScript 程式碼的能力,並且發揮了 JavaScript 不僅僅可以傳遞配置資訊,還可以表達邏輯資訊的優點。

當痛點遇上特點,兩者一拍即合,於是乎:

一個基於 JavaScript,具備動態配置能力,面向前端開發者的移動端開發框架,React Native,誕生了!

看到了麼,這是一個面向前端開發者的框架。它的宗旨是讓前端開發者像用 React 寫網頁那樣,用 React Native 寫移動端應用。這就是為什麼 React Native 自稱:

Learn once,Write anywhere!

而非很多跨平臺語言,專案所說的:

Write once, Run anywhere!

React Native 希望前端開發者學習完 React 後,能夠用同樣的語法、工具等,分別開發安卓和 iOS 平臺的應用並且不用一行原生程式碼。

如果用一個詞概括 React Native,那就是:Native 版本的 React

原理概述

React Native 不是黑科技,我們寫的程式碼總是以一種非常合理,可以解釋的方式的執行著,只是絕大多數人沒有理解而已。接下來我以 iOS 平臺為例,簡單的解釋一下 React Native 的原理。

首先要明白的一點是,即使使用了 React Native,我們依然需要 UIKit 等框架,呼叫的是 Objective-C 程式碼。總之,JavaScript 只是輔助,它只是提供了配置資訊和邏輯的處理結果。React Native 與 Hybrid 完全沒有關係,它只不過是以 JavaScript 的形式告訴 Objective-C 該執行什麼程式碼。

其次,React Native 能夠執行起來,全靠 Objective-C 和 JavaScript 的互動。對於沒有接觸過 JavaScript 的人來說,非常有必要理解 JavaScript 程式碼如何被執行。

我們知道 C 系列的語言,經過編譯,連結等操作後,會得到一個二進位制格式的可執行文,所謂的執行程式,其實是執行這個二進位制程式。

而 JavaScript 是一種指令碼語言,它不會經過編譯、連結等操作,而是在執行時才動態的進行詞法、語法分析,生成抽象語法樹(AST)和位元組碼,然後由直譯器負責執行或者使用 JIT 將位元組碼轉化為機器碼再執行。整個流程由 JavaScript 引擎負責完成。

蘋果提供了一個叫做 JavaScript Core 的框架,這是一個 JavaScript 引擎。通過下面這段程式碼可以簡單的感受一下 Objective-C 如何呼叫 JavaScript 程式碼:

這裡的 JSContext 指的是 JavaScript 程式碼的執行環境,通過 evaluateScript 即可執行 JavaScript 程式碼並獲取返回結果。

JavaScript 是一種單執行緒的語言,它不具備自執行的能力,因此總是被動呼叫。很多介紹 React Native 的文章都會提到 “JavaScript 執行緒” 的概念,實際上,它表示的是 Objective-C 建立了一個單獨的執行緒,這個執行緒只用於執行 JavaScript 程式碼,而且 JavaScript 程式碼只會在這個執行緒中執行。

Objective-C 與 JavaScript 互動

提到 Objective-C 與 JavaScript 的互動,不得不推薦 bang神的這篇文章:React Native通訊機制詳解 。雖然其中不少細節都已經過時,但是整體的思路值得學習。

本節主要分析 Objective-C 與 JavaScript 互動時的整理邏輯與流程,下一節將通過原始碼來分析具體原理。

JavaScript 呼叫 Objective-C

由於 JavaScript Core 是一個面向 Objective-C 的框架,在 Objective-C 這一端,我們對 JavaScript 上下文知根知底,可以很容易的獲取到物件,方法等各種資訊,當然也包括呼叫 JavaScript 函式。

真正複雜的問題在於,JavaScript 不知道 Objective-C 有哪些方法可以呼叫。

React Native 解決這個問題的方案是在 Objective-C 和 JavaScript 兩端都儲存了一份配置表,裡面標記了所有 Objective-C 暴露給 JavaScript 的模組和方法。這樣,無論是哪一方呼叫另一方的方法,實際上傳遞的資料只有 ModuleIdMethodIdArguments 這三個元素,它們分別表示類、方法和方法引數,當 Objective-C 接收到這三個值後,就可以通過 runtime 唯一確定要呼叫的是哪個函式,然後呼叫這個函式。

再次重申,上述解決方案只是一個抽象概念,可能與實際的解決方案有微小差異,比如實際上 Objective-C 這一端,並沒有直接儲存這個模組配置表。具體實現將在下一節中隨著原始碼一起分析。

閉包與回撥

既然說到函式互調,那麼就不得不提到回撥了。對於 Objective-C 來說,執行完 JavaScript 程式碼再執行 Objective-C 回撥毫無難度,難點依然在於 JavaScript 程式碼呼叫 Objective-C 之後,如何在 Objective-C 的程式碼中,回撥執行 JavaScript 程式碼。

目前 React Native 的做法是:在 JavaScript 呼叫 Objective-C 程式碼時,註冊要回撥的 Block,並且把 BlockId 作為引數傳送給 Objective-C,Objective-C 收到引數時會建立 Block,呼叫完 Objective-C 函式後就會執行這個剛剛建立的 Block。

Objective-C 會向 Block 中傳入引數和 BlockId,然後在 Block 內部呼叫 JavaScript 的方法,隨後 JavaScript 查詢到當時註冊的 Block 並執行。

圖解

好吧,如果你是新手,並且堅持讀到了這裡,估計已經懵逼了。不要擔心,與 JavaScript 的互動確實不是一下子能夠完全理清楚的,你可以先參考這個示意圖:

1171077-75412d65af198cf5
互動流程

注:

  1. 本圖由 bang 的文章中的圖片修改而來
  2. 本圖只是一個簡單的示意圖,不建議當做時序圖使用,請參考下一節原始碼分析。
  3. Objective-C 和 JavaScript 的互動總是由前者發起,本圖為了簡化,省略了這一步驟。

React Native 原始碼分析

要想深入理解 React Native 的工作原理,有兩個部分有必要閱讀一下,分別是初始化階段和方法呼叫階段。

為了提煉出程式碼的核心含義,我會在不改變程式碼意圖的基礎上對它做一些刪改,以便閱讀。

寫這篇文章是,React Native 還處於 0.27 版本,由於在 1.0 之前的變動幅度相對較大,因此下面的原始碼分析很可能隨著 React Native 的演變而過時。但不管何時,把下面的原始碼讀一遍都有助於你加深對 React Native 原理的理解。

初始化 React Native

每個專案都有一個入口,然後進行初始化操作,React Native 也不例外。一個不含 Objective-C 程式碼的專案留給我們的唯一線索就是位於 AppDelegate 檔案中的程式碼:

使用者能看到的一切內容都來源於這個 RootView,所有的初始化工作也都在這個方法內完成。

在這個方法內部,在建立 RootView 之前,React Native 實際上先建立了一個 Bridge 物件。它是 Objective-C 與 JavaScript 互動的橋樑,後續的方法互動完全依賴於它,而整個初始化過程的最終目的其實也就是建立這個橋樑物件。

初始化方法的核心是 setUp 方法,而 setUp 方法的主要任務則是建立 BatchedBridge

BatchedBridge 的作用是批量讀取 JavaScript 對 Objective-C 的方法呼叫,同時它內部持有一個 JavaScriptExecutor,顧名思義,這個物件用來執行 JavaScript 程式碼。

建立 BatchedBridge 的關鍵是 start 方法,它可以分為五個步驟:

  1. 讀取 JavaScript 原始碼
  2. 初始化模組資訊
  3. 初始化 JavaScript 程式碼的執行器,即 RCTJSCExecutor 物件
  4. 生成模組列表並寫入 JavaScript 端
  5. 執行 JavaScript 原始碼

我們逐個分析每一步完成的操作:

讀取 JavaScript 原始碼

這一部分的具體程式碼實現沒有太大的討論意義。我們只要明白,JavaScript 的程式碼是在 Objective-C 提供的環境下執行的,所以第一步就是把 JavaScript 載入進記憶體中,對於一個空的專案來說,所有的 JavaScript 程式碼大約佔用 1.5 Mb 的記憶體空間。

需要說明的是,在這一步中,JSX 程式碼已經被轉化成原生的 JavaScript 程式碼。

初始化模組資訊

這一步在方法 initModulesWithDispatchGroup: 中實現,主要任務是找到所有需要暴露給 JavaScript 的類。每一個需要暴露給 JavaScript 的類(也成為 Module,以下不作區分)都會標記一個巨集:RCT_EXPORT_MODULE,這個巨集的具體實現並不複雜:

這樣,這個類在 load 方法中就會呼叫 RCTRegisterModule 方法註冊自己:

因此,React Native 可以通過 RCTModuleClasses 拿到所有暴露給 JavaScript 的類。下一步操作是遍歷這個陣列,然後生成 RCTModuleData 物件:

可以想見,RCTModuleData 物件是模組配置表的主要組成部分。如果把模組配置表想象成一個陣列,那麼每一個元素就是一個 RCTModuleData 物件。

這個物件儲存了 Module 的名字,常量等基本資訊,最重要的屬性是一個陣列,儲存了所有需要暴露給 JavaScript 的方法。

暴露給 JavaScript 的方法需要用 RCT_EXPORT_METHOD 這個巨集來標記,它的實現原理比較複雜,有興趣的讀者可以自行閱讀。簡單來說,它為函式名加上了 __rct_export__ 字首,再通過 runtime 獲取類的函式列表,找出其中帶有指定字首的方法並放入陣列中:

因此 Objective-C 管理模組配置表的邏輯是:Bridge 持有一個陣列,陣列中儲存了所有的模組的 RCTModuleData 物件。只要給定 ModuleIdMethodId 就可以唯一確定要呼叫的方法。

初始化 JavaScript 程式碼的執行器,即 RCTJSCExecutor 物件

通過檢視原始碼可以看到,初始化 JavaScript 執行器的時候,addSynchronousHookWithName 這個方法被呼叫了多次,它其實向 JavaScript 上下文中新增了一些 Block 作為全域性變數:

有些同學讀原始碼時可能會走進一個誤區,如果在 Block 中打一個斷點就會發現,Block 其實是被執行了,但卻找不到任何能夠執行 Block 的程式碼。

這其實是因為這個 Block 並非由 Objective-C 主動呼叫,而是在第五步執行 JavaScript 程式碼時,由 JavaScript 在上下文中獲取到 Block 物件並呼叫,有興趣的讀者可以自行新增斷點並驗證。

這裡我們需要重點注意的是名為 nativeRequireModuleConfig 的 Block,它在 JavaScript 註冊新的模組時呼叫:

這就是模組配置表能夠載入到 JavaScript 中的原理。

另一個值得關注的 Block 叫做 nativeFlushQueueImmediate。實際上,JavaScript 除了把呼叫資訊放到 MessageQueue 中等待 Objective-C 來取以外,也可以主動呼叫 Objective-C 的方法:

目前,React Native 的邏輯是,如果訊息佇列中有等待 Objective-C 處理的邏輯,而且 Objective-C 超過 5ms 都沒有來取走,那麼 JavaScript 就會主動呼叫 Objective-C 的方法:

這個 handleBuffer 方法是 JavaScript 呼叫 Objective-C 方法的關鍵,在下一節——方法呼叫中,我會詳細分析它的實現原理。

一般情況下,Objective-C 會定時、主動的呼叫 handleBuffer 方法,這有點類似於輪詢機制:

然而由於卡頓或某些特殊原因,Objective-C 並不能總是保證能夠準時的清空 MessageQueue,這就是為什麼 JavaScript 也會在一定時間後主動的呼叫 Objective-C 的方法。檢視上面 JavaScript 的程式碼可以發現,這個等待時間是 5ms。

請牢牢記住這個 5ms,它告訴我們 JavaScript 與 Objective-C 的互動是存在一定開銷的,不然就不會等待而是每次都立刻發起請求。其次,這個時間開銷大約是毫秒級的,不會比 5ms 小太多,否則等待這麼久就意義不大了。

生成模組配置表並寫入 JavaScript 端

複習一下 nativeRequireModuleConfig 這個 Block,它可以接受 ModuleName 並且生成詳細的模組資訊,但在前文中我們沒有提到 JavaScript 是如何知道 Objective-C 要暴露哪些類的(目前只是 Objective-C 自己知道)。

這一步的操作就是為了讓 JavaScript 獲取所有模組的名字:

檢視原始碼可以發現,Objective-C 把 config 字串設定成 JavaScript 的一個全域性變數,名字叫做:__fbBatchedBridgeConfig

執行 JavaScript 原始碼

這一步也沒什麼技術難度可以,程式碼已經載入進了記憶體,該做的配置也已經完成,只要把 JavaScript 程式碼執行一遍即可。

執行程式碼時,第三步中所說的那些 Block 就會被執行,從而向 JavaScript 端寫入配置資訊。

至此,JavaScript 和 Objective-C 都具備了向對方互動的能力,準備工作也就全部完成了。

畫了一個簡陋的時序圖以供參考:

1171077-320b2bbb78e2b7e8

初始化過程

方法呼叫

如前文所述,在 React Native 中,Objective-C 和 JavaScript 的互動都是通過傳遞 ModuleIdMethodIdArguments 進行的。以下是分情況討論:

呼叫 JavaScript 程式碼

也許你在其他文章中曾經多次聽說 JavaScript 程式碼總是在一個單獨的執行緒上面呼叫,它的實際含義是 Objective-C 會在單獨的執行緒上執行 JavaScript 程式碼:

呼叫 JavaScript 程式碼的核心程式碼如下:

需要注意的是,這個函式名是我們要呼叫 JavaScript 的中轉函式名,比如 callFunctionReturnFlushedQueue。也就是說它的作用其實是處理引數,而非真正要呼叫的 JavaScript 函式。

這個中轉函式接收到的引數包含了 ModuleIdMethodIdArguments,然後由中轉函式查詢自己的模組配置表,找到真正要呼叫的 JavaScript 函式。

在實際使用的時候,我們可以這樣發起對 JavaScript 的呼叫:

這裡的 Name 和 Body 引數分別表示要呼叫的 JavaScript 的函式名和引數。

JavaScript 呼叫 Objective-C

在呼叫 Objective-C 程式碼時,如前文所述,JavaScript 會解析出方法的 ModuleIdMethodIdArguments 並放入到 MessageQueue 中,等待 Objective-C 主動拿走,或者超時後主動傳送給 Objective-C。

Objective-C 負責處理呼叫的方法是 handleBuffer,它的引數是一個含有四個元素的陣列,每個元素也都是一個陣列,分別存放了 ModuleIdMethodIdParams,第四個元素目測用處不大。

函式內部在每一次方呼叫中呼叫 _handleRequestNumber:moduleID:methodID:params 方法。,通過查詢模組配置表找出要呼叫的方法,並通過 runtime 動態的呼叫:

在這個方法中,有一個很關鍵的方法:processMethodSignature,它會根據 JavaScript 的 CallbackId 建立一個 Block,並且在呼叫完函式後執行這個 Block。

實戰應用

俗話說:“思而不學則神棍”,下面舉一個例子來演示 Objective-C 是如何與 JavaScript 進行互動的。首先新建一個模組:

Person 這個類是一個新的模組,它有兩個方法暴露給 JavaScript:

在 JavaScript 中,可以這樣呼叫:

有興趣的同學可以複製以上程式碼並自行除錯。

React Native 優缺點分析

經過一長篇的討論,其實 React Native 的優缺點已經不難分析了,這裡簡單總結一下:

優點

  1. 複用了 React 的思想,有利於前端開發者涉足移動端。
  2. 能夠利用 JavaScript 動態更新的特性,快速迭代。
  3. 相比於原生平臺,開發速度更快,相比於 Hybrid 框架,效能更好。

缺點

  1. 做不到 Write once, Run everywhere,也就是說開發者依然需要為 iOS 和 Android 平臺提供兩套不同的程式碼,比如參考官方文件可以發現不少元件和API都區分了 Android 和 iOS 版本。即使是共用元件,也會有平臺獨享的函式。
  2. 不能做到完全遮蔽 iOS 端或 Android 的細節,前端開發者必須對原生平臺有所瞭解。加重了學習成本。對於移動端開發者來說,完全不具備用 React Native 開發的能力。
  3. 由於 Objective-C 與 JavaScript 之間切換存在固定的時間開銷,所以效能必定不及原生。比如目前的官方版本無法做到 UItableview(ListView) 的檢視重用,因為滑動過程中,檢視重用需要在非同步執行緒中執行,速度太慢。這也就導致隨著 Cell 數量的增加,佔用的記憶體也線性增加。

綜上,我對 React Native 的定位是:

利用指令碼語言進行原生平臺開發的一次成功嘗試,降低了前端開發者入門移動端的門檻,一定業務場景下具有獨特的優勢,幾乎不可能取代原生平臺開發。

參考資料

  1. React Native 官方文件React Native 官方文件中文版
  2. React Native通訊機制詳解
  3. React 入門例項教程

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

React Native 從入門到原理 React Native 從入門到原理

相關文章