RN源代码
/Pods/React-Core/source/React/Modules/RCTUIManager.m
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
viewName:(NSString *)viewName
rootTag:(nonnull NSNumber *)rootTag
props:(NSDictionary *)props)
{
RCTComponentData *componentData = _componentDataByName[viewName];
if (componentData == nil) {
RCTLogError(@"No component found for view with name \"%@\"", viewName);
}
// Register shadow view
RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag];
if (shadowView) {
[componentData setProps:props forShadowView:shadowView];
_shadowViewRegistry[reactTag] = shadowView;
RCTShadowView *rootView = _shadowViewRegistry[rootTag];
RCTAssert([rootView isKindOfClass:[RCTRootShadowView class]] ||
[rootView isKindOfClass:[RCTSurfaceRootShadowView class]],
@"Given `rootTag` (%@) does not correspond to a valid root shadow view instance.", rootTag);
shadowView.rootView = (RCTRootShadowView *)rootView;
}
// Dispatch view creation directly to the main thread instead of adding to
// UIBlocks array. This way, it doesn't get deferred until after layout.
__block UIView *preliminaryCreatedView = nil;
void (^createViewBlock)(void) = ^{
// Do nothing on the second run.
if (preliminaryCreatedView) {
return;
}
preliminaryCreatedView = [componentData createViewWithTag:reactTag]; //
if (preliminaryCreatedView) {
self->_viewRegistry[reactTag] = preliminaryCreatedView;
}
};
// We cannot guarantee that asynchronously scheduled block will be executed
// *before* a block is added to the regular mounting process (simply because
// mounting process can be managed externally while the main queue is
// locked).
// So, we positively dispatch it asynchronously and double check inside
// the regular mounting block.
RCTExecuteOnMainQueue(createViewBlock);
[self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
createViewBlock();////调用 RCTViewManager的view() 方法 创建VIew
if (preliminaryCreatedView) {
[componentData setProps:props forView:preliminaryCreatedView]; ///这里调用 RCT_EXPORT_VIEW_PROPERTY JS端端属性绑定
}
}];
[self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]];
}
先 调用 RCTViewManager 的view() 方法 创建VIew
在 调用
[componentData setProps:props forView:preliminaryCreatedView]
方法,执行 RCT_EXPORT_VIEW_PROPERTY RCTViewManager
的View对应的属性设置
流程解析:
调用 RCTViewManager 的 view() 方法
➔ 创建出真正的 UIView 对象,比如你的 YRNAMapView。
➔ 这是在 createViewBlock 中调用 preliminaryCreatedView = [componentData createViewWithTag:reactTag] 完成的。
➔ createViewWithTag: -> 调用 RCTViewManager.view()。
设置属性(setProps)
➔ 在创建完 preliminaryCreatedView 后,调用
[componentData setProps:props forView:preliminaryCreatedView]
➔ 这里就对应你用 RCT_EXPORT_VIEW_PROPERTY 暴露给 JS 的那些属性,比如:myLocationStyle, uiSettings, onCameraChange 等。
➔ 这些 props 经过自动生成的 setter 方法,赋值给你的 Swift 代码里的 @objc var xxx 属性。
所以实际调用顺序是:
✅ 1. createView
✅ 2. setProps
而你遇到的问题:
init里面就发 onMapLoaded,但是收不到,有 "no listeners registered" 的警告。
原因也显然了:
在执行 init(初始化 YRNAMapView 时),React Native 侧还没来得及给 onMapLoaded 这个 block 赋值。
因为:
UIView 是 view() 里刚刚创建。
但属性(包括 onMapLoaded)的赋值还要等 setProps:forView: 后才完成。
所以在 init 阶段,self.onMapLoaded 还没值(是 nil)。\
解决方案
import UIKit
import MapKit
class YRNAMapView: MKMapView {
@objc var onMapLoaded: RCTBubblingEventBlock? { //先调用
didSet {
// 当 onMapLoaded 被设置,且之前还没发过,就发一次
sendMapLoadedEventIfNeeded()
}
}
private var hasSentMapLoaded = false // 标记是否已发送
override init(frame: CGRect) {
super.init(frame: frame)
// 这里暂时不发送 onMapLoaded,等 props 设置好后再发
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() { ///后调用
super.didMoveToWindow()
if window != nil {
DispatchQueue.main.async { [weak self] in
self?.sendMapLoadedEventIfNeeded()
}
}
}
private func sendMapLoadedEventIfNeeded() {
guard !hasSentMapLoaded else { return }
guard let onMapLoaded = onMapLoaded else { return }
hasSentMapLoaded = true
onMapLoaded([:]) // 发送空对象即可,React Native端收到事件
}
}
注意执行顺序:
init阶段不能直接依赖 props,因为 props 赋值要等 setProps。
属性赋值后(didSet)或者 view ready 后(didMoveToWindow)再触发逻辑,才不会有 "no listeners registered" 问题。
1、先调用onMapLoaded的属性观察器方法
2、再调用didMoveToWindow视图挂载方法
继续看问题:
///map组件:
const AMapView = (
{
onCameraChange,
onLocationChange,
onMapLoaded,
onMarkerClick,
uiSettings,
myLocationStyle,
...props
}: NativeProps,
ref: React.ForwardedRef<IAMapViewRef>
) => {
const mapRef = useRef<typeof AMapVIewNativeComponent>(null)
useLayoutEffect(() => {
}, [])
return (
<Box flex={1}>
<AMapVIewNativeComponent
flex={1}
ref={mapRef}
myLocationStyle={myLocationStyle}
uiSettings={uiSettings}
{...props}
/>
</Box>
)
}
const onMapLoaded = useMemoizedFn(() => {
console.log("onMapLoaded 222222")
})
///页面:
<AMapView
ref={mapRef}
onCameraChange={onCameraChange}
onLocationChange={onLocationChange}
onMapLoaded={onMapLoaded}
onMarkerClick={onMarkerClick}
myLocationStyle={{ showMyLocation: true }}
/>
onMapLoaded这个方法并没有调用到native的
@objc var onMapLoaded: RCTDirectEventBlock?
方法。native onMapLoaded
是nil
这里的问题是:
你在 RN 里 <AMapView onMapLoaded={onMapLoaded} />
但是 iOS native 里 @objc var onMapLoaded: RCTDirectEventBlock?
是 nil,导致 native 发送不了事件。
✅ 这类问题通常出在 ——
AMapView -> AMapViewNativeComponent 时,属性 onMapLoaded 没有正确传到 Native 组件上!
<AMapVIewNativeComponent
flex={1}
ref={mapRef}
myLocationStyle={myLocationStyle}
uiSettings={uiSettings}
{...props}
/>
注意:
onMapLoaded 是在 props 里,但是你自己手动提取了 onMapLoaded,却没传下去!
问题出在:
这里:
{
onCameraChange,
onLocationChange,
onMapLoaded, // 这里你把 onMapLoaded 单独提取了
onMarkerClick,
...
...props // props 里已经不包含 onMapLoaded 了!
}
结果 ...props 传下去的时候,onMapLoaded 丢了。
所以 Native 收不到这个 RCTDirectEventBlock,自然是 nil。
正确做法 ①:传递所有 props(别单独提 onMapLoaded)
不要单独拿出来,直接 props 透传:
const AMapView = (props: NativeProps, ref: React.ForwardedRef<IAMapViewRef>) => {
const mapRef = useRef<typeof AMapVIewNativeComponent>(null)
return (
<Box flex={1}>
<AMapVIewNativeComponent
flex={1}
ref={mapRef}
{...props} // 直接全部 props 透传!
/>
</Box>
)
}
正确做法 ②:提取的时候记得传回去!
如果你想继续提取出来(比如为了在 useLayoutEffect 做什么逻辑),
那也要手动补传:
<AMapVIewNativeComponent
flex={1}
ref={mapRef}
myLocationStyle={myLocationStyle}
uiSettings={uiSettings}
onCameraChange={onCameraChange}
onLocationChange={onLocationChange}
onMapLoaded={onMapLoaded} // 补上
onMarkerClick={onMarkerClick}
{...props}
/>
这样 Native 才能拿到 onMapLoaded 这个 Block!
继续看问题
<AMapView
ref={mapRef}
onCameraChange={onCameraChange}
onLocationChange={onLocationChange}
onMapLoaded={(data) => {
console.log('onMapLoaded99999',data);
}}
onMarkerClick={onMarkerClick}
myLocationStyle={{ showMyLocation: true }}
/>
const onMapLoaded = useMemoizedFn(() => {
console.log("onMapLoaded 222222")
})
将 onMapLoaded={onMapLoaded}
改成 onMapLoaded={(data) => { console.log('onMapLoaded99999',data); }}
确实调用了
这个{onMapLoaded} 与{(data) => { console.log('onMapLoaded99999',data); }}
是什么区别
那为什么之前你的 onMapLoaded={onMapLoaded} 不生效?
因为——
你定义的 onMapLoaded = useMemoizedFn(() => {...})
但是 Native 端 RCTDirectEventBlock 是在 JS 端 首渲染的时候生成的
useMemoizedFn 会生成一个壳函数,然后里面再持有最新逻辑,但外面的函数是稳定的。
也就是说:
useMemoizedFn 生成的是一个壳,React Native 桥接拿到的是这个壳函数的一种代理,但由于 RN 机制问题,有时候这个壳没有正确桥接到 Native!
而 (data) => {...} 这种匿名箭头函数,是在 React 渲染时实时创建的新的回调,一定能被 Native 捕获到。
简单总结
写法 | 安全性 | 解释 |
---|---|---|
直接传 useMemoizedFn 生成的 | ️ 有小概率失效(看 RN 版本) | useMemoizedFn 是优化性能的,但对 Native 事件绑定不够保险 |
直接写 (data) => {} |✅ 最保险! |每次新的渲染,新的回调,Native一定能收到
🧠 所以要记住一条:
如果是给原生 Native 组件绑定事件,建议写成直接箭头函数 (data) => {...},不要用 useCallback/useMemoizedFn 包一层。
因为 Native 需要拿到的是新鲜的、真实的回调,而不是某个壳子。
你的 JS 写法上,useMemoizedFn导致 Native 拿到的可能是 stale(老的)block。
直接写 (data) => {} 就是官方推荐的做法,最保险。
网友评论