useCommonImperativeHandle
import { useEffect, useRef, useCallback } from 'react';
export function useCommonImperativeHandle(ref, object) {
useEffect(() => {
if (ref) {
ref(object);
}
}, []);
}
众所周知,在 React 中我们可以通过 ref 配合 forwardRef 控制子组件,那么在 vue 中又该如何调用呢
useCommonImperativeHandle(commonRef, () => ({
handleSetVisibleUserAgreementPopup: (flag: boolean, cb?: any) => {
if (!flag) {
setIsChecked(false);
}
setVisibleUserAgreementPopup(flag);
cb && (popupConfirmCallbackRef.current = cb);
},
}));
useImperativeHandle(ref, () => ({
handleSetVisibleUserAgreementPopup: (flag: boolean, cb?: any) => {
if (!flag) {
setIsChecked(false);
}
setVisibleUserAgreementPopup(flag);
cb && (popupConfirmCallbackRef.current = cb);
},
}));
rule(options: any) {
const _dom: any = document.getElementById(options.appendDomId);
const root: ReactDOM.Root = ReactDOM.createRoot(_dom);
const key = Symbol();
root.render(
<Rule
commonRef={(ref: any) => {
this.maps.set(key, {root, ref});
}}
ref={options.ref}
value={options.isChecked}
onChange={(v: any) => {
options.onChange(v);
}}
text={options.text}
customLink={options.customLink}
className={options.className}
page={options.page}
/>,
);
return key;
}
this.op = window.SoybeanCommonMain.login.rule({
appendDomId: 'ruleDom',
onChange(v) {
self.hasChecked = v;
},
isChecked: false,
text: '阅读并同意',
});
if (!this.hasChecked) {
let ob = window.SoybeanCommonMain.login.maps.get(this.op);
ob.ref().handleSetVisibleUserAgreementPopup(true, () => {
this.handleCheckCode();
});
return;
}
补充说明
1. 这个 Hook 到底做了什么
useCommonImperativeHandle(ref, object) 可以理解为「回调版」的 useImperativeHandle:
标准 useImperativeHandle | useCommonImperativeHandle | |
|---|---|---|
| 接收的 ref | 一个 ref 对象(ref.current) | 一个 回调函数 ref(...) |
| 交出句柄的方式 | ref.current = factory() | ref(factory)(把工厂函数交给回调) |
| 触发时机 | 每次渲染后(受依赖数组控制) | useEffect 空依赖,仅首次挂载后执行一次 |
export function useCommonImperativeHandle(ref, object) {
useEffect(() => {
if (ref) {
ref(object); // 关键:把「工厂函数」交给回调,而不是写 ref.current
}
}, []);
}
注意一个细节:这里交给回调的是 object(工厂函数)本身,而不是执行后的结果。所以外部调用方取句柄时要先执行一次工厂再调方法:ref().method(...)。
2. 为什么需要它(React 组件如何被 Vue / 原生宿主命令式调用)
标准的 forwardRef + useImperativeHandle 只适用于「父组件也是 React 组件」的场景——父组件用 useRef 拿一个 ref 对象传下去,子组件往里写句柄。
但当 React 组件是被另一套框架(如 Vue)或普通 JS 命令式地挂载时,并不存在「React 父组件」来持有 ref 对象。此时挂载方只能走另一条通道:
- 通过
createRoot(dom).render(<Comp />)把 React 组件渲染进某个 DOM 容器; - 通过一个回调 prop(示例中的
commonRef)把自己「接收句柄」的回调传给组件; - 组件内部用
useCommonImperativeHandle(commonRef, factory),在挂载时把句柄推回给挂载方。
这也是示例代码里 useImperativeHandle 与 useCommonImperativeHandle 成对出现、方法体完全相同的原因:同一个组件既要能被 React 父组件通过 ref 控制,也要能被外部宿主通过 commonRef 控制,所以用两种方式各暴露一份同样的句柄。
3. 完整调用链(数据流)
结合示例代码,整条链路如下:
宿主(Vue / 原生 JS)
│ 1. 调用共享库的 rule({ appendDomId, ... }) 命令式挂载
▼
rule()
│ createRoot(dom).render(<Rule commonRef={cb} ref={...} ... />)
│ 生成一个 Symbol 作为 key,并 return 给调用方
▼
Rule 组件首次挂载
│ useCommonImperativeHandle(commonRef, factory)
│ → useEffect 触发 commonRef(factory)
▼
commonRef 回调
│ maps.set(key, { root, ref: factory }) // 把「卸载句柄 root」+「方法工厂 ref」存进 Map
▼
宿主后续需要控制组件时
│ maps.get(key).ref().method(...) // 取工厂 → 执行出句柄 → 调方法
三个要点:
key(Symbol)是连接凭据:由rule()返回,宿主自己保存,用来从maps里找回对应实例。maps存的是{ root, ref }:root负责卸载(root.unmount()),ref负责取句柄。- 为什么存工厂而不是对象:因为
useCommonImperativeHandle交给回调的是object(工厂),所以调用方要ref().method()先执行一次工厂。
4. 几个容易踩的坑
- 空依赖会导致闭包过期:
useEffect依赖是[],回调只执行一次,工厂函数捕获的 state/props 都是「首次挂载时」的值。示例里的方法没有直接读 state,而是通过setState(引用稳定)和useRef(popupConfirmCallbackRef)来读写,从而避开了过期闭包。建议句柄内部只使用稳定的 setter 与 ref,不要直接引用会变化的 state。 - 命令式挂载要记得卸载:命令式渲染不会像声明式组件那样随父级自动销毁,宿主在不再需要时(弹窗关闭、页面卸载)应主动调用
root.unmount(),否则会造成内存泄漏、事件残留。 - 句柄只交出去一次:
useCommonImperativeHandle不像useImperativeHandle那样随依赖更新重新赋值,它只在挂载后执行一次,适合「一次性把句柄交出去」的场景;若句柄需要随 state 变化而更新,应改用useImperativeHandle并传入依赖数组。