大家好我們是運滿滿前端團隊,這次給大家帶來的分享是如何解決資料驅動帶來強互動和深層次通訊的痛點,在vue react angular
三劍客的資料驅動模式的引領下,對資料展示的業務層結合spa應用帶來了很大的方便
專案和需求是千變萬化的,特別對於強互動和一些工具類的製作專案,每個元件通過事件管理器來管理自己的業務狀態,只是通過事件發射器來來觸發每個元件中監聽的事件,也可以稱之為簡單的訂閱釋出模式,在vue
中類似於中央事件,但是中央事件只能通過$emit
與$on
的模式進訂閱和釋出
考慮一下什麼情況下不適用於資料驅動模式,或者使用大量的資料驅動難以維護,元件多層級互動,跨層級互動帶來的不便
每一個圖形都是一個元件,有些工具類專案就像在一個畫布上畫畫一樣,互動行為很分散,資料跨越度太大,資料並沒有太大的關聯和展示性
通過資料驅動的模式如何去處理?
每個元件在互動的時候需要在頂層儲存大量的資料,每種資料格式都不同,一旦資料量大了,就會顯示的格外複雜,難以維護
正是因為spa
應用把每個元件連線成一個大容器,在元件時進行銷燬時,事件管理器同樣也應該做到對每個元件不但進行訂閱釋出,也同時要做到進行訂閱的銷燬
在此我們對訂閱釋出模式進行強化,在事件匯流排中孵化每個元件裡自己的訂閱狀態,同時在自身元件進行銷燬時,匯流排上的訂閱器同時能移除當前銷燬的元件的訂閱者
通過zma
我們解決了什麼問題?
- 讓資料更具有私有性,不讓頂能數源儲存大量的資料,再進行中轉
- 解決跨層級通訊的麻煩程度
- 通過簡單的訂閱去派發
- 每個元件維護自己的資料,讓資料變得更加可維護
- 在組銷燬時做到了總訂閱層的同時銷燬,效能提升
- 相比vue中央事件,提供了大量的api,能應用對付多樣的場景
zma文件使用地址 感覺適用自己專案的給個star哦!感謝!
程式碼分析
function zma () {
const manager = {
// 總訂閱器訂閱者儲存點
events: {},
// 總訂閱器一次訂閱者的存放點
onceEvents: [],
// 總訂閱器中需要凍結的訂閱者名
freezeEvents: [],
// 訂閱方法
on(type, fn) {},
// 釋出方法
fire(type, ...rest) {},
// 解除訂閱
unbind(type, fn) {},
// 一次訂閱
once(type, fn) {},
// 凍結訂閱者
freezeEvent(type) {},
// 解除凍結訂閱
clearFreezeEvent(type) {},
// 清除所有訂閱
clear() {},
// 生成當前元件的私有訂閱器
getProxy() {
return new ManagerProxy(this)
},
}
// 返回訂閱器
return manager
}
複製程式碼
匯流排的方法和大致輪廓實現
- 通過執行
zam函式
返回一個物件(總的訂閱器) - 屬性
events
是一個總的訂閱儲存點,存放著所有的訂閱者,包括只訂閱一次的訂閱者 - 屬性
onceEvents
同樣也是一個的訂閱器儲存點,與events不同的是,只存放只訂閱一次的訂閱者 - 屬性
freeeEvents
是一個凍結名單,存放著被凍結的訂閱者 - 方法
on
接收所有傳入的訂閱者,在傳入總訂閱器中,訂閱者名單和所要做的事情進行儲存 - 方法
fire
通過訂閱者名單,釋出這訂閱者所交待的事件 - 方法
once
接收訂閱一次的訂閱者,向onceEvents
向入訂閱者名單,再把所有的訂閱資訊交給總訂閱器進行記錄儲存 - 方法
unbind
根據傳入的訂閱者的資訊,解除某些訂閱者的訂閱 - 方法
freezeEvent
根據傳入訂閱者的名單,暫時凍結訂閱者的釋出 - 方法
clearFreezeEvent
根據傳入訂閱者名單,解決凍結訂閱者的釋出 - 方法
clear
清除總訂閱器上所的訂閱者 - 方法
getProxy
返回一個新的例項物件,孵化出對應當前元件各自內部的訂閱器,將manager總訂閱器當引數傳入,共享總訂閱器的所有方法
整個方法體全部定義在總訂閱器的物件上,通過getProxy
孵化出每個元件私有的訂閱器實列
呼叫
getProxy
孵化出對應組個元件內部管理的訂閱器
function ManagerProxy(extendSolt) {
this.extendSolt = extendSolt; // 上層的this指向manager物件
this.msgs = [];
}
ManagerProxy.prototype.on = function(type, fn) {
const result = this.extendSolt.on(type, fn);
if (result) {
this.msgs.push([type, fn]);
}
};
ManagerProxy.prototype.fire = function(type) {
this.extendSolt.fire(type);
};
ManagerProxy.prototype.once = function(type, fn) {
const result = this.extendSolt.once(type, fn);
if (result) {
this.msgs.push([type, fn]);
}
};
ManagerProxy.prototype.unbind = function(type) {
this.extendSolt.unbind(type);
};
ManagerProxy.prototype.freezeEvent = function(type) {
this.extendSolt.freezeEvent(type);
};
ManagerProxy.prototype.clearFreezeEvent = function(type) {
this.extendSolt.clearFreezeEvent(type);
};
ManagerProxy.prototype.dispose = function() {
const msgs = this.msgs;
let i = msgs.length;
while (i--) {
this.extendSolt.unbind(msgs[i][0], msgs[i][1]);
}
this.msgs = null;
this.extendSolt = null;
};
複製程式碼
私有訂閱器主流程:
通過ManagerProxy
建構函式new
出一個當前元件的訂閱器實列,所有的方法全都掛載到其原型鏈上,通過掛載到原型鏈上,所有new出來當前元件訂閱器的實列方法都是指向同一個引用,節省效能,減少記憶體的佔用
this.extendSolt
通過new ManagerProxy時,向每個實列傳入了總訂閱器物件,以便用總訂閱器和所有元件私有訂閱器的功能複用度
this.msgs
訂閱當前元件中所有的訂閱者,包括一次訂閱者訂閱者
on
呼叫總訂閱器的方法,一旦訂閱成功,則向當前元件訂閱器裡記錄訂閱fire
呼叫總訂閱器的fire方法,兩者共用once
呼叫總訂閱器once方法,同時訂閱成功後,則向當前元件訂閱器記錄訂閱unbind
freezeEvent
clearFreezeEvent
呼叫總訂閱器同樣的方法, 兩者共用dispose
迴圈當將要被銷燬的元件中所有的訂閱者,拿到訂閱名和訂閱事件,呼叫unbind方法,進行一個當前元件訂閱的銷燬
大致結構體已經很明確,無論是從總訂閱的大致結構,和私有訂閱器的設計初衷,在總訂閱器和私有訂閱器實現同一功能api運用,私有訂閱器有自我銷燬機制
從表面看內在
1 .訂閱方法
manager = {
events : {},
on (type, fn) {
if (isString(type) || isArray(type) && isFn(fn)) {
if (isArray(type)) {
// 如果傳入訂閱名是一個陣列,則遞迴呼叫on方法,達到可以共同訂閱
for (let key of type) {
this.on(key, fn);
}
} else {
// 通過總訂閱儲存點查詢,是否存在此訂閱名的訂閱空間
let eventHandler = this.events[type];
// 如果不存在,則把對應的訂閱空間設定為空陣列
if (!eventHandler) {
eventHandler = this.events[type] = [];
}
// 存在的情況下,獲取訂閱名對應的訂閱空間長度
let i = eventHandler.length;
// 把訂閱空間進行一個迴圈操作,
如果方法是指向同一個引用,則只存一份,直接return false
while (i--) {
if (eventHandler[i] == fn) {
return false;
}
}
// 把訂閱方法傳入訂閱名對應的訂閱空間中,return ture
eventHandler.push(fn);
return true;
}
}
return false;
}
}
複製程式碼
用events
物件來包裹每一個訂閱者,面對同名訂閱,所有的訂閱事件全放在對應的陣列中,以致於可以維護所有的同名訂閱者
2. 一次訂閱方法
once(type, fn) {
// 進行一個形參校驗
if (isString(type) && isFunc(fn)) {
// once只是一次訂閱,但是還是要通過on方法繼續向訂閱儲存點存放
const result = this.on(type, fn)
// 一旦成功,向一次訂閱儲存點進行儲存記錄,返回true
if (result) {
this.onceEvents.push([type, fn])
return true
}
// 否則返回false
return false
}
}
複製程式碼
從一次訂閱方法中可以看出,雖然一次訂閱但是還是必須把一次訂閱存放在events
物件中,但是同樣也要向onceEvents
中做好一次訂閱的記錄
3. 凍結訂閱者
freezeEvent(type) {
// 對形參進行一個校驗
if (isString(type) || isArray(type)) {
// 如果是多個事件名,調進行遞迴呼叫freezeEvent
if (isArray(type)) {
for (let key of type) {
this.freezeEvent(key);
}
} else {
// 向訂閱凍結儲存點傳入訂閱名
this.freezeEvents.push(type);
}
}
},
複製程式碼
凍結訂閱者,從訂閱名開始凍結,將所凍結的訂閱名存放在freezeEvents
中,一旦凍結,被 凍結的訂閱者不會發布
4. 解除凍結
clearFreezeEvent(type) {
if (isString(type) || isArray(type)) {
if (isArray(type)) {
// 如果是陣列,則進行遞迴解除凍結
for (let key of type) {
this.clearFreezeEvent(key);
}
} else {
// 通過訂閱名進行查詢
const index = this.freezeEvents.indexOf(type);
// 如果查到則把陣列中原本凍結名給去除,解除凍結
if (index >= 0) {
this.freezeEvents.splice(index, 1);
}
}
}
},
複製程式碼
在某些場景下通過凍結可以很好的維護訂閱者,不要返復的訂閱和解除訂閱,無論是對效能還是從功能方面來說,都是優先性!
5. 釋出訂閱
fire(type, ...rest) {
if (isString(type) || isArray(type)) {
if (isArray(type)) {
// 如果是陣列,則進行遞迴釋出
for (let key of type) {
this.fire(key, ...rest);
}
} else {
// 通過釋出名能過總訂閱儲存點拿到對應的釋出空間
let emitEvent = this.events[type];
// 如果沒有釋出空間,或者某訂閱名被凍結,直接退出
if (!emitEvent || this.freezeEvents.indexOf(type) >= 0) {
return false;
}
// 否則迴圈對應的訂閱釋出空間,觸發訂閱者的訂閱方法
let i = 0;
while (i < emitEvent.length) {
emitEvent[i](...rest);
i++;
}
// 同時通過訂閱名比較onceEvents儲存點進行一個比較
在相當同的訂閱中,比較events總的訂閱點和onceEvents一次訂閱點的
訂閱事件引用相同時,同時刪除者的訂閱,防止在下次釋出的時候再次觸發
for (let m = 0; m < this.onceEvents.length; m++) {
const onceEvents = this.onceEvents;
const curOnceEvents = onceEvents[m];
const curEventType = this.events[type];
if (curOnceEvents[0] === type) {
for (let j = 0; j < curEventType.length; j++) {
if (curOnceEvents[1] === curEventType[j]) {
curEventType.splice(j, 1);
onceEvents.splice(m, 1);
break;
}
}
}
}
// 比對過後,如果events某個訂閱者沒有任何訂閱事件上的話,則把當前訂閱者移除
if (!this.events[type].length) {
delete this.events[type];
}
}
}
},
複製程式碼
在釋出中,正是因為支援同名訂閱,所以要做到迴圈釋出,把每個訂閱名下的訂閱事件,進行一個迴圈釋出,同時對onceEvents
裡的一次訂閱點和總訂閱點進行一個比較,如果存在的話,進行移除訂閱者,因為支援同名訂閱,可能有些同名訂閱只訂閱一次,可能有些同名訂閱想訂閱多些,這裡進行了事件引用的比較,這樣就這可以區分同名的訂閱
6. 取消訂閱
unbind(type, fn) {
if (isString(type) || isFunc(fn)) {
if (!fn) {
delete this.events[type];
} else {
// 通過訂閱名拿到對應的取消訂閱空間
const eventHandler = this.events[type];
// 拿到訂閱空間的長度
let eventHandlerL = eventHandler.length;
// 如果有訂閱空間
if (eventHandler && eventHandlerL) {
// 有些情況下有兩種可能,是私有訂閱器需要進行元件級別的全部銷燬,還有一種是需要針對訂閱名進行銷燬
if (fn) {
while (eventHandlerL--) {
if (eventHandler[eventHandlerL] == fn) {
eventHandler.splice(eventHandlerL, 1);
break;
}
}
} else {
delete this.events[type]
}
// 比對過後,如果events某個訂閱者沒有任何訂閱事件上的話,則把當前訂閱者移除
if (!eventHandlerL) {
delete this.events[type];
}
}
}
}
},
複製程式碼
取消訂閱對於僅僅只有能過訂閱名進行取消訂閱的話,將會把所有訂閱名下的訂閱空間全都移除,同時也會導致如果是同名的訂閱的話,無法針對同名訂閱的某一個訂閱移除,所以儘量用不同名之間的區分去區別,如果非要針對同名事件取消其中想要的訂閱,在第二個引數,傳入繫結時的引用,把傳入的事件,額外提出來,在繫結和解綁的時候此時就是同一個引用,就可以針對性銷燬
7.元件銷燬
ManagerProxy.prototype.dispose = function() {
const msgs = this.msgs;
let i = msgs.length;
while (i--) {
this.extendSolt.unbind(msgs[i][0], msgs[i][1]);
}
this.msgs = null;
this.extendSolt = null;
};
複製程式碼
在每個元件私有的訂閱器上都有dispose
方法,通過拿到自己私有的訂閱儲存點this.msgs
進行一個迴圈,通過unbind
方法在總訂閱器中進行一一比較的銷燬,此時就有關聯到unbind
方法中有第二個fn
傳參時,在內部機制呼叫時,在總訂閱的儲存點中進行一個比對查詢進行銷燬當前執行銷燬元件中的訂閱者,注意在使用時,必須在元件銷燬時呼叫dispose
方法