从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)

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

相关推荐
Feather_743 小时前
从Taro的Dialog.open出发,学习远程控制组件之【事件驱动】
javascript·学习·taro
超级土豆粉3 天前
Taro 本地存储 API 详解与实用指南
前端·javascript·react.js·taro
用户3802258598243 天前
vue3源码解析:Teleport组件实现
前端·vue.js·源码阅读
Hao.Zhou4 天前
taro+pinia+小程序存储配置持久化
小程序·taro
waillyer4 天前
taro跳转路由取值
前端·javascript·taro
超级土豆粉4 天前
Taro 路由相关 API 详解与实战
前端·javascript·react.js·ecmascript·taro
惊鸿2875 天前
Taro3+小程序分包配置指南
前端·taro
程序猿阿越6 天前
Kafka源码(二)分区新增和重分配
java·后端·源码阅读
用户3802258598248 天前
vue3源码解析:生命周期
前端·vue.js·源码阅读