React-Native

课程目标

  1. 会使用 RN,了解 RN 同类别的产品,了解移动端的主要技术方案,有一定的跨端开发经验,踩过一些坑;
  2. 知道如何与 native 进行数据交互,知道 ios 与安卓 jsbridge 实现原理。
  3. 知道移动端 webview 和基础能力,包括但不限于:webview 资源加载优化方案;webview 池管理、独立进程方案;native 路由等。
  4. 能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。

知识要点

移动端跨平台开发方案的演进

当前热点

在 2021 JavaScript Rising Star: 链接

  • React Native
  • lonic
  • Expo
  • Quasar
  • Flipper
  • Flutter

演进历史

  • Hybrid
  • ReactNative (Weex)
  • Flutter
  • RN + Fabric

端与跨端

  • 端:数据获取、状态管理、页面渲染。(FE三板斧)
  • 跨端:虚拟机、渲染引擎、原生交互、开发环境。

数据获取

还是老三样:fetch \ axios \ XHR

状态管理

React 中的 state 模式

页面渲染

vDom -> yoga -> iOS / android / DOM APIs -> iOS / android / Web

虚拟机

RN:
  • JSC - Objective-c / JS
  • 到原生层,用了大量的 bridge
Flutter:
  • JIT(dev) + AOT(prod)
  • Skia

渲染引擎

yoga

原生交互

Jsbridge

React Native 的原理

React 的设计理念

在运行时开发者能够处理 React JSX 的核心基础其实在于 React 的设计理念,React 将自身能力充分解耦,并提供给社区接入关键环节。这里我们需要先进行一些 React 原理解析。

React 的整体设计理念可以分为三个部分:

  • React Core
  • React Renderer
  • Reconciler

在这里我们需要了解的是:

自定义 renderer — 宿主配置 hostConfig — React reconciler — react core

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是所有 React Native 应用的 JS 入口。应用的根组件应当通过AppRegistry.registerComponent方法注册自己,然后原生系统才可以加载应用的代码包并且在启动完成之后通过调用AppRegistry.runApplication来真正运行应用。

1
AppRegistry.registerComponent(appName, () => App);
Libraries/ReactNative/AppRegistry.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
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;
}


Renderer

补充知识点

实现⼀个简易 Render

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import ReactReconciler from "react-reconciler";

function traceWrap(hostConfig) {
let traceWrappedHostConfig = {};
Object.keys(hostConfig).map((key) => {
const func = hostConfig[key];
traceWrappedHostConfig[key] = (...args) => {
console.trace(key);
return func(...args);
};
});
return traceWrappedHostConfig;
}

const rootHostContext = {};
const childHostContext = {};

const hostConfig = {
now: Date.now,
getRootHostContext: () => {
return rootHostContext;
},
prepareForCommit: () => {},
resetAfterCommit: () => {},
getChildHostContext: () => {
return childHostContext;
},
shouldSetTextContent: (type, props) => {
return (
typeof props.children === "string" || typeof props.children === "number"
);
},
/**
This is where react-reconciler wants to create an instance of UI element in terms of the target. Since our target here is the DOM, we will create document.createElement and type is the argument that contains the type string like div or img or h1 etc. The initial values of domElement attributes can be set in this function from the newProps argument
*/
createInstance: (
type,
newProps,
rootContainerInstance,
_currentHostContext,
workInProgress
) => {
const domElement = document.createElement(type);
Object.keys(newProps).forEach((propName) => {
const propValue = newProps[propName];
if (propName === "children") {
if (typeof propValue === "string" || typeof propValue === "number") {
domElement.textContent = propValue;
}
} else if (propName === "onClick") {
domElement.addEventListener("click", propValue);
} else if (propName === "className") {
domElement.setAttribute("class", propValue);
} else {
const propValue = newProps[propName];
domElement.setAttribute(propName, propValue);
}
});
return domElement;
},
createTextInstance: (text) => {
return document.createTextNode(text);
},
appendInitialChild: (parent, child) => {
parent.appendChild(child);
},
appendChild(parent, child) {
parent.appendChild(child);
},
finalizeInitialChildren: (domElement, type, props) => {},
supportsMutation: true,
appendChildToContainer: (parent, child) => {
parent.appendChild(child);
},
prepareUpdate(domElement, oldProps, newProps) {
return true;
},
commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
Object.keys(newProps).forEach((propName) => {
const propValue = newProps[propName];
if (propName === "children") {
if (typeof propValue === "string" || typeof propValue === "number") {
domElement.textContent = propValue;
}
} else {
const propValue = newProps[propName];
domElement.setAttribute(propName, propValue);
}
});
},
commitTextUpdate(textInstance, oldText, newText) {
textInstance.text = newText;
},
removeChild(parentInstance, child) {
parentInstance.removeChild(child);
},
clearContainer() {}
};
const ReactReconcilerInst = ReactReconciler(traceWrap(hostConfig));
// eslint-disable-next-line import/no-anonymous-default-export
export default {
render: (reactElement, domElement, callback) => {
// Create a root Container if it doesnt exist
if (!domElement._rootContainer) {
domElement._rootContainer = ReactReconcilerInst.createContainer(
domElement,
false
);
}

// update the root Container
return ReactReconcilerInst.updateContainer(
reactElement,
domElement._rootContainer,
null,
callback
);
}
};