前言
公司前段时间开发A项目的时候,因为A项目涉及的模块比较多,而且好多又是具有相当独立性等特点的,比如编辑器模块、可视化编辑模块、动态表单自定义配置模块、导航栏及权限页面配置模块等。
而且,部分功能模块在B项目、C项目等中也有使用到,所以,综合考虑,这些模块还是作为单独组件模块开发比较合适,通过统一的包管理,如npm
,可以方便其他人员和项目快速添加使用,避免重复劳动,也避免复制粘贴这种不易维护和稳定以及同步更新的麻烦~
其中,有不少场景是需要跨组件模块传值的。而我们使用组件的常规方式基本是通过 import
这种引入一个组件模块,然后通过props
传值的方式进行接收,类似antd
这种。通过父组件 Parent结合props
也可以实现 Child1 和 Child2的通信,但是这样对于组件内部分模块嵌套又比较深的,操作起来耦合性比较大,也比较乱,所以,我就在想如何降低耦合性并实现组件间的传值和通信呢~
简单看下代码和逻辑结构
代码大致结构
import React from 'react'
import Child1 from 'M' // npm包 M
import Child2 from 'N' // npm包 N
const Parent:React.FC=()=>{
return <div>
<Child1 />
<Child2 />
{/* 其他业务组件等 */}
{/* ... */}
</div>
}
export default Parent
组件之间的关系
如图所示,Child1组件内部又分为很多父子小模块,这时我们需要和Child1中嵌套比较深的E模块进行通信,props
和Context
方式都有一定的成本和耦合性,联想到Vue
的Event
事件通信传值,我觉得或许可以换个思路试一试~
事件通信的几种方式
✨Event Bus
主要就是通过发布订阅模式实现,这个我在另一篇文章里有写过 发布订阅模式vs观察者模式
具体实现和原理就不说了,简单看下使用方式
yarn add events
创建 src/utils/eventBus.ts
,进行实例化,确保实例唯一,之后使用的时候,都应该引用这个
import { EventEmitter } from 'events'
// 保证唯一实例
const EventBus = new EventEmitter()
export default EventBus
简单写个demo测试下
目录结构如下
components-eventBus
├── Parent
├── Child1
├── Child2
utils
├── eventBus
Parent
组件
import React from "react"
import Child1 from "./Child1"
import Child2 from "./Child2"
const Parent: React.FC = () => {
return (
<div className="parent-container">
<div className="content-box">
<Child1 />
<Child2 />
</div>
</div>
)
}
export default Parent
Child1
组件1
import React, { useEffect, useState } from "react"
import EventBus from "../utils/eventBus"
interface StateProps {
name: string
age: number
count: number
}
const PERSON_INIT: StateProps = {
name: "小明",
age: 18,
count: 0,
}
const Child1: React.FC = () => {
const [state, setState] = useState<StateProps>(PERSON_INIT) // 本组件的 state
const [recevie, setRecevie] = useState() // 接收来自其他组件的 state
const eventRegister = (args: any) => {
console.log("我是Child1,我接收到了来自Child2的消息:", args)
setRecevie(args)
}
useEffect(() => {
// mount
EventBus.on("msgTochild1", eventRegister)
// unmount
return () => {
EventBus.off("msgTochild1", eventRegister)
}
}, [])
const sendMsgToChild2 = () => {
EventBus.emit("msgTochild2", state)
}
return (
<div>
<h2>Child1</h2>
<article>
<p>state:</p>
<pre>{JSON.stringify(state, null, 2)}</pre>
<br />
<p>event recevie:</p>
<pre>{JSON.stringify(recevie, null, 2)}</pre>
<br />
<button onClick={() => setState((prev) => ({ ...prev, count: prev.count + 1 }))}>
count + 1
</button>
<button onClick={sendMsgToChild2}>sendMsgToChild2</button>
</article>
</div>
)
}
export default Child1
Child2
组件2
import React, { useEffect, useState } from "react"
import EventBus from "../utils/eventBus"
interface ListProps {
name: string
age: number
count: number
}
const LIST_INIT: ListProps[] = [
{
name: "张三",
age: 10,
count: 0,
},
]
const Child2: React.FC = () => {
const [list, setlist] = useState<ListProps[]>(LIST_INIT) // 本组件的 state
const [recevie, setRecevie] = useState() // 接收来自其他组件的 state
const eventRegister = (args: any) => {
console.log("我是Child2,我接收到了来自Child1的消息:", args)
setRecevie(args)
}
useEffect(() => {
// mount
EventBus.on("msgTochild2", eventRegister)
// unmount
return () => {
EventBus.off("msgTochild2", eventRegister)
}
}, [])
const sendMsgToChild1 = () => {
EventBus.emit("msgTochild1", list)
}
return (
<div>
<h2>Child2</h2>
<article>
<p>list:</p>
<pre>{JSON.stringify(list, null, 2)}</pre>
<br />
<p>event recevie:</p>
<pre>{JSON.stringify(recevie, null, 2)}</pre>
<br />
<button onClick={() => setlist((prev) => [...prev, { name: "小李", age: 11, count: 0 }])}>
count + 1
</button>
<button onClick={sendMsgToChild1}>sendMsgToChild1</button>
</article>
</div>
)
}
export default Child2
看下效果
传值没啥问题,配合 callback
即可完成状态的更新,不过,这里有个问题
首先,我们的需求是和Child1组件的E模块进行通信,目前这个简易demo只是验证了eventBus
通信传值的可行性,即Child1对Child2通信
但这里的Child1其实是我们抽象出来的的演示组件,实际场景中它应该是我们通过npm包-M
引入的组件,因此,这样看来我们还是要通过props
对M组件进行传值通信,毕竟我们只能直接接触到M组件,而无法接触到M组件内的E模块
因为eventBus
要确保实例唯一,而我们的eventBus
实例是放在父组件的项目内的,所以,M组件也是无法直接获取到实例化的eventBus
,我们不可能把eventBus
通过Props
传递下去,感觉这样又绕回来了
目前可行的方案,就是把实例化后的eventBus
挂载到组件不通过props
就可以访问到的地方
-
window
全局属性
// Parent组件 挂载 EventBus 到 window
import EventBus from "../utils/eventBus"
window._MY_EVENTBUS_ = EventBus
// M组件
const EventBus = window._MY_EVENTBUS_
当前项目下window
是全局唯一,任意组件都可访问,不受局限,看起来似乎没问题
不过这里存在一些风险
- 变量污染
- 安全性
因此,这种方式也不建议使用~
✨JavaScript 自定义事件
直接使用JavaScript原生自定义事件 Event
和 CustomEvent
,先看下用法
1. ????使用JavaScript内置的 Event 构造函数
const myEvent = new Event(typeName, option)
- typeName :
DOMString
类型,表示创建事件的名称; - option : 可选配置项
-
bubbles
:表示该事件是否冒泡,默认null
-
cancelable
:表示该事件能否被取消,默认false
-
composed
:指示事件是否会在影子DOM根节点之外触发侦听器(影子DOM:Shadow DOM
),默认false
-
示例
// 创建一个支持冒泡的 event-A 事件
const myEvent = new Event("event-A", { bubbles: true })
// 触发事件
document.dispatchEvent(myEvent);
事件通信没啥问题,不过这个方式不支持直接传递参数值,该方式有待考虑
2. ????使用JavaScript内置的 CustomEvent 构造函数
const myEvent = new CustomEvent(typeName, option)
- typeName :
DOMString
类型,表示创建事件的名称; - option : 可选配置项
-
detail
:表示该事件中需要被传递的数据,在 EventListener 获取,默认null
-
bubbles
:表示该事件是否冒泡,默认false
-
cancelable
:表示该事件能否被取消。(影子DOM:Shadow DOM
),默认false
-
示例
// 创建事件
const myEvent = new CustomEvent("eventName", { detail: list })
// 添加事件监听
window.addEventListener("eventName", e=> console.log(e))
// 派发事件
window.dispatchEvent(myEvent)
同样的简单写个demo测试下
目录结构如下
components-customEvent
├── Parent
├── Child1
├── Child2
Parent
组件和上面一样,没变
Child1
组件1
import React, { useEffect, useState } from "react"
interface StateProps {
name: string
dec: string
count: number
}
const PERSON_INIT: StateProps = {
name: "小明",
dec: "Child1的CustomEvent自定义事件event-A",
count: 0,
}
const Child1: React.FC = () => {
const [state, setState] = useState<StateProps>(PERSON_INIT)
const [recevie, setRecevie] = useState()
const eventRegister = (args: any) => {
console.log("我是Child1,我接收到了来自Child2的消息:", args)
setRecevie(args.detail)
}
useEffect(() => {
// mount
window.addEventListener("event-B", eventRegister)
// unmount
return () => {
window.removeEventListener("event-B", eventRegister)
}
}, [])
const sendMsgToChild2 = () => {
// 创建事件
const myEvent = new CustomEvent("event-A", { detail: state })
// 派发事件
window.dispatchEvent(myEvent)
}
return (
<div>
<h2>Child1</h2>
<article>
<p>state:</p>
<pre>{JSON.stringify(state, null, 2)}</pre>
<br />
<p>event recevie:</p>
<pre>{JSON.stringify(recevie, null, 2)}</pre>
<br />
<button onClick={() => setState((prev) => ({ ...prev, count: prev.count + 1 }))}>
count + 1
</button>
<button onClick={sendMsgToChild2}>sendMsgToChild2</button>
</article>
</div>
)
}
export default Child1
Child2
组件2
import React, { useEffect, useState } from "react"
interface ListProps {
name: string
dec: string
count: number
}
const LIST_INIT: ListProps[] = [
{
name: "小李",
dec: "Child2的CustomEvent自定义事件event-B",
count: 0,
},
]
const Child2: React.FC = () => {
const [list, setlist] = useState<ListProps[]>(LIST_INIT)
const [recevie, setRecevie] = useState()
const eventRegister = (args: any) => {
console.log("我是Child2,我接收到了来自Child1的消息:", args)
setRecevie(args.detail)
}
useEffect(() => {
// mount
window.addEventListener("event-A", eventRegister)
// unmount
return () => {
window.removeEventListener("event-A", eventRegister)
}
}, [])
const sendMsgToChild1 = () => {
// 创建事件
const myEvent = new CustomEvent("event-B", { detail: list })
// 派发事件
window.dispatchEvent(myEvent)
}
return (
<div>
<h2>Child2</h2>
<article>
<p>list:</p>
<pre>{JSON.stringify(list, null, 2)}</pre>
<br />
<p>event recevie:</p>
<pre>{JSON.stringify(recevie, null, 2)}</pre>
<br />
<button
onClick={() =>
setlist((prev) => [
...prev,
{ name: "小李", dec: "Child2的CustomEvent自定义事件event-B", count: 0 },
])
}
>
person list + 1
</button>
<button onClick={sendMsgToChild1}>sendMsgToChild1</button>
</article>
</div>
)
}
export default Child2
看下效果
可以看到该方式是支持事件通信和参数传值的,基本可以满足我们的需求
而对于使用方式和普通的事件 addEventListener
也差不多,添加订阅后,通过 callback
接收事件的传值和状态更新操作,但是我们也可以发现有些不一样的地方
即,它不像 EventBus
那样必须使用单一实例,我们只要指定自定义事件的 typeName
,就可以在任意地方触发使用,似乎用起来更方便简单~
话说不知道有没有坑~
看了下兼容性,如下
IE还是稳啊????,IE浏览器是不支持CustomEvent.detail
的,似乎Edge 14+才开始支持,怎么办,难道IE浏览器就不能使用的吗?
这里贴一份张鑫旭博客提供的Polyfill方案,cv~
/**
* CustomEvent constructor polyfill for IE
*/
(function () {
if (typeof window.CustomEvent === 'function') {
// 如果不是IE
return false;
}
var CustomEvent = function (event, params) {
params = params || {
bubbles: false,
cancelable: false,
detail: undefined
};
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
};
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
})();
结语
OK,到此结束
事件本质是一种消息,事件模式本质上是观察者模式的实现,即能用观察者模式的地方,自然也能用事件模式。
又回到了观察者模式了呀~