2019 年我第一次在生产项目里用 React Native,当时的版本还是 0.59,光是环境搭建就花了两天。现在 RN 已经迭代到了新架构(Fabric + TurboModules + JSI),底层的 Bridge 通信模型被彻底替换,但核心的设计思路——“Learn once, write anywhere”——一直没变。
这篇文章从移动端跨平台方案的演进讲起,然后拆解 RN 的原理,最后给一个自定义 Renderer 的最小实现,帮你理解 React 是怎么把自己”嫁接”到不同平台上的。
移动端跨平台方案的演进 2021 年的生态格局 根据 JavaScript Rising Stars 2021 :
React Native
lonic
Expo
Quasar
Flipper
Flutter
演进历史 演进路径
Hybrid :WebView 内嵌 H5,通过 JSBridge 调用原生能力。接入成本最低,但体验离原生差距明显
React Native(Weex) :JS 驱动原生渲染,通过 Bridge 异步通信。体验大幅提升,但 Bridge 成为性能瓶颈
Flutter :自绘引擎(Skia),不走平台原生组件。体验最接近原生,但需要学习 Dart 和全新的 widget 体系
RN + Fabric(新架构) :用 JSI 替代 Bridge,JS 和 Native 直接通信;TurboModules 实现按需加载原生模块;Fabric 作为新的渲染引擎
跨端到底”跨”了什么 端的概念:数据获取、状态管理、页面渲染。跨端则需要在这三者之外,额外处理虚拟机、渲染引擎、原生交互、开发环境。
数据获取 还是老三样:fetch、axios、XHR。跨端框架需要保证这些 API 在不同平台上行为一致。
状态管理 React 的 state 模式在 RN 中可以直接复用——这是复用 React 技术栈的最大收益。
页面渲染 VDOM → Yoga(RN 的布局引擎)→ iOS/Android/DOM API。Yoga 是 Facebook 用 C++ 实现的 Flexbox 布局引擎,负责将 React 的样式描述翻译为各平台的布局计算。
虚拟机 RN :JSC(JavaScriptCore),到原生层通过大量 Bridge 调用。新架构中 JSI 允许 JS 直接持有 C++ 对象的引用,省去了 Bridge 的序列化开销。
Flutter :开发期 JIT(热重载),生产期 AOT(编译为原生代码)。
原生交互 JSBridge 是跨端框架的命门——JS 和 Native 之间的通信效率直接决定了框架的性能上限。
React Native 原理:Renderer 的视角 RN 之所以能用 React 语法写原生应用,根本原因在于 React 把自己拆成了三层:
React Core :处理组件、状态、生命周期等核心逻辑
Reconciler :负责 diff 和协调更新
Renderer :负责将 Reconciler 的输出渲染到具体平台
关键是 Renderer 是可替换的。react-dom 是一个 Renderer(渲染到 DOM),react-native 是另一个 Renderer(渲染到原生组件)。只要实现一套 hostConfig,你甚至能写一个渲染到终端字符画的 Renderer。
hostConfig 定义了 Reconciler 与宿主平台交互的全部接口:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 HostConfig.getPublicInstance HostConfig.getRootHostContext HostConfig.getChildHostContext HostConfig.prepareForCommit HostConfig.resetAfterCommit HostConfig.createInstance HostConfig.appendInitialChild HostConfig.finalizeInitialChildren HostConfig.prepareUpdate HostConfig.shouldSetTextContent HostConfig.shouldDeprioritizeSubtree HostConfig.createTextInstance HostConfig.scheduleDeferredCallback HostConfig.cancelDeferredCallback HostConfig.setTimeout HostConfig.clearTimeout HostConfig.noTimeout HostConfig.now HostConfig.isPrimaryRenderer HostConfig.supportsMutation HostConfig.supportsPersistence HostConfig.supportsHydration // ------------------- // Mutation // (optional) // ------------------- HostConfig.appendChild HostConfig.appendChildToContainer HostConfig.commitTextUpdate HostConfig.commitMount HostConfig.commitUpdate HostConfig.insertBefore HostConfig.insertInContainerBefore HostConfig.removeChild HostConfig.removeChildFromContainer HostConfig.resetTextContent HostConfig.hideInstance HostConfig.hideTextInstance HostConfig.unhideInstance HostConfig.unhideTextInstance // ------------------- // Persistence // (optional) // ------------------- HostConfig.cloneInstance HostConfig.createContainerChildSet HostConfig.appendChildToContainerChildSet HostConfig.finalizeContainerChildren HostConfig.replaceContainerChildren HostConfig.cloneHiddenInstance HostConfig.cloneUnhiddenInstance HostConfig.createHiddenTextInstance // ------------------- // Hydration // (optional) // ------------------- HostConfig.canHydrateInstance HostConfig.canHydrateTextInstance HostConfig.getNextHydratableSibling HostConfig.getFirstHydratableChild HostConfig.hydrateInstance HostConfig.hydrateTextInstance HostConfig.didNotMatchHydratedContainerTextInstance HostConfig.didNotMatchHydratedTextInstance HostConfig.didNotHydrateContainerInstance HostConfig.didNotHydrateInstance HostConfig.didNotFindHydratableContainerInstance HostConfig.didNotFindHydratableContainerTextInstance HostConfig.didNotFindHydratableInstance HostConfig.didNotFindHydratableTextInstance
从 AppRegistry 到原生渲染 RN 应用的 JS 入口是 AppRegistry.registerComponent:
1 AppRegistry .registerComponent (appName, () => App );
注册后,原生端调用 AppRegistry.runApplication 启动应用,最终走到 renderApplication → ReactNativeRenderer.render(),调用链为:
render → updateContainer → scheduleUpdateOnFiber → performSyncWorkOnRoot → renderRootSync → workLoopSync → performUnitOfWork → completeWork → createInstance
在 createInstance 中,React 调用 UIManager.createView 创建原生视图:
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 registerComponent ( appKey : string, componentProvider : ComponentProvider , section?: boolean, ): string { let scopedPerformanceLogger = createPerformanceLogger (); runnables[appKey] = { componentProvider, run : (appParameters, displayMode ) => { const concurrentRootEnabled = appParameters.initialProps ?.concurrentRoot || appParameters.concurrentRoot ; renderApplication ( componentProviderInstrumentationHook ( componentProvider, scopedPerformanceLogger, ), appParameters.initialProps , appParameters.rootTag , wrapperComponentProvider && wrapperComponentProvider (appParameters), appParameters.fabric , showArchitectureIndicator, scopedPerformanceLogger, appKey === 'LogBox' , appKey, coerceDisplayMode (displayMode), concurrentRootEnabled, ); }, }; if (section) { sections[appKey] = runnables[appKey]; } return appKey; }, runApplication ( appKey : string, appParameters : any, displayMode?: number, ): void { if (appKey !== 'LogBox' ) { const logParams = __DEV__ ? '" with ' + JSON .stringify (appParameters) : '' ; const msg = 'Running "' + appKey + logParams; infoLog (msg); BugReporting .addSource ( 'AppRegistry.runApplication' + runCount++, () => msg, ); } invariant ( runnables[appKey] && runnables[appKey].run , `"${appKey} " has not been registered. This can happen if:\n` + '* Metro (the local dev server) is run from the wrong folder. ' + 'Check if Metro is running, stop it and restart it in the current project.\n' + "* A module failed to load due to an error and `AppRegistry.registerComponent` wasn't called." , ); SceneTracker .setActiveScene ({name : appKey}); runnables[appKey].run (appParameters, displayMode); },
Libraries/ReactNative/renderApplication.js 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 function renderApplication<Props : Object >( RootComponent : React .ComponentType <Props >, initialProps : Props , rootTag : any, WrapperComponent ?: ?React .ComponentType <any>, fabric?: boolean, showArchitectureIndicator?: boolean, scopedPerformanceLogger?: IPerformanceLogger , isLogBox?: boolean, debugName?: string, displayMode?: ?DisplayModeType , useConcurrentRoot?: boolean, ) { invariant (rootTag, 'Expect to have a valid rootTag, instead got ' , rootTag); const performanceLogger = scopedPerformanceLogger ?? GlobalPerformanceLogger ; let renderable = ( <PerformanceLoggerContext.Provider value ={performanceLogger} > <AppContainer rootTag ={rootTag} fabric ={fabric} showArchitectureIndicator ={showArchitectureIndicator} WrapperComponent ={WrapperComponent} initialProps ={initialProps ?? Object.freeze ({})} internal_excludeLogBox ={isLogBox} > <RootComponent {...initialProps } rootTag ={rootTag} /> </AppContainer > </PerformanceLoggerContext.Provider > ); if (__DEV__ && debugName) { const RootComponentWithMeaningfulName = getCachedComponentWithDebugName ( `${debugName} (RootComponent)` , ); renderable = ( <RootComponentWithMeaningfulName > {renderable} </RootComponentWithMeaningfulName > ); } performanceLogger.startTimespan ('renderApplication_React_render' ); performanceLogger.setExtra ('usedReactFabric' , fabric ? '1' : '0' ); if (fabric) { require ('../Renderer/shims/ReactFabric' ).render ( renderable, rootTag, null , useConcurrentRoot, ); } else { require ('../Renderer/shims/ReactNative' ).render (renderable, rootTag); } performanceLogger.stopTimespan ('renderApplication_React_render' ); }
Libraries/Renderer/implementations/ReactNativeRenderer-dev.js 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 function render (element, containerTag, callback ) { var root = roots.get (containerTag); if (!root) { root = createContainer (containerTag, LegacyRoot , false , null , false ); roots.set (containerTag, root); } updateContainer (element, root, null , callback); return getPublicRootInstance (root); } function createInstance ( type, props, rootContainerInstance, hostContext, internalInstanceHandle ) { var tag = allocateTag (); var viewConfig = getViewConfigForType (type); { for (var key in viewConfig.validAttributes ) { if (props.hasOwnProperty (key)) { ReactNativePrivateInterface .deepFreezeAndThrowOnMutationInDev ( props[key] ); } } } var updatePayload = create (props, viewConfig.validAttributes ); ReactNativePrivateInterface .UIManager .createView ( tag, viewConfig.uiViewClassName , rootContainerInstance, updatePayload ); var component = new ReactNativeFiberHostComponent ( tag, viewConfig, internalInstanceHandle ); precacheFiberNode (internalInstanceHandle, tag); updateFiberProps (tag, props); return component; }
UIManager.createView 就是 JS 和 Native 的边界——这一行代码会通过 Bridge(或新架构中的 JSI)调用原生侧的视图创建方法。
完整的渲染链路:
实现一个简易 Renderer 用 react-reconciler 写一个自定义 Renderer,可以帮你理解 RN 到底做了什么。下面是一个渲染到 DOM 的最小实现——换掉 DOM 相关代码,就能渲染到任意平台:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import ReactReconciler from "react-reconciler" ;const ReactReconcilerInst = ReactReconciler (hostConfig);export default { render : (reactElement, domElement, callback ) => { if (!domElement._rootContainer ) { domElement._rootContainer = ReactReconcilerInst .createContainer (domElement, false ); } return ReactReconcilerInst .updateContainer (reactElement, domElement._rootContainer , null , callback); } };
关键的 hostConfig 实现了 Reconciler 需要的所有平台相关操作——createInstance 创建 DOM 元素、appendChild 插入节点、commitUpdate 更新属性。换一套 hostConfig,同样的 React 代码就能渲染到 Canvas、终端、甚至 PDF。
RN 就是这样做跨端的:React 负责组件逻辑和 diff,Renderer 负责把结果翻译给不同平台。理解了这一层,你就理解了所有 React 跨端方案(RN、Remax、Rax)的共通骨架。