React-Native — 原理探究

本文主要大致介紹 React-Native 框架的底層原理,以及新架構(gòu)的演變。

文章骨架主體來自React Native 原理與實(shí)踐,很多細(xì)節(jié)上加以補(bǔ)充。關(guān)于新、舊架構(gòu)的篇幅側(cè)重增加了一些

一、什么是 React Native?

React Native 是一個(gè)由 Facebook 于 2015 年 9 月發(fā)布的一款開源的 JavaScript 框架,它可以讓開發(fā)者使用 JavaScript 和 React 來開發(fā)跨平臺(tái)的移動(dòng)應(yīng)用。它既保留了 React 的開發(fā)效率,又同時(shí)擁有 Native 應(yīng)用的良好體驗(yàn),加上 Virtual DOM 跨平臺(tái)的優(yōu)勢,實(shí)現(xiàn)了真正意義上的:Learn Once,Write Anywhere.

二、React Native 的特點(diǎn)

2.1 跨平臺(tái)

React Native 使用了 Virtual DOM(虛擬 DOM),只需編寫一套代碼,便可以將代碼打包成不同平臺(tái)的 App,極大提高了開發(fā)效率,并且相對全部原生開發(fā)的應(yīng)用來說,維護(hù)成本也相對更低。

2.2 上手快

相比于原生開發(fā),JavaScript 學(xué)習(xí)成本低、語法靈活。允許讓 Web 開發(fā)者更多地基于現(xiàn)有經(jīng)驗(yàn)開發(fā) App。React Native 只需使用 JavaScript 就能編寫移動(dòng)原生應(yīng)用,它和 React 的設(shè)計(jì)理念是一樣的,因此可以毫不夸張地說:你如果會(huì)寫 React,就會(huì)寫 React Native!

2.3 原生體驗(yàn)

由于 React Native 提供的組件是對原生 API 的暴露,雖然我們使用的是 JavaScript 語言編寫的代碼,但是實(shí)際上是調(diào)用了原生的 API 和原生的 UI 組件。因此,體驗(yàn)和性能足以媲美原生應(yīng)用。

2.4 熱更新

React Native 開發(fā)的應(yīng)用支持熱更新,因?yàn)?React Native 的產(chǎn)物是 bundle 文件,其實(shí)本質(zhì)上就是 JS 代碼,在 App 啟動(dòng)的時(shí)候就會(huì)去服務(wù)器上獲取 bundle 文件,我們只需要更新 bundle 文件,從而使得 App 不需要重新前往商店下載包體就可以進(jìn)行版本更新,開發(fā)者可以在用戶無感知的情況下進(jìn)行功能迭代或者 bug 修復(fù)。

但是值得注意的是,AppStore 禁止熱更新的功能中有調(diào)用私有 API、篡改原生代碼和改變 App 的行為。

2.5 Flutter vs React Native

首先來簡單了解下 Flutter 和 React Native 的背景,F(xiàn)lutter 是由谷歌開發(fā)的軟件開發(fā)工具包(SDK)。它可以幫助開發(fā)人員使用單一代碼庫構(gòu)建 iOS 和 Android 應(yīng)用程序。React Native 與 Flutter 具有相同的目的,但方式不同。它是由 Facebook 建立的,基于 React 用于創(chuàng)建移動(dòng)應(yīng)用程序,而不會(huì)影響應(yīng)用程序的外觀和感覺。

2.5.1 開發(fā)體驗(yàn)

React Native 在界面開發(fā)延續(xù)了 React 開發(fā)風(fēng)格,支持 css-in-js(其實(shí)就是用 js 來寫 css),而且在 0.59 版本之后支持了 React Hook 函數(shù)式編程,開發(fā)的時(shí)候大多只關(guān)心樣式界面的搭建,原生能力有客戶端或者 Bridge 實(shí)現(xiàn)。

Flutter 最大的特點(diǎn)在于:Flutter 是一套平臺(tái)無關(guān)的 UI 框架,并且在 Flutter 里面萬物皆 Widget。很多時(shí)候開發(fā)一個(gè)控件需要嵌套多個(gè) Widget 去實(shí)現(xiàn),與 JS 里面的回調(diào)地獄有點(diǎn)像,而這也是被吐槽代碼嵌套樣式難看的原因。

2.5.2 狀態(tài)管理

React Native 和 Flutter 對于狀態(tài)管理,兩者有著很高的相似度,雖然內(nèi)部實(shí)現(xiàn)很大差別,但是都可以獲取 state 和 setState 的方式去更新頁面的狀態(tài)。

2.5.3 產(chǎn)物

React Native 產(chǎn)生的是 bundle 文件,實(shí)際上就是 JS 腳本文件;而 Flutter 編譯后 Android 產(chǎn)生的主要是一些應(yīng)用程序指令段、數(shù)據(jù)段,虛擬機(jī)數(shù)據(jù)段、指令段,iOS 則是 App.framework,其實(shí)也是一些原生的數(shù)據(jù)集。

2.5.4 原生能力 & 性能

其實(shí)兩者的在這方面的區(qū)別不是很大,性能方面 React Native 稍微差一點(diǎn)。

由于 React Native 和原生交互依賴的只有一個(gè) Bridge,而且 JS 和 Native 交互是異步的,所以對需要和 Native 大量實(shí)時(shí)交互的功能可能會(huì)有性能上的不足,比如動(dòng)畫效率,性能是不如原生的。

React Native 始終是依賴原生的能力,所以擺脫不了對原生的依賴,相對 Flutter 的自己來畫 UI 來說,React Native 顯得有些尷尬。

但是在原生靈活性上 React Native 要有優(yōu)勢。

最后,目前來看兩者是不分上下,各有所長。不過隨著RN新架構(gòu)的展開,性能方面的劣勢會(huì)有很大改善。

三、React Native 原理

3.1 當(dāng)前架構(gòu)

在 React Native 里面,真正有三個(gè)重要的線程在執(zhí)行,他們分別是 Shadow thread、UI thread 和 JS thread。

  • JS thread。是讀取和編譯所有 JavaScript 代碼、處理應(yīng)用程序大部分業(yè)務(wù)邏輯的地方。
    • Metro(打包工具)將React源碼打包成一個(gè)單一JS文件(就是圖中JSBundle)。然后傳給JS引擎執(zhí)行(現(xiàn)在ios和android統(tǒng)一用的是JSC)。
    • JS thread 負(fù)責(zé) JS 和原生代碼的交互,因?yàn)?JS 是單線程模型,所以需要一個(gè)單獨(dú)的線程來驅(qū)動(dòng),并且 JS 和 Native 交互(Bridge)是異步的。
  • UI Thread(Main Thread/Native thread)。這個(gè)線程主要負(fù)責(zé)兩部分:
    • 原生渲染(Native UI):負(fù)責(zé)頁面的交互,以及當(dāng)顯示、更改UI時(shí),完成控件繪制邏輯。
    • 調(diào)用原生功能(Native Modules):比如藍(lán)牙等。當(dāng)我們使用應(yīng)用程序時(shí),所有本地模塊都會(huì)啟動(dòng)。這意味著即使不需要使用藍(lán)牙模塊,React Native 也將始終激活它。
  • Shadow Thread。 是 React Native 計(jì)算布局的地方。
    • 這個(gè)線程會(huì)創(chuàng)建Shadow Tree來模擬React結(jié)構(gòu)樹。Shadow Tree可以類似虛擬dom。
    • 它使用 Facebook 自己的名為 Yoga 的布局引擎來計(jì)算 flexbox 布局,然后將結(jié)果發(fā)送到 Native UI(RN使用Flexbox布局,但是原生是不支持,所以Yoga就是用來將Flexbox布局轉(zhuǎn)換為原生平臺(tái)的布局方式)。

3.2 JavaScriptCore

JavaScriptCore 是 JavaScript 引擎,通常會(huì)被叫做虛擬機(jī),專門設(shè)計(jì)來解釋和執(zhí)行 JavaScript 代碼。在 React Native 里面,JavaScriptCore 負(fù)責(zé) bundle 產(chǎn)出的 JS 代碼的解析和執(zhí)行。

3.2.1 JS Engine

React Native 需要一個(gè) JS 的運(yùn)行環(huán)境,因?yàn)?React Native 會(huì)把應(yīng)用的 JS 代碼編譯成一個(gè) JS 文件(x x.bundle),React Native 框架的目標(biāo)就是解釋運(yùn)行這個(gè) JS 腳本文件。

如果是 Native 拓展的 API,則直接通過 bridge 調(diào)用 Native 方法,最基礎(chǔ)的比如繪制 UI 界面,映射 Virtual DOM 到真實(shí)的 UI 組件中。

脫離 React Native,純原生端是如何與 JS 交互的?來看下 iOS 里面是如何實(shí)現(xiàn)的。

在 Native 創(chuàng)建一個(gè) JS 上下文:

// 創(chuàng)建一個(gè)ctx的JS上下文
JSContent *ctx = [[JSContent alloc] init];
// 創(chuàng)建一個(gè)變量name
[ctx evaluateScript:@"var name = 'Hellen'"];
// 創(chuàng)建一個(gè)方法
[ctx evaluateScript:@"var hello = function(name) { return 'hello ' + name }"];

Native 調(diào)用 JavaScript 方法:

// 通過ctx上下文對象,獲取到hello方法
JSValue *helloFUnction = ctx[@"hello"];
// 運(yùn)行js方法
JSValue *greetings = [helloFunction callWithArguments:@[@"bytedancers"]; // hello bytedancers

所以,JavaScript 代碼只要將變量暴露在 JS 上下文全局,Native 就能獲取到,并運(yùn)行 JS 的代碼。

JavaScript 調(diào)用 Native,首先需要在 Native 端,將一個(gè)變量暴露在 JS 上下文全局,在 JavaScript 全局變量里面就能獲取到并執(zhí)行這個(gè)方法:

ctx[@"createdByNative"] = ^(NSString *name) {
    // do something
    return someResult
}

React Native 同樣借助 JS Engine 的能力,基于 JavaScriptCore 來執(zhí)行 JS,但是是通過 Bridge 來進(jìn)行交互的,JS 不會(huì)直接引用 Native 層的對象實(shí)例,Native 也不會(huì)直接引用 JS 層的對象實(shí)例(在 React Native 里所有 Native 和 JS 互調(diào)都是通過 Bridge 層的幾個(gè)最基礎(chǔ)的方法銜接的)。

3.2.2 Hermes Engine

Hermes 是 Facebook 在 2019 年發(fā)布的新一代 JS Engine,Hermes 是一款小巧輕便的 JavaScript 引擎,專門針對在 Android 上運(yùn)行 React Native 進(jìn)行了優(yōu)化:應(yīng)用啟動(dòng)時(shí)間減少、減少內(nèi)存使用量并縮小應(yīng)用程序大小,此外因?yàn)樗捎?JavaScript 標(biāo)準(zhǔn)實(shí)現(xiàn),所以很容易在 React Native 應(yīng)用中集成。

3.2.3 Hermes vs JavaScriptCore vs V8

經(jīng)過官方的數(shù)據(jù)驗(yàn)證,F(xiàn)aceback 團(tuán)隊(duì)提出的關(guān)鍵性指標(biāo)相較于原先的 JavaScriptCore 方案都有了顯著提高。首先,是產(chǎn)物文件的大小方面,RN 所依賴的必要 so 庫,Hermes 比 JavaScriptCore 減少了約 16%,V8 則要遠(yuǎn)大于 Hermes 和 JavaScriptCore。

3.3 Bridge

上面我們有提到 JS Engine,Native 可以把原生方法暴露到全局,同樣的 JS 也可以把方法暴露給 Native,但是 React Native 并沒有這樣做,原因之一是這樣會(huì)導(dǎo)致大量的全局變量污染,所以為了規(guī)范這個(gè)通信過程,React Native 自己實(shí)現(xiàn)了 Bridge。

  • Bridge 定義了 JS Thread 和 Native Thread 的通信規(guī)范,實(shí)現(xiàn)原生端和 JavaScript 之間的交互。
  • Bridge 給 React Native 內(nèi)嵌的 JS Engine 提供原生接口的擴(kuò)展,供 JS 調(diào)用(做定制化的時(shí)候會(huì)對 Bridge 添加修改代碼)。
  • 所有的本地存儲(chǔ)、圖片資源訪問、圖形圖像繪制、3D 加速、網(wǎng)絡(luò)訪問、震動(dòng)效果、NFC、原生控件繪制、地圖、定位、通知等都是通過 Bridge 封裝成 JS 接口以后注入 JS Engine 供 JS 調(diào)用。

理論上,任何原生代碼能實(shí)現(xiàn)的效果都可以通過 Bridge 封裝成 JS 可以調(diào)用的組件和方法,以 JS 模塊的形式提供給 RN 使用。

3.3.1 Bridge的運(yùn)行過程

為了讓,我們需要使用一個(gè)名為 Bridge 的 C++ 模塊。

當(dāng)我們寫了類似下面的React源碼。

<View style={{
        backgroundColor: 'pink',
        width: 200, 
        height: 200}}/> 
  1. JS thread會(huì)先對其序列化,形成下面一條消息

    UIManager.createView([343,"RCTView",31,{"backgroundColor":-16181,"width":200,"height":200}])
    
  2. 通過Bridge發(fā)到ShadowThread。Shadow Tread接收到這條信息后,先反序列化,形成Shadow tree,然后傳給Yoga,形成原生布局信息。

  3. 接著又通過Bridge傳給UI thread。

  4. UI thread 拿到消息后,同樣先反序列化,然后根據(jù)所給布局信息,進(jìn)行繪制。

從上面過程可以看到三個(gè)線程的交互都是要通過Bridge,因此瓶頸也就在此。

Bridge的三個(gè)特點(diǎn):

  1. 異步。這些消息隊(duì)列是異步的,無法保證處理事件。

    所有基于 JSON 信號(hào)流的線程都通過 Bridge 異步傳輸,并且它們將被發(fā)送到任何一方,希望(但不是保證)會(huì)收到響應(yīng)。未來,您也可能會(huì)遇到信息擁塞并且永遠(yuǎn)不會(huì)收到響應(yīng)。

  2. 序列化。每當(dāng)它從任何一方(JS 線程或本地線程)接收數(shù)據(jù)時(shí),這些數(shù)據(jù)將被序列化為 JSON 并發(fā)送到隊(duì)列,最終在到達(dá)時(shí)進(jìn)行解碼。

    通過JSON格式來傳遞消息,每次都要經(jīng)歷序列化和反序列化,開銷很大。

  3. 批處理。對Native調(diào)用進(jìn)行排隊(duì),批量處理。

3.3.2 Bridge的實(shí)現(xiàn)細(xì)節(jié)

1. RCTRootView

原生項(xiàng)目如果想用 React Native,那么就需要用到 RCTRootView,它是 React Native 加載的地方,可以把它看作是一個(gè)容器。

// RCTRootView.m
- (void)javaScriptDidLoad:(NSNotification *)notification{
  RCTAssertMainQueue();
  RCTBridge *bridge = notification.userInfo[@"bridge"];
  if (bridge != _contentView.bridge) {
    [self bundleFinishedLoading:bridge];
  }
}

- (void)bundleFinishedLoading:(RCTBridge *)bridge{
  // 省略創(chuàng)建RCTRootContentView...

  [self runApplication:bridge];

  // 省略添加一個(gè)RCTRootContentView...
}

- (void)runApplication:(RCTBridge *)bridge{
  NSString *moduleName = _moduleName ?: @""; // 這里是@"NewProject"
  NSDictionary *appParameters = @{
            @"rootTag": _contentView.reactTag,
            @"initialProps": _appProperties ?: @{},
  };

  [bridge enqueueJSCall:@"AppRegistry"
                 method:@"runApplication"
                   args:@[moduleName, appParameters]
             completion:NULL];
}

上面的源碼(iOS)可以看出 RCTRootView 其實(shí)做了這些事情:

  • 創(chuàng)建了負(fù)責(zé) React Native 和 Native 通信的 RCTBridge 實(shí)例的初始化。

  • 初始化了真正展示視圖的 RCTRootContentView。

  • 通過 runApplication 方法把必要的參數(shù)(moduleName, params)傳給 JS 側(cè)的 AppRegistry 的 runApplication 方法,從而運(yùn)行起了 React Native 的功能。(在 React native 中,根組件是需要通過 AppRegistry 的 registerComponent 方法進(jìn)行注冊的。所謂根組件,就是 Native to JS 的入口文件)

渲染過程:

2. Native 調(diào)用 JS

在 React Native 里面,JS 的方法可以通過 global.batchedBridge.callFunctionReturnFlushedQueue 這個(gè)方法進(jìn)行調(diào)用,所以在 Native 側(cè),只需將 React Native 里面的 global.batchedBridge 對象中的方法和 Native 側(cè)的 JSIExecutor 方法進(jìn)行綁定(本質(zhì)上 Native 指針指向 JS 函數(shù))

JSIExecutor::callFunctionReturnFlushedQueue_ = global.batchedBridge.callFunctionReturnFlushedQueue

Native 側(cè)的 callFunctionReturnFlushedQueue 主要做了這樣的事情:

  • 通過 moduleid 和 methodid 完成方法的調(diào)用,通過這兩個(gè)參數(shù)可以找到 JS 側(cè)定義的方法模塊。
3. JS 調(diào)用 Native

React Native 中的 Native 模塊如何暴露給 JS?

我們知道,React Native 可以調(diào)用 Native 側(cè)的方法,并且只要 Native 側(cè)只要遵循一定的規(guī)則,是可以將方法暴露給 JS 調(diào)用的:

// iOS端原生代碼
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface NativeLogModule : NSObject<RCTBridgeModule>

@end

#import "NativeLogBridge.h"

@implementation NativeLogModule
RCT_EXPORT_MODULE()
RCT_EXPORT_METHOD(nativeLog:(id)obj) {
  NSLog(@"%@",obj);
}
@end

// JS端調(diào)用
import { NativeModules } from 'react-native';
NativeModules.NativeLogModule.nativeLog('從JS側(cè)來的消息');

可以看到,上面的代碼中使用了RCT_EXPORT_MODULE() 宏將 Native 類以 module 的形式暴露給了 JS,然后使用了RCT_EXPORT_METHOD將 Native 的方法暴露給 JS,最后在 JS 側(cè)直接引用一個(gè)模塊,便可以直接調(diào)用暴露的方法與 Native 通信。

JS 調(diào)用 Native

當(dāng) JS 調(diào)用 Native 模塊的時(shí)候,會(huì)調(diào)用一個(gè) Native 暴露出來的全局方法:nativeFlushQueueImmediate,并通過傳入要調(diào)用的 moduleName 、methodName、callback 參數(shù)給這個(gè)方法,然后這個(gè)方法再通知給 Native 側(cè)找到相應(yīng)的模塊并執(zhí)行。

3.3.3 Bridge的問題

異步設(shè)計(jì)的好處是不阻塞,這種設(shè)計(jì)在大部分情況下性能滿足需求,但是在某些情況下就會(huì)出問題,比如瀑布流滾動(dòng)。

當(dāng)瀑布流向下滑動(dòng)的時(shí)候,需要發(fā)請求給服務(wù)端拿數(shù)據(jù)進(jìn)行下一步渲染。

滾動(dòng)事件發(fā)生在UI thread,然后通過Bridge發(fā)給JS thread。JS thread 監(jiān)聽到消息后發(fā)請求,服務(wù)端返回?cái)?shù)據(jù),再通過Bridge返回給Native進(jìn)行渲染。由于都是異步,就會(huì)出現(xiàn)空白模塊,導(dǎo)致性能問題。

從上面可以看出,性能瓶頸主要是存在JS線程和Native有交互的情況,如果不存在交互,RN的性能良好。

因此,對于RN的優(yōu)化,主要集中在Bridge上,有下面3個(gè)原則:

  1. JS和Native端不通信。最徹底的方式,消息不走Bridge。
  2. JS和Native減少通信。在兩端無法避免的情況下,盡量通信減少次數(shù)。比如多個(gè)請求合并成一個(gè)。
  3. 較少JSON的大小。比如圖片轉(zhuǎn)為Base64會(huì)導(dǎo)致傳輸數(shù)據(jù)變大,用網(wǎng)絡(luò)圖片代替。

RN里面可以通過MessageQueue來監(jiān)聽Bridge通信,主要代碼如下

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue.js';

const spyFunction = (msg) => {
  console.log(msg);
};

MessageQueue.spy(spyFunction);

下面是監(jiān)聽到的信息

3.4 Virtual DOM

在認(rèn)識(shí) Virtual DOM 之前,我們先來看看瀏覽器的工作原理:

  • 瀏覽器有一套完整的 UI 控件,樣式和功能都是按照 HTML 標(biāo)準(zhǔn)實(shí)現(xiàn)的。

  • 瀏覽器能夠解析 HTML 和 CSS,通過 HTML 告訴瀏覽器需要生成什么類型的 UI 控件,并通過 CSS 樣式文件來描述該 UI 控件的外觀(大小,背景,布局等等...)。

  • 瀏覽器的主要作用就是解析 HTML 和 CSS 來形成渲染樹,并通過 Render Engine 將頁面渲染出來。

了解瀏覽器的工作原理之后,Virtual DOM 是如何工作的?

  • 首先 Virtual DOM 和真實(shí) DOM 都有一個(gè)共同點(diǎn):都是用來描述頁面 UI 控件。

  • Virtual DOM 具有平臺(tái)無關(guān)性:它描述的 UI 控件只是數(shù)據(jù)結(jié)構(gòu)層的,具體渲染工作是交給了原生渲染引擎(瀏覽器、iOS、Android)去處理。

3.4.1 React (Native)的 Virtual DOM

在 React Native 里面,是如何把 Virtual DOM 渲染成真實(shí)的 UI 的呢?

首先,在 React 里面,用來表示 dom 屬性的對象有以下關(guān)鍵屬性:

var ele = {
    ...
    type: type, // 元素的類型
    key: key, // 元素key標(biāo)示
    ref: ref, // 元素的引用
    props: props, // 元素的參數(shù),包含children
    ...
}

// example 1
<div>hello</div>
// 會(huì)被描述為

{type: 'div',
    props: {
        children: ['hello']
    }
}

// example 2
<CustomerComponents />
// 會(huì)被描述為
{
    type: CustomerComponents
}

React 里面的 Virtual DOM 把真實(shí) DOM 分為了以下幾種類型:

  • 原子類型

    • 類型為字符串,結(jié)構(gòu)上不可再分解,渲染由平臺(tái)底層支持。
    • 在瀏覽器端:原子類型表示為瀏覽器支持的原始標(biāo)簽,例如 div、ul、li、p、a、span 等等。
    • 在 Native 端:原子類型表示為 Native 端的各種基礎(chǔ) UI 組件,例如 RCTText、RCTView 等等
  • 組合類型

    • 類型為函數(shù)構(gòu)造器,它給我們提供了一種自定義元素 UI 和行為的能力,當(dāng)渲染器遇到組合類型的元素時(shí),會(huì)使用它的構(gòu)造器創(chuàng)建一個(gè)實(shí)例并運(yùn)行 render 方法得到一個(gè)新元素(原子類型,或者組合類型),然后再拿該元素繼續(xù)進(jìn)行渲染或者分解。
    • 用戶自定義的組件元素。

3.4.2 渲染器 — 瀏覽器端

在瀏覽器端和 Native 端,React (Native)中 Virtual DOM 用來渲染真實(shí) DOM 的渲染器是不一樣的:

在瀏覽器端:

// 文本類型渲染器工作原理
mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context
) {
    // 獲取到DOM對象
    var lazyTree = DOMLazyTree(ownerDocument.createDocumentFragment());
    // 判斷是不是文本類型
    if (this._stringText) {
        DOMLazyTree.queueChild(
            lazyTree,
            DOMLazyTree(ownerDocument.createTextNode(this._stringText))
        );
    }
    return lazyTree
}

// 原子類型渲染器工作原理
mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context
) {
    var ownerDocument = hostContainerInfo._ownerDocument;
    // 創(chuàng)建原子type類型dom
    var el = 
ownerDocument.createElement(this._currentElement.type)
    ...

    // 創(chuàng)建子節(jié)點(diǎn)
    this._createInitialChildren(transaction, props, context, lazyTree);
    ...
}

// 組合類型渲染器工作原理
mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context
) {

    // 運(yùn)行構(gòu)造器,獲取到組件實(shí)例

    var inst = this._constructComponent({
        doConstruct,
        publicProps,
        publicContext,
        updateQueue,
    });

    ...

    // 獲取到虛擬dom
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }

    ...

    // 獲取到分解后的組件類型(ReactXXXComponent,可以看作是三種組件類型的集合)
    this._renderedComponent = this._instantiateReactComponent(
      renderedElement
    );
    // 最后通過ReactReconciler.mountComponent間接的調(diào)用不同類型的渲染器的mountComponent方法獲取到生產(chǎn)的node節(jié)點(diǎn),并返回。
    var markup = ReactReconciler.mountComponent(
      this._renderedComponent,
      rootID,
      transaction,
      this._processChildContext(context)
    );

    return markup
}

看以下代碼結(jié)構(gòu):

3.4.3 渲染器 — Native端

在 Native 端:

在瀏覽器里面,JavaScript 可以調(diào)用 DOM API 去完成創(chuàng)建 UI 的工作,而在 React Native 里面,是通過 UI Manager 來創(chuàng)建視圖的,基于 Virtual DOM ,React Native 把不同平臺(tái)創(chuàng)建視圖的邏輯封裝了一層,不同平臺(tái)通過 Bridge 調(diào)用 UI Manager 來創(chuàng)建不同的 Native 視圖。

3.5 熱更新

React Native 的產(chǎn)物 bundle 文件,本質(zhì)上是 JS 的邏輯代碼加上 React Native 的 Runtime 的集合,所以在應(yīng)用一啟動(dòng)的時(shí)候就會(huì)去獲取 bundle 文件,之后解析 bundle 文件,最后再由 JS Engine 去執(zhí)行具體的業(yè)務(wù)代碼邏輯。這就可以允許開發(fā)者在云端去更新 bundle 文件,然后應(yīng)用啟動(dòng)的時(shí)候獲取最新的 bundle 文件,這一整個(gè)流程下來就實(shí)現(xiàn)了熱更新。

3.5.1 增量更新(拆包)

對于 React Native 的代碼打包之后只會(huì)生成一個(gè) Bundle 文件,這里面包含了基礎(chǔ)業(yè)務(wù)邏輯、React Native 的基礎(chǔ)庫類,所以我們可以把一個(gè)包拆分成:一個(gè)基礎(chǔ)包+ n 個(gè)業(yè)務(wù)包,其中基礎(chǔ)包是不變的,這就是 runtime,業(yè)務(wù)包就是具體的業(yè)務(wù),后面如果有更新,也只需要再打出一個(gè)業(yè)務(wù)包就行。

目前行業(yè)的解決方案有 facebook 官方提供的 metro bundle:facebook.github.io/metro/

四、React Native新架構(gòu)

FB團(tuán)隊(duì)逐漸意識(shí)到Bridge存在的一些問題,同時(shí)也受到Flutter的壓力,在2018年提出了新架構(gòu):移除了Bridge,取而代之的是一個(gè)名為 Javascript Interface (JSI) 的新組件。

新的架構(gòu)主要由 JSI、Fabric、TurboModules、CodeGen、LeanCode組成。

4.1 JSI

JSI(Javascript Interface)是整個(gè)架構(gòu)的核心和基石,所有的一切都是建立在它上面。用于取代原先的 bridge,提高通信效率,JSI 已經(jīng)跟隨 RN 0.59 (JSIExecuter.cpp) 發(fā)布。

JSI 本身不是 React Native 的一部分——它是一個(gè)統(tǒng)一的、輕量的、通用適用于任何(理論上) JavaScript 虛擬機(jī)的接口層。讓各種平臺(tái)可以方便地使用不同的 JavaScript 解析引擎(JavaScript virtual machine 包含 JavaScript Engine)。

當(dāng)把 JSI 加入到新架構(gòu)中后,它使得一些真正重要的改進(jìn)成為可能。

  • 第一個(gè)改進(jìn)很直觀 —— 有了JSI,JS引擎不再局限于JSC。換句話說,JSC 引擎現(xiàn)在可以與其他具有更好性能的 JavaScript 引擎交換,比如微軟的 ChakraCore 和谷歌的 V8等,進(jìn)一步提高JS解析執(zhí)行的速度。

  • 通過JSI,JS對象可以直接持有C++宿主對象(Host Objects)引用,并調(diào)用它們的方法。

    宿主對象:由宿主環(huán)境提供的對象,對應(yīng)Native Object。如JavaScript中,瀏覽器宿主環(huán)境中的window對象以及其下邊所有的子對象(如bom、dom等等),node宿主環(huán)境中的globla及其子對象。

這意味著:自此三個(gè)線程通信再也不需要通過Bridge,JavaScript 和 Native 之間真正地相互知曉,并且不再需要通過 JSON 序列化傳遞消息,這會(huì)消除 Bridge 的阻塞問題。不像原來那樣用一層 bridge 來排隊(duì)等待原生層返回的消息,讓同步通信成為現(xiàn)實(shí)。具體的用法可以看 官方例子。

JS -> JSI -> C++ -> ObjectC/Java

4.2 Fabric

Fabric是整個(gè)架構(gòu)中的新UI層,包括了新架構(gòu)圖中的renderer和shadow thread。

下圖是舊的通信模型。

三個(gè)線程通過Bridge異步通信,數(shù)據(jù)需要拷貝多份。

有了JSI以后,JS可以直接掉調(diào)用其他線程,實(shí)現(xiàn)同步通信機(jī)制。另外數(shù)據(jù)可以直接引用,不需要拷貝,于是就變成了下面新的通信模式.

除了同步能力,直接引用,另外一個(gè)好處是Fabric現(xiàn)在支持渲染優(yōu)先級(jí)比如React的Concurrent和Suspense模式

下面兩張圖是從啟動(dòng)到渲染階段,加入Fabric前后的變化。

改造為Fabric之后

4.3 TurboModules

TurboModules主要和原生應(yīng)用能力相關(guān),對應(yīng)新架構(gòu)圖上的Native Modules,這部分的優(yōu)化是:

  1. 通過JSI,可以讓JS直接調(diào)用Native模塊,實(shí)現(xiàn)一些同步操作。比如調(diào)用攝像頭能力。

  2. Native模塊懶加載。之前RN框架啟動(dòng)的時(shí)候會(huì)加載所有Native模塊,導(dǎo)致啟動(dòng)慢,時(shí)間久?,F(xiàn)在有了TurboModules后,可以實(shí)現(xiàn)按需加載,減少啟動(dòng)時(shí)間,提高性能。

4.4 CodeGen

通過CodeGen,自動(dòng)將Flow或者Ts等有靜態(tài)類型的JS代碼翻譯成Fabric和TurboModules使用的原生代碼。

4.5 Lean Core

這部分主要是包的瘦身,以前所有的包都放在RN核心工程里面?,F(xiàn)在RN核心只保留必要的包,其他都移到react-native-community 或者拆出單獨(dú)的組件,比如Webview和AsyncStore。

4.6 開發(fā)進(jìn)度

  • 2018 年 6 月,F(xiàn)acebook 曾在 宣布了大規(guī)模 重構(gòu) RN 的計(jì)劃和路線圖;

  • 期間,JSI、LeanCore、Fabric、TurboModules一項(xiàng)一項(xiàng)陸續(xù)開發(fā)完成;

  • 2021 年 7 月 14 日,React Native 核心團(tuán)隊(duì)的 Joshua Gross 在 Twitter 說,RN 的新架構(gòu)已經(jīng)在 Facebook 內(nèi)部落地了,并且 99%的代碼已經(jīng)開源。

4.7 性能評(píng)價(jià)

引自React Native 迎來重大架構(gòu)升級(jí),性能將大幅提升

重構(gòu)目的是為了讓 RN 更輕量化、更適應(yīng)混合開發(fā),接近甚至達(dá)到原生的體驗(yàn)。具體包括以下幾個(gè)方面:

  1. 改變線程模型。UI 更新不再同時(shí)需要在三個(gè)不同的線程上觸發(fā)執(zhí)行,而是可以在任意線程上同步調(diào)用 JavaScript 進(jìn)行優(yōu)先更新,同時(shí)將低優(yōu)先級(jí)工作推出主線程,以便保持對 UI 的響應(yīng)。

  2. 引入異步渲染能力,允許多個(gè)渲染并簡化異步數(shù)據(jù)處理。

  3. 簡化 JSBridge,讓它更快更輕量。

這次升級(jí)過后,RN 在性能上能夠追平 Flutter。首先,JavaScript 和 Dart 語言上都支持了 AOT 預(yù)編譯,打個(gè)平手。其次,JavaScript 和 Dart 和底層交互都是通過 C++ 進(jìn)行的,也是打個(gè)平手。最后,RN 原生組件繪制有平臺(tái)的優(yōu)化加成, 相對于 Flutter 自繪引擎繪制,可能還會(huì)好上一些。

除了顯著提升線程之間的通信性能外,這種新架構(gòu)還讓我們可以直接控制 Native Modules。也就是說,可以僅在需要時(shí)使用原生模塊,而不是在啟動(dòng)應(yīng)用程序時(shí)將它們?nèi)考せ睢?/p>

這為應(yīng)用程序啟動(dòng)時(shí)間提供了顯著的性能改進(jìn)。這種新機(jī)制有可能在許多不同的用例中使我們受益。例如,現(xiàn)在我們掌握了 C++ 的強(qiáng)大功能,很容易看出 React Native 是如何用于大型系統(tǒng)目標(biāo)的。

五、參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文鏈接 一、JavaScriptCore 講React Native之前,了解JavaScriptCore會(huì)有幫...
    peaktan閱讀 6,281評(píng)論 2 27
  • 使用App.png 本文結(jié)構(gòu) 目前App的幾種常見的開發(fā)模式 關(guān)于React-Native的一點(diǎn)小看法 React...
    ZeroJ閱讀 5,568評(píng)論 0 22
  • 前言 在《一篇文章詳解React Native初始化和通信機(jī)制》[https://cloud.tencent.co...
    VV木公子閱讀 9,885評(píng)論 0 14
  • 運(yùn)用一個(gè)架構(gòu),總得了解一下背后的原理,本文基于react-native的最新版本(0.47.1),簡要探究一下re...
    木中木閱讀 1,369評(píng)論 2 3
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn),也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 8,177評(píng)論 0 4

友情鏈接更多精彩內(nèi)容