react 中發佈訂閱模式使用
場景
怎麼能將設計模式應用到我們的 React 項目中?以前一直在思考這個問題。
場景一
模塊 A 模塊 B 需要用到同一個數據 data,A 和 B 都會修改這份數據,且這兩個模塊會同時存在;這時我們如何做到數據公用與各個模塊的更新?
方案一:
將這份數據作爲公共的數據 data,A B 模塊同時使用並更改這份數據這一份數據。若使用 redux 代碼可能是這樣:
// store
const store = {
common: { data: [] },
A: {},
B: {},
};
// reducer
function commonReducer(state = { data: [] }, action) {
switch (action.type) {
case 'common_setData': {
return {
...state,
data: action.data,
};
}
default:
return state;
}
}
// connect
const actionCreator = () => {};
connect(({ A, common }) => ({ ...A, data: common.data }))(A);
connect(({ B, common }) => ({ ...A, data: common.data }))(B);
// change
// A B change調用方法;
this.props.dispatch({
type: 'common_setData',
data: [1, 2],
});
好的,第一種場景可以使用 redux 完美解決
方案二:待補充
場景二
A 模塊使用了 data1, B 模塊使用了 data2;A B 模塊可以修改對應的 data;這兩份 data 結構上不同,但是存在業務上的聯繫: 當 data1 更新後需要 data2 更新;data2 更新同樣需要 data1 同步;對應後端的兩個不同的 API。
我們整理一下
- A B 使用兩份存在聯繫的 data
- 其中一個更新需要另一個更新
- 兩份 data 對應不同的 API 接口
- A B 對應兩個不同的 tab 且可能同時存在
方案一
當其中一個數據因操作發生更新時,判斷另一個模塊是否存在 如果存在則調用他的數據更新邏輯;
如果你使用了 redux,可能方便一點:
// reducerA
// 省略B
function reducerA(state = { data: [] }, action) {
switch(action.type) {
case 'A_setDataA': {
return {
...state,
data: action.data
}
}
default: return state
}
}
// 假設使用了thunk中間件
const queryA = () => async (dispatch, getState) => {
const dataA = await API.queryA()
dispatch({
type: 'A_setDataA'
data: dataA
})
}
// page
class B extends React.Component {
handleUpdateData = () => {
// 如果 A模塊存在
const { isAExistFlag, dispatch, queryA, queryB } = props
dispatch(queryB())
if (isAExistFlag) {
dispatch(queryA())
}
}
}
這樣利用了 redux 可以實現功能,在模塊 B 內調用模塊 A 的更新邏輯;但這樣邏輯就耦合了,我在模塊 A 調用模塊 B 方法 在模塊 B 調用模塊 A 的方法;但很有可能這兩個模塊是沒有其他交互的。這違反了低耦合高內聚的原則
而且書寫 redux 的一個原則就是 不要調用(dispatch)其他模塊的 action
如果你不使用 redux 如果是一個模塊內調用其他模塊的方法也是沒有做到解耦的;那如何做到解耦尼?請看方案二
方案二:利用事件系統
如果您的項目中沒有一個全局的事件系統,可能需要引入一個;一個簡單的事件系統大概是:
class EventEmitter {
constructor() {
this.listeners = {};
}
on(type, cb, mode) {
let cbs = this.listeners[type];
if (!cbs) {
cbs = [];
}
cbs.push(cb);
this.listeners[type] = cbs;
return () => {
this.remove(type, cb);
};
}
emit(type, ...args) {
console.log(
`%c event ${type} be triggered`,
'color:rgb(20,150,250);font-size:14px',
);
const cbs = this.listeners[type];
if (Array.isArray(cbs)) {
for (let i = 0; i < cbs.length; i++) {
const cb = cbs[i];
if (typeof cb === 'function') {
cb(...args);
}
}
}
}
remove(type, cb) {
if (cb) {
let cbs = this.listeners[type];
cbs = cbs.filter(eMap => eMap.cb !== cb);
this.listeners[type] = cbs;
} else {
this.listeners[type] = null;
delete this.listeners[type];
}
}
}
export default new EventEmitter();
這個事件系統具有註冊,發佈,移除事件的功能。那我們怎麼在剛纔這個場景去使用它尼?
- 發佈:當A模塊內數據因操作發生變化時,觸發該數據變化的事件,定義
type
爲data1Change
; - 註冊:這裏B模塊的註冊的時機,上述的場景爲A和B模塊可能同時出現,所以A模塊存在B模塊卻不存在。所以這個B模塊事件的監聽選擇在B模塊組件的
componentDidMount
的時候註冊,在componentWillUnmount
時移除
大致的代碼如下:
import EventEmitter from 'eventEmitter'
class A extends React.Component {
handleUpdateData = () => {
// 如果 A模塊存在
const { dispatch, queryB } = props
dispatch(queryA())
EventEmitter.emit('data1Change')
}
}
// B
import EventEmitter from 'eventEmitter'
class B extends React.Component {
componentDidMount() {
const unlistener = EventEmitter.on('data1Change', this.handleData1Change)
}
componentWillUnmount() {
EventEmitter.on('data1Change', this.handleData1Change)
}
handleData1Change = () => {
const { dispatch, queryB } = this.props
dispatch(queryB())
}
}
這樣通過事件系統做到了兩個模塊之間的解耦,作爲事件發佈方只管發佈自己的事件。兩個模塊在事件系統唯一的聯繫就是事先定義好事件的type。
不過這也增加了幾行的代碼量,但相比帶來的優勢來說可以不計。
其他方案歡迎大家評論
其他場景
待大家補充