从Taro的Dialog.open出发,学习远程控制组件之【事件驱动】

源码

typescript 复制代码
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
typescript 复制代码
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 方式控制。

静态方法挂载

并不是在「传入」 openclose,而是在声明 Dialog 这个组件的类型扩展 :除了是一个函数组件(FunctionComponent),它还具有两个静态方法 openclose

你看到的 含义
& { open: typeof open } 把类型合并到组件对象上,使 TypeScript 知道它有这些静态方法
Dialog.open = open 这是实际的实现,把方法挂到组件上

远程控制组件(解耦)

彻底解耦组件的显示控制权 ,让组件行为可被远程、跨层级、非嵌套方式调用,实现 React 中的"服务式组件使用方式"。

远程控制组件 :指在不直接传入 props、不嵌套在父组件中的情况下,在任何地方(例如业务逻辑层、工具函数、服务层)就能直接"控制"组件的行为,比如打开弹窗。

传统方式的问题:props 方式耦合性强

typescript 复制代码
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) 通常指的是某个东西的位置或地址。

typescript 复制代码
const path = getCurrentInstance().router?.path

标识路径是用来唯一标识一个事件的字符串,通常由:

  • 当前页面路径
  • 加上一个特定的事件名(selector)

组合而成。

事件驱动(Event-Driven)

事件驱动是一种编程模式,它的核心思想是:

"当某个事件发生时,系统会触发并执行与该事件相关的逻辑。"

在 UI 和前端开发中,"事件驱动"意味着:

  • 你不主动调用某个函数去更新状态,
  • 而是注册监听器,等待事件发生后触发动作。
typescript 复制代码
// 1. 注册监听器
customEvents.on('dialog__open', (params) => {
  showDialog(params)
})

// 2. 触发事件
customEvents.trigger('dialog__open', { title: '确认删除?' })
  • on():注册监听函数
  • trigger():触发事件(并通知所有监听者)
  • off():取消监听
typescript 复制代码
[业务代码调用 Dialog.open()]
           ↓
[trigger 一个事件: "dialog__open"]
           ↓
[Dialog 组件内部 useEffect 注册监听 "dialog__open"]
           ↓
[事件到来 → 执行 setVisible(true), setParams()]
           ↓
[Dialog 显示了,并渲染对应内容]

发布/订阅模式

发布/订阅模式的核心思想是解耦。在这种模式下,有两个主要角色:

  1. 发布者(Publisher):负责发布消息或事件,但不知道谁会接收这个消息。
  2. 订阅者(Subscriber):负责订阅某个事件,并在事件发布时收到通知。

发布者和订阅者之间没有直接的联系,它们通过一个中介(通常是一个事件总线消息队列)来进行通信。这种方式使得发布者和订阅者可以互相独立地工作,减少耦合。

  1. 发布事件

open 函数中,使用了 customEvents.trigger 来发布一个事件。

typescript 复制代码
customEvents.trigger(path, { status: true, options })

这里的 path 是事件的标识,{ status: true, options } 是要发布的消息内容。

  1. 订阅事件

useCustomEvent 中,组件会订阅某个事件,通过 customEvents.on(path, cb) 来注册事件回调函数 cb

typescript 复制代码
customEvents.on(path, cb)

当事件 path 被触发时,cb 会被执行。

  1. 取消订阅

useCustomEvent 中,组件也可以在不需要监听时通过 customEvents.off(path) 来移除订阅。

typescript 复制代码
customEvents.off(path)

发布/订阅模式的好处在于它可以让不同模块之间解耦。发布者并不知道谁在订阅它发布的事件,而订阅者也无需知道谁在发布事件。它们通过事件总线来间接交流,降低了系统的复杂度和模块之间的依赖。

相关推荐
魏思凡2 天前
爆肝一万多字,我准备了寿司 kotlin 协程原理
kotlin·源码阅读
GISer_Jing2 天前
大前端——Taro、React-Native、Electron 大前端
前端·javascript·electron·taro
白鲸开源6 天前
一文掌握 Apache SeaTunnel 构建系统与分发基础架构
大数据·开源·源码阅读
Tans516 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Tans519 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
Tans523 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
Thetimezipsby23 天前
基于Taro4打造的一款最新版微信小程序、H5的多端开发简单模板
前端·javascript·微信小程序·typescript·html5·taro
凡小烦23 天前
LeakCanary源码解析
源码阅读·leakcanary
gitboyzcf1 个月前
基于Taro4最新版微信小程序、H5的多端开发简单模板
前端·vue.js·taro
浩星1 个月前
react+taro中使用vant 工具:taroify
前端·react.js·taro