React-Native 渲染實現分析

前言

React Native與傳統的HybirdApp最大區別就是拋開WebView,使用JSC+原生組件的方式進行渲染,那麼整個App啓動/渲染流程又是怎樣的呢?javascript

React Native啓動流程

首先從組件的角度來看下RN的啓動流程:(Android爲例)java

  1. Native初始化,主要流程:ReactNativeHost -> Activity -> ReactRootView -> startReactApplication -> createReactContextInBackground(期間有模塊/UI組件信息收集、JSC初始化等工做)
  2. 後臺異步加載、執行JSBundle
  3. Native端執行setupReactContext初始化React上下文,調用JS端AppRegistry.runApplication(key,params),key爲模塊/組件名稱,參數包含rootTag、initialProps
  4. JS端找到註冊的對應啓動組件,執行renderApplication渲染整個應用

RN啓動流程

renderApplication函數中會執行:react

ReactNative.render(
  <AppContainer>
    <RootComponent
      {...initialProps}
      rootTag={rootTag}
    />
  </AppContainer>,
  rootTag
);

其中ReactNative是在React庫中定義的,AppContainer是一個JS組件,使用View包裹了根組件,開發時工具InspectorYellowBox都是在這個組件中加載,RootComponent是傳入的根組件。react-native

JS端註冊組件:(在第2步執行JSBundle時)網絡

AppRegistry.registerComponent('TiebaNext', rootComponent);

*僅在JS端處理,記錄在一個Map中。數據結構

Android端定義啓動組件,Activity中,繼承ReactActivity:(在第1步時調用)異步

@Override
protected String getMainComponentName() {
  return "TiebaNext";
}

iOS端定義啓動組件:ide

self.rctRootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                        moduleName:@"TiebaNext"
                                        initialProperties:nil
                                        launchOptions:nil];

簡單說就是Native初始化 -> 加載JS,JS端註冊組件 -> 端上調用JS端run方法,傳入入口組件名稱 -> JS端啓動渲染流程。函數

React Native渲染流程

React的渲染都是以組件爲單位,上面已經分析了,啓動的最後階段就是JS端開始渲染根組件。首先咱們先看下React的組件是怎麼編寫的,以及他的生命週期:(熟悉React可略過)工具

一個例子,無網絡提示組件:

無網絡提示組件

(例子語言Typescript)

// 組件的屬性定義
interface PropsDefine {
    // 組件寬度
    width: number
    // 組件高度
    height: number
    // 點擊刷新按鈕回調,可選
    onClickRefresh?: () => void
}
export class NoNetwork extends React.Component<PropsDefine, {}> { // 組件無狀態,定義爲空:{}
    // 組件的默認屬性定義,單例,實例間共享
    static defaultProps = {
        onClickRefresh: () => { }
    }

    render() {
        let {width, height} = this.props

        return (
            <View style={[Styles.panel, {
                width: width,
                height: height,
            }]}>
                <View style={Styles.picBlock}>
                    <Image source={Styles.picUrl}/>
                </View>
                <View style={Styles.textBlock}>
                    <Text style={Styles.text}>你的網絡好像不給力</Text>
                    <Text style={Styles.text}>點擊按鈕刷新</Text>
                </View>
                <TouchableOpacity style={Styles.button} onPress={this.props.onClickRefresh}>
                    <Text style={Styles.buttonText}>刷新</Text>
                </TouchableOpacity>
            </View>
        )
    }
}

跟端上組件開發同樣,React組件也定義了組件的生命週期:

實例化

  • getDefaultProps
    組件類型首次實例化時初始化默認props屬性,多實例共享
  • getInitialState
    實例化時初始化默認state屬性
  • componentWillMount
    在渲染以前觸發一次
  • render
    渲染函數,返回DOM結構
  • componentDidMount
    在渲染以後觸發一次

有須要從新渲染(props變動或者setState改變state時)

  • componentWillReceiveProps
    組件接收到新的props時調用,並將其做爲參數nextProps使用,可在此更改組件state
  • shouldComponentUpdate
    判斷是否須要更新組件(在首次渲染期間或者調用了forceUpdate方法後,該方法不會被調用)
  • componentWillUpdate
    更新渲染前調用
  • render
    渲染函數,返回DOM結構
  • componentDidUpdate
    更新渲染後調用

銷燬

  • componentWillUnmount
    組件移除以前調用

那麼這個組件究竟是怎麼用原生組件渲染的呢?首先咱們先來看看最主要的render作了什麼。jsx不太直觀,咱們先翻譯一下render:

render() {
    let { width, height } = this.props;
    return (React.createElement(View, { style: [Styles.panel, {
                width: width,
                height: height,
            }] },
        React.createElement(View, { style: Styles.picBlock },
            React.createElement(Image, { source: Styles.picUrl })),
        React.createElement(View, { style: Styles.textBlock },
            React.createElement(Text, { style: Styles.text }, "\u4F60\u7684\u7F51\u7EDC\u597D\u50CF\u4E0D\u7ED9\u529B"),
            React.createElement(Text, { style: Styles.text }, "\u70B9\u51FB\u6309\u94AE\u5237\u65B0")),
        React.createElement(TouchableOpacity, { style: Styles.button, onPress: this.props.onClickRefresh },
            React.createElement(Text, { style: Styles.buttonText }, "\u5237\u65B0"))));
}

這下清晰多了吧?

React.createElement的方法簽名:

ReactElement.createElement = function (type, config, children){ ... }

ReactNative的UI組件經過requireNativeComponent -> createReactNativeComponentClass -> ReactNativeBaseComponent下mountComponent的調用關係,最終在mountComponent中調用UIManager組件建立View:UIManager.createView(tag, this.viewConfig.uiViewClassName, nativeTopRootTag, updatePayload);,在Native端,UIManager調用對應組件類型的ViewManager(單例,管理類)建立實例。

RN渲染流程

*UIManager是一個NativeModule,待下面分析

接下來咱們來詳細分析下原生組件的實現方法,以Image組件爲例:

iOS和Android實現有必定差別,首先是Image組件JS端代碼,都須要requireNativeComponent加載原生組件:

const RCTImageView = requireNativeComponent('RCTImageView', Image);

Image的JS端實際上也是一個React JS組件,他也有render,返回的是:(iOS)

<RCTImageView
  {...this.props}
  style={style}
  resizeMode={resizeMode}
  tintColor={tintColor}
  source={sources}
/>

由於業務邏輯是寫在JS端的,建立出了Native組件就須要進行控制,天然就涉及到屬性傳遞、方法調用、事件回調這3個需求。

Native組件跟JS端通信方式

JS端組件跟Native真正實現的組件主要涉及三件事:

  • 屬性同步
  • JS端調用Native方法
  • Native事件回調JS端

屬性同步

屬性同步很簡單,其實是在組件從新render的時候調用ReactNativeBaseComponentreceiveComponent -> UIManager.updateView完成的。

屬性同步

JS端調用Native方法

兩種方法,一種是調用NativeModules(後面有簡單分析),若是想直接調用一個具體View的方法,那就須要使用UIManager模塊:

Android端UIManager中的定義:

@ReactMethod
  public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs) {
    mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
  }

iOS端UIManager中的定義:

RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag
                  commandID:(NSInteger)commandID
                  commandArgs:(NSArray<id> *)commandArgs)
{
  RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
  RCTComponentData *componentData = _componentDataByName[shadowView.viewName];
  Class managerClass = componentData.managerClass;
  RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)];
  id<RCTBridgeMethod> method = moduleData.methods[commandID];

  NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs];
  [method invokeWithBridge:_bridge module:componentData.manager arguments:args];
}

這個方法是從端上映射到JS的,因此在JS端能夠這樣調用:

UIManager.dispatchViewManagerCommand(
    findNodeHandle(this), // 找到與NativeUI組件對應的JS組件實例
    UIManager.[UI組件名].Commands.[方法],
    [] // 參數
)

findNodeHandle方法是在React中定義,能夠找到組件實例的reactTag(執行在JS端),UIManager能夠把調用命令分發到Native端對應的組件類型的ViewManager,再經過ViewManager調用View組件實例的對應方法。

Native事件回調JS端

Android端使用的是相似JS端調用Native的方式,使用了事件機制,不過事件的接收者是從JS端映射過來的,React下ReactNativeEventEmitter.receiveEvent(tag, topLevelType, nativeEventParam),因此須要先實現一個Event:(Switch的onValueChange事件)

class ReactSwitchEvent extends Event<ReactSwitchEvent> {
    public static final String EVENT_NAME = "topChange"; // topChange會被映射成onChange,具體映射關係參見 UIManagerModuleConstants.java

    public ReactSwitchEvent(int viewId, boolean isChecked) {
        super(viewId);
        mIsChecked = isChecked;
    }

    public boolean getIsChecked() {
        return mIsChecked;
    }

    @Override
    public String getEventName() {
        return EVENT_NAME;
    }

    @Override
    public short getCoalescingKey() {
        // All switch events for a given view can be coalesced.
        return 0;
    }

    @Override
    public void dispatch(RCTEventEmitter rctEventEmitter) {
        rctEventEmitter.receiveEvent(getViewTag(), getEventName(), serializeEventData());
    }

    private WritableMap serializeEventData() {
        WritableMap eventData = Arguments.createMap();
        eventData.putInt("target", getViewTag());
        eventData.putBoolean("value", getIsChecked());
        return eventData;
    }
}

而後在ViewManager或View中進行事件派發:

ReactContext reactContext = (ReactContext) buttonView.getContext();
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher().dispatchEvent(
    new ReactSwitchEvent(
        buttonView.getId(),
        isChecked));

iOS端實現有所區別,iOS端將JS函數直接映射到Native,因此能夠直接調用(可屢次調用):(View爲RCTSwitch)

// ViewManager中聲明事件爲RCTBubblingEventBlock或RCTDirectEventBlock
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock);

// View中聲明
@property (nonatomic, copy) RCTBubblingEventBlock onChange;

// view實例化時監聽onChange
- (void)onChange:(RCTSwitch *)sender
{
  if (sender.wasOn != sender.on) {
    if (sender.onChange) {
      sender.onChange(@{ @"value": @(sender.on) });
    }
    sender.wasOn = sender.on;
  }
}

這樣就能夠從JS端建立NativeUI組件了,能夠看到UI組件的Native和JS端是經過reactTag進行的關聯,經過UIManager模塊,在Native端的DOM和React的DOM進行同步操做,保持結構一致。

UIManager

模塊數據結構,JS端可訪問:

UIManager.[UI組件名].[Constants(靜態值)/Commands(命令/方法)]

從端上映射的方法:(部分)

  • createView(int tag, String className, int rootViewTag, ReadableMap props)
    建立View
  • updateView(int tag, String className, ReadableMap props)
    更新View
  • manageChildren(int viewTag, Array moveFrom, Array moveTo, Array addChildTags, Array addAtIndices, Array removeFrom)
    批量添加/刪除/移動一個view下面的view
  • measure(int reactTag, Callback callback)
    測量View的位置、size等,結果異步回調
  • measureInWindow(int reactTag, Callback callback)
    測量View相對屏幕的位置、size等,結果異步回調
  • dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs)
    派發View命令,也就是用來調用對應View的方法

這個模塊是NativeModule方式定義的,在RN的JS端啓動時,端上會經過JSC把收集到的模塊信息(名稱)打到JS端全局變量global.__fbBatchedBridgeConfig中,並採用延遲加載策略:設置NativeModules.[模塊名]的getter,延遲經過JSC讀取模塊詳細信息(方法、命令號等信息)。在調用的時候會放到MessageQueue的隊列裏,批量提交,兩次批量提交限制的最小間隔爲5ms。

關於React Native通信更詳盡的分析參見:React Native通信原理

相關文章
相關標籤/搜索