前言
React Native與傳統的HybirdApp最大區別就是拋開WebView,使用JSC+原生組件的方式進行渲染,那么整個App啟動/渲染流程又是怎樣的呢?
React Native啟動流程
首先從組件的角度來看下RN的啟動流程:(Android為例)
- Native初始化,主要流程:ReactNativeHost -> Activity -> ReactRootView -> startReactApplication -> createReactContextInBackground(期間有模塊/UI組件信息收集、JSC初始化等工作)
- 后台異步加載、執行JSBundle
- Native端執行
setupReactContext
初始化React上下文,調用JS端AppRegistry.runApplication(key,params)
,key為模塊/組件名稱,參數包含rootTag、initialProps - 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>
)
}
}
跟端上組件開發一樣,React組件也定義了組件的生命周期:
實例化
getDefaultProps
組件類型首次實例化時初始化默認props屬性,多實例共享getInitialState
實例化時初始化默認state屬性componentWillMount
在渲染之前觸發一次render
渲染函數,返回DOM結構componentDidMount
在渲染之后觸發一次
有需要重新渲染(props變更或者setState改變state時)
componentWillReceiveProps
組件接收到新的props時調用,並將其作為參數nextProps使用,可在此更改組件stateshouldComponentUpdate
判斷是否需要更新組件(在首次渲染期間或者調用了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];
}
這個方法是從端上映射到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)
創建ViewupdateView(int tag, String className, ReadableMap props)
更新ViewmanageChildren(int viewTag, Array moveFrom, Array moveTo, Array addChildTags, Array addAtIndices, Array removeFrom)
批量添加/刪除/移動一個view下面的viewmeasure(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通訊原理