众所周知,RN和H5的区别在于:RN是使用Native组件来渲染的,而H5是依赖WebView。那么RN是如何做到写js代码,渲染Native组件的呢,这篇文章我们深入源码,一探究竟。使用的RN版本是v0.62.0
JS侧的UI是使用React来实现的。熟悉React的同学,都知道React使用jsx来写布局,然后会转换成虚拟dom树,最后再渲染到浏览器的真实dom里,那React Native是怎么做的呢?
为了方便阅读,这里先把文中提到的一些函数列出来:
一、启动
以默认的demo为例,我们的代码入口在App.js,并且导出了jsx渲染函数。
const App = () => { return ( <View key={'view-parent'} style={styles.parent}><Text key={'text-1'} style={styles.text1}> Hello World!Text><Text key={'text-2'} style={styles.text2}> zey RN TestText>View> ); }; export default App;
在根目录的index.js里会把这个函数根据appName,注册进去。
import {AppRegistry} from 'react-native'; import App from './App'; import {name as appName} from './app.json'; AppRegistry.registerComponent(appName, () => App);
看下注册函数:
registerComponent( appKey: string, componentProvider: ComponentProvider, section?: boolean, ): string { let scopedPerformanceLogger = createPerformanceLogger(); //存在runnales里 runnables[appKey] = { componentProvider, run: appParameters => { //运行run的时候,开始执行渲染。 renderApplication( componentProviderInstrumentationHook( componentProvider, scopedPerformanceLogger, .....//省略 ) ); }, }; .....//省略 return appKey; },
这里把渲染函数存在runnables对象里。那么,是什么时候开始执行这里注册的runnables呢?这里就和客户端的调用有关了。
在启动RN页面时,客户端内部会调用下面这行代码,调用runApplication,传入对应的appName和一些参数。
catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);
然后在js里AppRegistry.js就会执行对应名称的注册函数:
runApplication(appKey: string, appParameters: any): void { .....//省略 runnables[appKey].run(appParameters); },
可以看到走到了renderApplication里执行渲染逻辑。
再跟进去会发现有两个选择,ReactFabric和ReactNative,Fabric就是RN的新架构,现在最新的代码还是用的ReactNative:
GlobalPerformanceLogger.startTimespan('renderApplication_React_render');//渲染计时开始 if (fabric) { require('../Renderer/shims/ReactFabric').render(renderable, rootTag);//令人期待的新框架Fabric } else { require('../Renderer/shims/ReactNative').render(renderable, rootTag); } GlobalPerformanceLogger.stopTimespan('renderApplication_React_render');//渲染计时结束
二、遍历虚拟dom
接下来的渲染逻辑就和React非常相关了。(其实就是React的代码
进入Render函数:
render: function(element, containerTag, callback) { var root = roots.get(containerTag); if (!root) { root = createContainer(containerTag, LegacyRoot, false, null); roots.set(containerTag, root); } updateContainer(element, root, null, callback); //进入这里继续执行渲染 return getPublicRootInstance(root); }
在updateConainer里会创建一个update,并且插入到队列里,然后执行队列,接下来就是对组件树的遍历了。
var update = createUpdate(expirationTime, suspenseConfig); update.payload = { element: element }; enqueueUpdate(current$$1, update); scheduleWork(current$$1, expirationTime);
接下来就是不停地检查、插入队列、根据优先级处理(但这里是串行的,并没有异步),这里省略具体代码,依次的函数调用顺序如下:
updateContainer scheduleUpdateOnFiber flushSyncCallbackQueue flushSyncCallbackQueueImpl runWithPriority performSyncWorkOnRoot workLoopSync
在workLoopSync里,我们可以看到一个while循环,这里就开始遍历组件树了
function workLoopSync() { while (workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress); } }
react遍历树的时候有两个重要的函数performUnitOfWork和completeUnitOfWork。我理解performUnitOfWork就是深度遍历到底,然后执行completeUnitOfWork回退,同时创建对应的dom/Native组件。也就是先创建的子节点再创建父节点的。
看下performUnitOfWork的部分代码:
function performUnitOfWork(unitOfWork) { //开始处理,会返回子组件fiber实例,用于深度循环遍历,把任务加入队列 next = beginWork$$1(current$$1, unitOfWork, renderExpirationTime); if (next === null) { // 不存在子级fiber,完成当前单元任务的处理。 next = completeUnitOfWork(unitOfWork); } return next; }
这样就完成了一个子集任务的内容。
在completeUnitOfWork里,主要就是找父组件回退、找兄弟组件继续遍历:
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { workInProgress = unitOfWork; do { //完成当前的工作 next = completeWork(current, workInProgress, renderExpirationTime); //兄弟组件 const siblingFiber = workInProgress.sibling; if (siblingFiber !== null) { //返回兄弟组件继续遍历 return siblingFiber; } //否则回到父组件继续完成工作 workInProgress = returnFiber; } while (workInProgress !== null); return null; }
从这里可以看出,React遍历组件树深度遍历走到底了,就算作一个单元,完成当前的渲染工作。
这样做的好处是,可以把遍历工作分散成小单元工作。这也是Fiber的一个重要设计思路。可以避免一次渲染大量组件而阻塞了线程。导致用户操作没有响应。更多关于组件Fiber链表和遍历的介绍可以看这个文章
三、创建Native组件
创建组件是在completeWork里完成的。里面有很多不同类型的组件。里面涉及创建真实渲染的Dom或Native组件的是HostComponent,这个组件最后会调用createInstance来创建组件。
激动人心,终于要创建组件了!
function createInstance() { var updatePayload = create(props, viewConfig.validAttributes); ReactNativePrivateInterface.UIManager.createView( tag, // reactTag viewConfig.uiViewClassName, // viewName rootContainerInstance, // rootTag updatePayload // props ); var component = new ReactNativeFiberHostComponent(tag, viewConfig); return component; }
这里主要是调用了UIManager的createView方法,传入了tag、viewName、rootTag、props参数信息。这里的UIManager实际上是映射到Java里的一个class— UIManagerModule. 对应的函数如下:
public void createView(int tag, String className, int rootViewTag, ReadableMap props) { mUIImplementation.createView(tag, className, rootViewTag, props); }
mUIImplementation的createView是这样的:
public void createView(int tag, String className, int rootViewTag, ReadableMap props) { ReactShadowNode cssNode = createShadowNode(className); ReactShadowNode rootNode = mShadowNodeRegistry.getNode(rootViewTag); cssNode.setReactTag(tag); // Thread safety needed here cssNode.setViewClassName(className); cssNode.setRootTag(rootNode.getReactTag()); cssNode.setThemedContext(rootNode.getThemedContext()); mShadowNodeRegistry.addNode(cssNode); ReactStylesDiffMap styles = null; if (props != null) { //这里这里!!!---元素的样式解析、赋值 styles = new ReactStylesDiffMap(props); cssNode.updateProperties(styles); } handleCreateView(cssNode, rootViewTag, styles); } }
这一步主要是把样式和一些配置解析出来,方便下一步渲染。
执行handleCreateView之后,会把这些信息转换成一个对象,放入队列里,等待执行。
我们直接跳到执行创建的地方:
NativeViewHierarchyManager.java
public synchronized void createView( ThemedReactContext themedContext,int tag, String className, @Nullable ReactStylesDiffMap initialProps) { ViewManager viewManager = mViewManagers.get(className); View view = viewManager.createView(themedContext, null, null, mJSResponderHandler); viewManager.updateProperties(view, initialProps); }
可以看出,创建是通过ViewManager来创建View的,这个ViewManager有很多不同的实现类用来实现不同的Native组件,除了官方提供的常见的以外,业务方也可以根据自己需求实现。比如文本Text标签,对应的ViewManager实现是ReactTextViewManager对象。如此得以创建真实的Native节点。
四、操作组件
上面的流程创建出了Native的组件,但是仅仅创建是不行的。还需要根据父子关系来把子组件添加到父组件里面。
所以createInstance之后又会调用UIManager.setChildren来设置组件的父子关系:
对应的安卓代码如下:
/** * 快速添加初始View的接口,子View 的tag 被认为是按元素顺序排列的 * @param viewTag the view tag of the parent view * @param childrenTags An array of tags to add to the parent in order */ @ReactMethod public void setChildren(int viewTag, ReadableArray childrenTags)
因为是UI创建阶段,所以只需要执行添加就可以了。setChildren相当于一个简易操作UI的实现。
对UI的操作还有移动、移除等,就需要用到manageChildren:
/** * 响应js 添加、移除、移动 父元素的views * * @param viewTag 父元素的tag * @param moveFrom 一个index列表,从哪里移动 * @param moveTo 和moveFrom对应, 一个index列表,元素移动到哪里 * @param addChildTags 添加到父元素的 view的tag 列表 * @param addAtIndices 和addChildTags,指明这些元素插入到哪里的一个index列表。 * @param removeFrom 一个元素永久移除的列表。对应元素的内容会被回收 */ @ReactMethod public void manageChildren( int viewTag, @Nullable ReadableArray moveFrom, @Nullable ReadableArray moveTo, @Nullable ReadableArray addChildTags, @Nullable ReadableArray addAtIndices, @Nullable ReadableArray removeFrom)
而如果是修改元素的样式,比如文字颜色、文字大小等,用到的是updateView
我们来举个例子了解一下流程:
下面是一个RN的demo,其中上面两个是Text元素,下面是一个Button。点击Button之后左边的Hello World会改变颜色。
通过log可以看出渲染时元素的创建、添加、更新等步骤:
1.创建Hello World文字:
//创建文字 (虚拟node ,没有创建真实View) Hello World (UIManager.createView) tag: 3, class: RCTRawText, props: { NativeMap: {"text":"Hello World!"} } //创建ReactTextView (UIManager.createView) tag: 5, class: RCTText, props: { NativeMap: {"ellipsizeMode":"tail","allowFontScaling":true,"accessible":true,"lineHeight":50,"height":50,"textAlign":"center","backgroundColor":-7876885,"flex":1,"color":-16776961} } //添加 文字(3) 到 ReactTextView(5) (UIManager.setChildren) tag: 5, children: [3]
2.创建zey RN Test文字:
//创建文字 zey RN Test (UIManager.createView) tag: 7, class: RCTRawText, props: { NativeMap: {"text":"zey RN Test"} } //创建ReactTextView (UIManager.createView) tag: 9, class: RCTText, props: { NativeMap: {"ellipsizeMode":"tail","allowFontScaling":true,"accessible":true,"lineHeight":50,"height":50,"textAlign":"center","backgroundColor":-12156236,"flex":1,"color":-65536} } //添加 文字(7)到 ReactTextView(9) (UIManager.setChildren) tag: 9, children: [7]
3.把上面两个文字放入到Flex容器中:
//接下来创建的是两个文字的父元素,用了Flex布局 (UIManager.createView) tag: 13, class: RCTView, props: { NativeMap: {"backgroundColor":-5185306,"flexDirection":"row","marginTop":20,"display":"flex"} } //添加5、9两个文字元素到父元素里 (UIManager.setChildren) tag: 13, children: [5,9]
4.创建下面的Button:
//创建文字 CLICK ME (UIManager.createView) tag: 15, class: RCTRawText, props: { NativeMap: {"text":"CLICK ME"} } //创建ReactTextView 下面那个按钮里的 (UIManager.createView) tag: 17, class: RCTText, props: { NativeMap: {"ellipsizeMode":"tail","allowFontScaling":true,"accessible":true,"fontWeight":"500","color":-1,"margin":8,"textAlign":"center"} } //文字添加到ReactTextView里 (UIManager.setChildren) tag: 17, children: [15] //下面的button外面还有一层View包裹 (UIManager.createView) tag: 19, class: RCTView, props: { NativeMap: {"focusable":true,"accessibilityState":{},"accessibilityRole":"button","accessible":true,"nativeBackgroundAndroid":{"attribute":"selectableItemBackground","type":"ThemeAttrAndroid"},"borderRadius":2,"backgroundColor":-14575885,"elevation":4} } //添加到容器 (UIManager.setChildren) tag: 19, children: [17]
5.添加到整体的父容器里:
//整体的父元素 (UIManager.createView) tag: 23, class: RCTView, props: { NativeMap: {"flex":1,"pointerEvents":"box-none","collapsable":true} } (UIManager.setChildren) tag: 23, children: [13,19] //再包裹了一层 (UIManager.createView) tag: 25, class: RCTView, props: { NativeMap: {"pointerEvents":"box-none","flex":1} } (UIManager.setChildren) tag: 25, children: [23] //最后添加到root上 (UIManager.setChildren) tag: 1, children: [25]
6.点击按钮改变颜色:
//点击“CLICK ME”之后,会改变'Hello World'的颜色,触发了updateView (UIManager.updateView) tag: 5, class: RCTText, props: { NativeMap: {"color":-39394} }
到这里就介绍了一下React Native的大致渲染原理。若有错误和不足的地方欢迎指出~
还有个有意思的问题是,React和React Native本是同根生,是怎么做到同样的渲染逻辑,渲染出不同的组件的呢?通过源码就可以看得一清二楚了。
打开React库的代码,在ReactFiberCompleteWork.js:
所有的渲染实际Dom相关的函数从一个文件获得。
import { createInstance, createTextInstance, appendInitialChild, finalizeInitialChildren, prepareUpdate, supportsMutation, supportsPersistence, cloneInstance, cloneHiddenInstance, cloneHiddenTextInstance, createContainerChildSet, appendChildToContainerChildSet, finalizeContainerChildren, getFundamentalComponentInstance, mountFundamentalComponent, cloneFundamentalInstance, shouldUpdateFundamentalComponent, } from './ReactFiberHostConfig';
这个ReactFiberHostConfig文件会根据实际渲染的内容,映射到对应的文件,从而实现不同的渲染方式,有如下这些文件:React Native有两种方式一个是.native.js一个是.fabric.js
参考文章:
View是如何创建的(https://maxiee.github.io/post/ReactNativeCode13md/)
「ReactNative」View创建过程浅析(https://juejin.im/post/5bfbaaf1f265da615a417f69)
React Fiber初探(https://juejin.im/post/5a2276d5518825619a027f57#heading-24)
The how and why on React’s usage of linked list in Fiber to walk the component’s tree(https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7)