源码
export const BaseDialog: FunctionComponent<Partial<DialogProps>> & {
open: typeof open
close: typeof close
} = (props) => {
...
const {
params: {...},
setParams,
} = useParams(mergeProps(defaultProps, props))
useCustomEvent(
id as string,
({ status, options }: { status: boolean; options: any }) => {
if (status) {
setParams({ ...options, visible: true })
} else {
setParams({ ...options, visible: false })
}
}
)
...
}
export function open(selector: string, options: Partial<typeof defaultProps>) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const path = useCustomEventsPath(selector)
customEvents.trigger(path, { status: true, options })
}
export function close(selector: string) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const path = useCustomEventsPath(selector)
customEvents.trigger(path, { status: false })
}
BaseDialog.displayName = 'NutDialog'
BaseDialog.open = open
BaseDialog.close = close
export const customEvents = new Events()
export function useCustomEventsPath(selector?: string) {
selector = selector || ''
const path = getCurrentInstance().router?.path
return path ? `${path}__${selector}` : selector
}
export function useCustomEvent(selector: string, cb: any) {
const path = useCustomEventsPath(selector)
useEffect(() => {
customEvents.on(path, cb)
return () => {
customEvents.off(path)
}
}, [])
const trigger = <T = any>(args: T) => {
customEvents.trigger(path, args)
}
const off = () => {
customEvents.off(path)
}
return [trigger, off]
}
export function useParams<T = any>(args: T) {
const forceUpdate = useForceUpdate()
const stateRef = useRef(args)
const currentRef = useRef<T>()
const previousRef = useRef<T>()
if (!isEqual(currentRef.current, args)) {
previousRef.current = currentRef.current
currentRef.current = args
stateRef.current = args
}
const setParams = (args: T) => {
stateRef.current = { ...stateRef.current, ...args }
forceUpdate()
}
const params = stateRef.current
return { params, setParams }
}
基于事件的组件通信机制
在组件之间通过自定义事件进行通信(发布/订阅模式)
open、close
通过事件路径机制远程控制组件行为的方法
- 根据
selector
构造一个唯一事件路径; - 然后通过
customEvents.trigger()
触发一个事件; - 组件内部监听这个路径的事件,收到
{ status: true }
后,就会“打开/关闭”对话框。
customEvents
全局的事件总线(发布-订阅系统),用于组件间通过事件通信(发布/订阅模式)
useCustomEventsPath
生成事件通信路径,为组件内的事件触发/监听系统提供唯一标识,确保同一页面多个组件之间的事件不会冲突
useCustomEvent
监听对话框“事件总线”
- 注册监听器:在组件 mount 时监听路径对应的事件,触发了事件就会执行
cb
,从而更新组件内部状态。 - 销毁监听器:在组件 unmount 时自动解绑监听 ,防止内存泄漏。
- 返回操作工具:
trigger(args)
:触发对应路径事件。off()
:手动取消监听(可选)。
useParams
这个 Hook 是一个轻量的、非状态驱动的参数存储器。
主要功能:
params
:当前参数。setParams(newPartial)
:设置新参数并强制组件刷新。- 内部使用
useRef
存储状态,避免频繁 re-render。 - 使用
lodash.isequal
做浅变更检测,避免不必要的更新。
对比 useState
:
useParams
更适合用于组件“打开时传入参数”的存储;- 比如弹窗打开后接收参数,并在内部读取和变更,但不需要 prop 方式控制。
静态方法挂载
并不是在「传入」 open
和 close
,而是在声明 Dialog
这个组件的类型扩展:除了是一个函数组件(FunctionComponent
),它还具有两个静态方法 open
和 close
。
你看到的 | 含义 |
---|---|
& { open: typeof open } |
把类型合并到组件对象上,使 TypeScript 知道它有这些静态方法 |
Dialog.open = open |
这是实际的实现,把方法挂到组件上 |
远程控制组件(解耦)
彻底解耦组件的显示控制权,让组件行为可被远程、跨层级、非嵌套方式调用,实现 React 中的“服务式组件使用方式”。
远程控制组件:指在不直接传入 props、不嵌套在父组件中的情况下,在任何地方(例如业务逻辑层、工具函数、服务层)就能直接“控制”组件的行为,比如打开弹窗。
传统方式的问题:props 方式耦合性强
function App() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>打开弹窗</button>
<Dialog open={open} onClose={() => setOpen(false)} />
</>
)
}
问题 | 说明 |
---|---|
耦合性高 | 你必须在父组件声明 open 状态,并通过 props 传入子组件 |
无法跨组件调用 | 如果要在另一个组件(比如 Header)里控制 Dialog,必须中间层层传递回调 |
状态提升复杂 | 多个弹窗、多层组件时,状态提升会导致“状态穿透地狱” |
父组件控制权太多 | 父组件得管理打开/关闭逻辑、参数逻辑、动画状态等 |
事件式控制:彻底解耦
解耦 = 不需要显式父子通信关系,也不需要依赖上下文(如 props/context)。 你只要调用一个静态方法,就能完成一切 —— 这就是彻底解耦。
- 不需要父组件声明
useState
- 不需要
<Dialog open={xxx} />
- 不用写 onClose 回调
- 只需要你在页面里挂一个组件
<Dialog selector="my-dialog" />
就可以 - 甚至你在 service 层也能触发它
特性 | 传统 props 控制 | 事件式控制(open() ) |
---|---|---|
组件位置 | 必须嵌套 | 可以任意放置 |
控制方式 | 由父组件状态控制 | 全局或业务代码中调用即可 |
多组件支持 | 控制多个难 | 每个组件监听独立事件路径 |
状态管理 | 由父组件集中管理 | 每个组件内部自控 |
适合场景 | 小项目、页面内组件 | 全局组件库、低代码平台、复杂 UI 系统 |
事件、路径、标识路径
事件(Event) 是程序中的一种“信号”或“通知”,代表某个动作发生了。
- 用户点击按钮 → 触发一个 “点击事件”
- 表单提交 → 触发一个 “提交事件”
- 你调用了某个函数
open()
→ 触发一个 “自定义事件”
路径(Path) 通常指的是某个东西的位置或地址。
const path = getCurrentInstance().router?.path
标识路径是用来唯一标识一个事件的字符串,通常由:
- 当前页面路径
- 加上一个特定的事件名(selector)
组合而成。
事件驱动(Event-Driven)
事件驱动是一种编程模式,它的核心思想是:
“当某个事件发生时,系统会触发并执行与该事件相关的逻辑。”
在 UI 和前端开发中,“事件驱动”意味着:
- 你不主动调用某个函数去更新状态,
- 而是注册监听器,等待事件发生后触发动作。
// 1. 注册监听器
customEvents.on('dialog__open', (params) => {
showDialog(params)
})
// 2. 触发事件
customEvents.trigger('dialog__open', { title: '确认删除?' })
on()
:注册监听函数trigger()
:触发事件(并通知所有监听者)off()
:取消监听
[业务代码调用 Dialog.open()]
↓
[trigger 一个事件: "dialog__open"]
↓
[Dialog 组件内部 useEffect 注册监听 "dialog__open"]
↓
[事件到来 → 执行 setVisible(true), setParams()]
↓
[Dialog 显示了,并渲染对应内容]
发布/订阅模式
发布/订阅模式的核心思想是解耦。在这种模式下,有两个主要角色:
- 发布者(Publisher):负责发布消息或事件,但不知道谁会接收这个消息。
- 订阅者(Subscriber):负责订阅某个事件,并在事件发布时收到通知。
发布者和订阅者之间没有直接的联系,它们通过一个中介(通常是一个事件总线或消息队列)来进行通信。这种方式使得发布者和订阅者可以互相独立地工作,减少耦合。
- 发布事件:
在 open
函数中,使用了 customEvents.trigger
来发布一个事件。
customEvents.trigger(path, { status: true, options })
这里的 path
是事件的标识,{ status: true, options }
是要发布的消息内容。
- 订阅事件:
在 useCustomEvent
中,组件会订阅某个事件,通过 customEvents.on(path, cb)
来注册事件回调函数 cb
。
customEvents.on(path, cb)
当事件 path
被触发时,cb
会被执行。
- 取消订阅:
在 useCustomEvent
中,组件也可以在不需要监听时通过 customEvents.off(path)
来移除订阅。
customEvents.off(path)
发布/订阅模式的好处在于它可以让不同模块之间解耦。发布者并不知道谁在订阅它发布的事件,而订阅者也无需知道谁在发布事件。它们通过事件总线来间接交流,降低了系统的复杂度和模块之间的依赖。