React Native 核心渲染流程分析(1) - 初识组件系统

本文基于源码:0.40.0

最近把yoga源码看了一遍,它是一个按照Flexbox规范,利用Web熟悉的API做高效measure的库。本来想做个它在React Native for Android (以下简称RN4A)中的应用与分析,但是在这之前应该先将RN渲染流程搞懂,我们才能去进一步分析如何应用yoga去辅助测量。

JSX的转码

如果只是开发Web应用,都要给babel添加React preset,如果是开发RN,那么它都内置做好了,使用者体会不到。这个preset的作用,就是将JSX语法转化为纯js的代码。转码的结果可以参考React Without JSX

我拿了一个最普通的文件来做转码示例,原始的React代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class Sample extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
</View>
);
}
}
AppRegistry.registerComponent('Sample', () => Sample);

转码后:(经过部分精简)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactNative = require('react-native');
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {default: obj};
}
// ...
Sample = function (_Component) {
_createClass(Sample, [{
key: 'render', value: function render() {
return (
_react2.default.createElement(_reactNative.View, {style: styles.container},
_react2.default.createElement(_reactNative.Text, {style: styles.welcome}, 'Welcome to React Native!')));
}
}]);
return Sample;
}(_react.Component);
_reactNative.AppRegistry.registerComponent('Sample', function () {
return Sample;
});

我们可以看到,原先JSX的控件都被React.createElement转化为ReactElement。在ReactElement中使用type字段存放原始对象(在此处就是ReactNative.View/ReactNative.Text),使用props存放childrens、其他传入属性等。

JSX仅是一个语法糖,实际上它们最终都是React.createElement这样的原始写法

ReactNative的js启动流程

渲染是一个流程,它不会平白无故发生,总是有触发时机。第一次渲染在启动的时候,也是最容易把控到痕迹的时候,我们可以通过它去一步步剖析这个流程:

rn-render-start

首先,ReactNative的js代码都需要通过AppRegistry.registerComponent注册对应appkey的Component才能被启动。我们可以在AppRegistry.js中看到它注册了一个对应的回调,在Native启动过程中会通过jsbridge调用AppRegistry.runApplication启动js渲染流程,在js中会调用对应runnable,即后面的renderApplication。(关于RN4A启动流程与jsBridge初始化可以参考:【ReactNative For Android】框架启动核心路径剖析【React Native for Android】jsBridge实现原理)

renderApplication时会将传入的Component变成ReactElement,包裹在AppContainer中,这个AppContainer主要用于外面包围一些Debug用的工具(如红盒)。在这之后如上述流程图中一步步走了下去,没什么其他分支,走到ReactNativeMount中就会有料出现了,我们来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//ReactNativeMount.js
renderComponent: function(
nextElement: ReactElement<*>,
containerTag: number,
callback?: ?(() => void)
): ?ReactComponent<any, any, any> {
// 将Element使用相同顶层Wrapper包裹,render方法返回child(即nextElement)
var nextWrappedElement = React.createElement(
TopLevelWrapper,
{ child: nextElement }
);
// 检查之前是否有节点已mount到目标节点上,若有则进行比较处理
var instance = instantiateReactComponent(nextWrappedElement, false);
// 将mount任务提交入回调Queue
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
instance,
containerTag
);
// ...
return component;
}

这里将传入的Element都用TopLevelWrapper进行封装,但是它直接透传目标给render函数,可以暂时忽略这层。这里通过instantiateReactComponent生成了一个渲染对象实例,将batchedMountComponentIntoNode()方法提交入回调Queue,它里面最终会走到ReactReconciler.mountComponent里面,直接调用instance.mountComponent

接下来就有两处关键地方要理解了:

  1. instantiateReactComponent利用输入的ReactElement生成了什么东西?
  2. 利用instance.mountComponent怎么进行渲染?

如何生成React组件?

先解释一下instantiateReactComponent的作用。

从一定角度上来说,React的组件可以分为两种:

  • 元组件 框架内置的,可以直接用的组件。在RN上可以理解为就是View/Image这种。不同平台有不同的元组件实现,在Web上的组件实现可以参考深入理解react(源码分析),在RN上见后续分析ReactNative的组件
  • 复合组件 用户封装之后的组件,一般可以通过React.createClass来构建,提供render()方法返回渲染目标(ES6中可以继承React.Component)。

在React核心库中提供了instantiateReactComponent.js,供渲染平台调用。它在碰见ReactElement时会根据其中的type生成元组件或者复合组件,逻辑如下:

rn-render-instantiate

在React早先的一个版本中,将代码拆分为React与ReactDOM(见Two Packages: React and React DOM)。核心的React包中包含了基础的createElement/createClass/生命周期等React相关、渲染平台无关的代码。其中绿色的部分是React核心库提供的,蓝色部分是需要渲染平台提供的。

instantiateReactComponent中依次判断了如下流程:

  1. 对象的type如果是string或function(至于为何是function,后面ReactNative的元组件中有解释)时走第一层逻辑。当type为string时,渲染平台可以通过ReactHostComponent.injectGenericComponentClass这个API来注入生成组件逻辑,ReactDOM注入了这层处理,生成ReactDOMComponent,而RN不处理。若type函数的原型链中具有元组件API时,则new一个type实例;否则就生成一个ReactCompositeComponent(即复合组件);
  2. 当对象为string时(此种情况对应直接写string,而不是用jsx包裹起来的语法),也交由渲染平台处理,可以通过ReactHostComponent.injectTextComponentClass来注入组件生成逻辑;
  3. 以上都不符合,则报错。

判断函数对象是否元组件,它的原型链中需要提供以下两个API:

  • mountComponent 在首次渲染组件时调用;
  • receiveComponent 更新组件内容、属性时调用;

ReactCompositeComponent中也包含有这两个API供调用,如在mountComponent时:

rn-render-composite

它会获取render()方法返回的渲染节点,并对它继续走instantiate/mountComponent的流程。如果render()返回的节点还是自定义的复合组件,那这个流程还会向下走,也即:mount流程会递归向下调用直到最后一个元组件

ReactNative的元组件

我们可以拿任意一个UI控件来入手看,就拿最简单的View来看一下,它的代码在Libraries/Components/View/View.js下,其实是一个复合组件,但是它的render方法返回的是一个元组件,看一下相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// View.js
const View = React.createClass({
// 属性声明...
render: function() {
return <RCTView {...this.props} />;
},
});
const RCTView = requireNativeComponent('RCTView', View, {
nativeOnly: {
nativeBackgroundAndroid: true,
nativeForegroundAndroid: true,
}
});

那这个requireNativeComponent做的什么呢?紧接着来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// requireNativeComponent.js
function requireNativeComponent(
viewName: string,
componentInterface?: ?ComponentInterface,
extraConfig?: ?{nativeOnly?: Object},
): Function {
const viewConfig = UIManager[viewName]; // 由Native传入的对应ViewModule配置
viewConfig.uiViewClassName = viewName;
viewConfig.validAttributes = {};
viewConfig.propTypes = componentInterface && componentInterface.propTypes;
// 所有React视图控件的prop都继承View的prop
const nativeProps = {
...UIManager.RCTView.NativeProps,
...viewConfig.NativeProps,
};
return createReactNativeComponentClass(viewConfig);
}

createReactNativeComponentClass.js中,我们可以看到它是返回了一个构造ReactNativeBaseComponent构造函数。这下知道了为什么上面instantiateReactComponent里面使用的是“type是否为函数”的判断了吧。如果type为函数,并且原型链中含有元组件API,它就会用new一个这个函数实例。

ReactNativeBaseComponent会在mountComponent/receiveComponent时对自己的child进行递归调用,以mount为例:

rn-render-rnbase

其中ReactMultiChild负责对节点的children进行递归调用、instantiate/mount,UIManager负责所有生成Native组件的操作。

总结

整个过程就是这样,RN将代码由JSX转化为JS组件,启动过程中利用instantiateReactComponent将ReactElement转化为复合组件ReactCompositeComponent与元组件ReactNativeBaseComponent,利用ReactReconciler对他们进行渲染。

下一篇中会讲述ReactNative如何利用UIManager组建Native的视图结构,敬请期待。