React-Native 渲染實現分析

rceb9168 8年前發布 | 13K 次閱讀 ReactNative 移動開發 React Native

前言

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

React Native啟動流程

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

  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 渲染整個應用

renderApplication 函數中會執行:

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

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

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

AppRegistry.registerComponent('TiebaNext', rootComponent);

*僅在JS端處理,記錄在一個Map中。

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

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

iOS端定義啟動組件:

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>
    )
}

}</code></pre>

跟端上組件開發一樣,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(單例,管理類)創建實例。

* 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的時候調用 ReactNativeBaseComponent 下 receiveComponent -> 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]; }</code></pre>

這個方法是從端上映射到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;
}

}</code></pre>

然后在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; } }</code></pre>

    這樣就可以從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。

     

     

    來自:http://www.cnblogs.com/zhang740/p/5978323.html

     

 本文由用戶 rceb9168 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!