美文网首页
2025-04-26 RCTViewManager View方法

2025-04-26 RCTViewManager View方法

作者: 我是小胡胡123 | 来源:发表于2025-04-25 23:02 被阅读0次

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) => {} 就是官方推荐的做法,最保险。

相关文章

网友评论

      本文标题:2025-04-26 RCTViewManager View方法

      本文链接:https://www.haomeiwen.com/subject/xdaobjtx.html