原生UI组件封装深度解析
从ViewManager生命周期到事件回调,拆解RN原生UI组件封装的完整流程和工程化要点。
一句话概括
RN 原生 UI 组件封装就是「写个原生 View → 配个 ViewManager 管理生命周期 → 在 JS 侧包一层组件」的三件套流程,核心掌握 ViewManager 生命周期、属性同步、事件回传三种机制。
核心知识点
1. 封装三件套——缺一不可
任何原生 UI 组件都由三个文件组成:
1
2
3
4
5
Native View (Android: View子类 / iOS: UIView子类) ← 实际渲染
↕
ViewManager (Android: SimpleViewManager / iOS: RCTViewManager) ← 生命周期、属性、命令
↕
JS 包装层 (requireNativeComponent) ← JSX 中使用
为什么不能跳过 ViewManager 直接用 View? 因为 RN 框架不认识你的原生 View——ViewManager 是 RN 和原生 View 之间的「翻译官」:告诉 RN 这个 View 叫什么名字、有哪些属性、支持哪些命令。
2. ViewManager 生命周期——这四个方法最要紧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyViewManager extends SimpleViewManager<MyView> {
@Override
public String getName() { return "MyView"; } // ① JS侧通过这个名字引用
@Override
protected MyView createViewInstance(ThemedReactContext ctx) {
return new MyView(ctx); // ② 只调用一次,创建原生View
}
@Override
public void onAfterUpdateTransaction(MyView view) {
// ③ 所有属性设置完成后调用,做收尾工作(如触发布局刷新)
}
@Override
public void onDropViewInstance(MyView view) {
super.onDropViewInstance(view);
view.release(); // ④ 清理资源(取消网络请求、注销监听器)
}
}
最容易踩的坑: 在 onDropViewInstance 里忘记清理资源。组件卸载后异步回调还在跑,访问已回收的 View → 野指针 crash。必须把异步任务、监听器、定时器都在这里取消掉。
3. 属性传递——JS 写到原生 View 上
1
2
3
4
5
6
7
8
9
@ReactProp(name = "strokeColor") // JS侧: <MyView strokeColor="#ff0000" />
public void setStrokeColor(MyView view, @Nullable String color) {
view.setStrokeColor(Color.parseColor(color));
}
@ReactProp(name = "strokeWidth", defaultFloat = 2f) // 有默认值,JS 不传就用 2
public void setStrokeWidth(MyView view, float width) {
view.setStrokeWidth(width);
}
关键点:
@ReactProp的方法在组件首次创建时全部调用一次(应用默认值),之后每次属性变化再增量调用- 属性名和 JS 侧的 prop 名保持一致
- 复杂对象用
ReadableMap,数组用ReadableArray
4. 事件回传——原生怎么「告诉」JS
核心 API: 原生侧把事件数据打包,通过 EventDispatcher 发送给 JS。
1
2
3
4
5
6
7
8
9
10
11
12
// 原生侧发送事件
private void emitEvent(String eventName, WritableMap data) {
ReactContext reactContext = (ReactContext) getContext();
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(getId(), eventName, data);
}
// JS侧接收
<MyView
onTouchEvent={(e) => console.log(e.nativeEvent.x, e.nativeEvent.y)}
/>
注意: 组件卸载后不能再发事件。务必在 onDropViewInstance 中把事件发送逻辑关掉,否则 receiveEvent 发送到一个不存在的 viewId → crash。
5. 命令式操作——JS 调原生方法
当属性不够用时(比如「清空画布」这种动作),用命令:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 原生侧注册命令
@Override
public Map<String, Integer> getCommandsMap() {
return Map.of("clear", 1, "undo", 2); // 命令名 → 命令ID
}
@Override
public void receiveCommand(MyView view, int commandId, @Nullable ReadableArray args) {
switch (commandId) {
case 1: view.clear(); break;
case 2: view.undo(); break;
}
}
1
2
3
4
5
6
7
// JS侧调用
const tag = findNodeHandle(ref.current);
UIManager.dispatchViewManagerCommand(
tag,
UIManager.getViewManagerConfig('MyView').Commands.clear,
[]
);
新架构替代方案: Fabric 下命令被 NativeCommands 替代,不需要 findNodeHandle 这套黑魔法了——直接 ref.current.clear()(前提是 Codegen 生成了对应的 TS 类型)。
其实你每天都在用
- 地图组件(react-native-maps): 底层是原生
MapView(Android) /MKMapView(iOS),通过 ViewManager 桥接到 JS——缩放、定位、标注全是原生渲染,所以 60fps - 视频播放器(react-native-video):
ExoPlayer(Android) /AVPlayer(iOS) 封装——缓冲、画中画、倍速这些全靠原生 API,JS 只负责传配置 - WebView(react-native-webview): 原生
WebView/WKWebView的封装——你看到的onMessage、injectJavaScript背后就是receiveEvent+dispatchCommand - 扫码相机:
react-native-camera在原生侧开Camera2/AVCaptureSession,扫码结果通过事件回传给 JS - Lottie 动画:
lottie-react-native封装的LottieAnimationView——JSON 动画文件在原生线程渲染,JS 侧只控制播放/暂停
常见误解(FAQ)
❌ 误区1:「原生组件封装只要 ViewManager 就够了,不需要写 Native View」
ViewManager 是管理者,Native View 是执行者——就像餐厅经理和厨师的关系。经理可以调度但不做菜。必须有一个实际的 View/UIView 子类来做渲染、触摸处理。
❌ 误区2:「属性更新很快,@ReactProp 的方法每次都走 Bridge 也没事」
如果 onScroll 里每帧更新一个属性,每 16ms 就触发一次 Bridge 序列化往返。当用户快速滑动时,Bridge 消息过多会导致 JS 线程追不上,表现为「滑动跟手性差」。高频属性更新应该考虑用原生驱动的事件替代。
❌ 误区3:「iOS 和 Android 的 ViewManager 可以写成一样的」
Android 用 SimpleViewManager<T extends View> + @ReactProp 注解;iOS 用 RCTViewManager + RCT_EXPORT_VIEW_PROPERTY 宏。语法不同,逻辑一样。如果想复用,可以把核心渲染逻辑写在 C++ 层,Android 通过 JNI 调、iOS 通过 ObjC++ 调。
❌ 误区4:「requireNativeComponent 拿到的组件可以直接用 ref 调用原生方法」
不行。requireNativeComponent 返回的 ref 指向的是 React 组件实例,不是原生 View。要调原生方法必须走 UIManager.dispatchViewManagerCommand 这套命令式 API。
一句话总结
原生 UI 组件封装 = ViewManager 管生命周期 + @ReactProp 管属性 + EventEmitter 管通信 + 命令系统管操作——记住在 onDropViewInstance 清理一切,这是避免线上 crash 的底线。