• 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渲染整个应用

    RN启动流程

    renderApplication函数中会执行:

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

    其中ReactNative是在React库中定义的,AppContainer是一个JS组件,使用View包裹了根组件,开发时工具InspectorYellowBox都是在这个组件中加载,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 {
        // 组件宽度
         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,
                    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,
                    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 }, "u4F60u7684u7F51u7EDCu597Du50CFu4E0Du7ED9u529B"),
                React.createElement(Text, { style: Styles.text }, "u70B9u51FBu6309u94AEu5237u65B0")),
            React.createElement(TouchableOpacity, { style: Styles.button, onPress: this.props.onClickRefresh },
                React.createElement(Text, { style: Styles.buttonText }, "u5237u65B0"))));
    }
    

    这下清晰多了吧?

    React.createElement的方法签名:

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

    ReactNative的UI组件通过requireNativeComponentcreateReactNativeComponentClassReactNativeBaseComponent下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通讯原理

  • 相关阅读:
    Windows 8实例教程系列 开篇
    qt 开发发布于 windeploy.exe
    qt qoci 测试验证
    vmware vmx 版本不兼容
    qt oracle
    vc qt dll
    QOCIDriver unable to create environment
    qoci 编译完 放置位置 具体根据情况
    calling 'lastError' with incomplete return type 'QSqlError' qsqlquer
    Hbase 操作工具类
  • 原文地址:https://www.cnblogs.com/zhang740/p/5978323.html
Copyright © 2020-2023  润新知