React-Native

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 启动应用,最终走到 renderApplicationReactNativeRenderer.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
// 22976
function render(element, containerTag, callback) {
var root = roots.get(containerTag);

if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = createContainer(containerTag, LegacyRoot, false, null, false);
roots.set(containerTag, root);
}

updateContainer(element, root, null, callback); // $FlowIssue Flow has hardcoded values for React DOM that don't work with RN

return getPublicRootInstance(root);
}
// updateContainer
// scheduleUpdateOnFiber
// performSyncWorkOnRoot
// renderRootSync
// workLoopSync
// performUnitOfWork
// completeWork
// -HostComponent-createInstance
// -> 一直走到 createInstance

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, // reactTag
viewConfig.uiViewClassName, // viewName
rootContainerInstance, // rootTag
updatePayload // props
);
var component = new ReactNativeFiberHostComponent(
tag,
viewConfig,
internalInstanceHandle
);
precacheFiberNode(internalInstanceHandle, tag);
updateFiberProps(tag, props); // Not sure how to avoid this cast. Flow is okay if the component is defined
// in the same file but if it's external it can't see the types.

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";

// ... (完整的 hostConfig 实现见下文)

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)的共通骨架。