我們?cè)谔幚順I(yè)務(wù)需求的時(shí)候,常常會(huì)遇到一些情況,在二級(jí)或者三級(jí)頁(yè)面進(jìn)行某些操作或者變更后,需要將結(jié)果通知到上級(jí)頁(yè)面去。比如:
- 選擇了某些配置項(xiàng),點(diǎn)擊保存后,外部頁(yè)面能夠立即變更
- 在上傳頭像頁(yè)面,上傳完畢后,外部頁(yè)面的頭像能夠立即顯示為新頭像。
所以,這個(gè)時(shí)候就涉及到如何在頁(yè)面之間通信的問(wèn)題了。
跨頁(yè)面通信進(jìn)一步說(shuō)其實(shí)就是一個(gè)程序內(nèi)部的事件通知機(jī)制問(wèn)題,在其他平臺(tái)或者OS上都一些相應(yīng)的實(shí)現(xiàn),比如:
- iOS SDK自帶的 NotificationCenter
- Android 平臺(tái)著名的第三方庫(kù) EventBus
目前微信小程序官方SDK還沒(méi)有提供 Event API 來(lái)幫助開(kāi)發(fā)者實(shí)現(xiàn)頁(yè)面間通信,所以我們今天來(lái)看看,自己如何實(shí)現(xiàn)這樣一個(gè)簡(jiǎn)單的小工具。
Quick And Dirty
我們知道,在小程序里面一個(gè)頁(yè)面的變化,是通過(guò)調(diào)用 setData 函數(shù)來(lái)實(shí)現(xiàn)的。所以想做到在二級(jí)頁(yè)面里讓一級(jí)頁(yè)面產(chǎn)生變化,最 Quick And Dirty 的做法就是把一級(jí)頁(yè)面的 this 傳入到二級(jí)頁(yè)面去,這樣我們?cè)诙?jí)頁(yè)面調(diào)用 page1.setData(…) 就可以立即引發(fā)外部的變化。
但是這并不是一個(gè)好的方案,不僅產(chǎn)生了頁(yè)面的耦合,而且也并不能處理復(fù)雜的數(shù)據(jù)邏輯,因?yàn)槎?jí)頁(yè)面不并清楚也不應(yīng)該關(guān)心一級(jí)頁(yè)面想怎么處理當(dāng)前數(shù)據(jù)。所以二級(jí)頁(yè)面只應(yīng)該把變更后的數(shù)據(jù)通知給一級(jí)頁(yè)面即可,至于一級(jí)頁(yè)面是想刷新界面,還是想本地存儲(chǔ)或者發(fā)起網(wǎng)絡(luò)通信,別人都不需知曉了。
簡(jiǎn)單的Callback
如果只是想把數(shù)據(jù)通知給外部頁(yè)面,那應(yīng)該怎么做呢?
我們來(lái)看看第二個(gè)方案,如果想產(chǎn)生一個(gè)通知,這里就需要用到 callback 機(jī)制了。
即關(guān)心數(shù)據(jù)變化的頁(yè)面,注冊(cè)一個(gè) callback 函數(shù)到一個(gè)公共的地方;而數(shù)據(jù)變更者在變更數(shù)據(jù)后,將新的數(shù)據(jù)放入同一個(gè)公共的地方;在放入數(shù)據(jù)時(shí),同時(shí)調(diào)用這個(gè) callback 函數(shù),讓 callback 函數(shù)實(shí)現(xiàn)者接收到這個(gè)變化。
哪這個(gè)公共的地方在哪里呢?
第一反應(yīng)就是 app.js 里面,因?yàn)樾〕绦蛱峁┝艘粋€(gè) API 叫做 getApp(),讓 page 初始化時(shí),可以通過(guò)以下代碼:
var app = getApp()
來(lái)獲取 app 實(shí)例,從而實(shí)現(xiàn)全局的數(shù)據(jù)共享,并且微信也很貼心的在 Demo 代碼里面留了一個(gè) globalData 字段,以暗示開(kāi)發(fā)者這里是可以用來(lái)存儲(chǔ)全局?jǐn)?shù)據(jù)的。
App({
...
globalData:{
userInfo:null
}
...
})
基于 app.js 方案的偽代碼如下:
//app.js
App({
addListener: function(callback) {
this.callback = callback;
},
setChangedData: function(data) {
this.data = data;
if(this.callback != null) {
this.callback(data);
}
}
})
然后我們?cè)谝患?jí)頁(yè)面的 onLoad中 調(diào)用 addListener:
//page1.js
var app = getApp()
Page({
onLoad: function () {
app.addListener(function(changedData) {
that.setData({
data: changedData
});
});
}
})
在二級(jí)頁(yè)面數(shù)據(jù)變更的地方調(diào)用:
//page2.js
var app = getApp()
Page({
onBtnPress: function() {
app.setChangedData('page2-data');
}
})
一個(gè)基本合格的方案
以上就是跨頁(yè)面通信的最基本原理,不過(guò)這也是一個(gè)很 dirty 的方案,因?yàn)樯厦娴拇a只能支持一種 Event 的通知,而且也不能針對(duì)這個(gè) Event 添加多個(gè)監(jiān)聽(tīng)者(比如有多個(gè)頁(yè)面需要同時(shí)知道某數(shù)據(jù)變更)。
讓我們來(lái)看看一個(gè)基本合格的 Event 管理器應(yīng)該具備怎樣的能力?
- 支持多種 Event 的通知
- 支持對(duì)某一 Event 可以添加多個(gè)監(jiān)聽(tīng)者
- 支持對(duì)某一 Event 可以移除某一監(jiān)聽(tīng)者
- 將 Event 的存儲(chǔ)和管理放在一個(gè)單獨(dú)模塊中,可以被所有文件全局引用
根據(jù)以上的描述,我們來(lái)設(shè)計(jì)一個(gè)新的 Event 模塊,對(duì)應(yīng)上面的能力,它應(yīng)該具有如下三個(gè)函數(shù):
- on 函數(shù),用來(lái)向管理器中添加一個(gè) Event 的 Callback,且每一個(gè) Event 必須有全局唯一的 EventName,函數(shù)內(nèi)部通過(guò)一個(gè)數(shù)組來(lái)保存同一 Event 的多個(gè) Callback
- remove 函數(shù),用來(lái)向管理器移除一個(gè) Event 的 Callback
- emit 函數(shù),用來(lái)觸發(fā)一個(gè) Event
我們?cè)谛〕绦虻?utils 目錄中,新建一個(gè) event.js 文件,來(lái)作為一個(gè)獨(dú)立的模塊,偽代碼如下:
//event.js
var events = {};
function on(name, callback) {
var callbacks = events[name];
addToCallbacks(callbacks, callback);
}
function remove(name, callback) {
var callbacks = events[name];
removeFromCallbacks(callbacks, callback);
}
function emit(name, data) {
var callbacks = events[name];
emitToEveryCallback(callbacks, data);
}
exports.on = on;
exports.remove = remove;
exports.emit = emit;
我們來(lái)看看在一二級(jí)頁(yè)面應(yīng)該如何來(lái)使用這個(gè) Event 模塊
在二級(jí)頁(yè)面中觸發(fā)事件:
//page2.js
var event = require('../../utils/event.js');
Page({
onBtnPress: function() {
event.emit('DataChanged', 'page2-data');
}
});
在一級(jí)頁(yè)面的 onLoad 中監(jiān)聽(tīng)事件,onUnload 中取消監(jiān)聽(tīng):
//page1.js
var event = require('../../utils/event.js');
Page({
onLoad: function() {
var that = this;
event.on('DataChanged', function(changedData) {
that.setData({
data: changedData
});
});
},
onUnload: function() {
event.remove('DataChanged', ...);
}
});
咦,似乎哪里不對(duì)?
remove 需要接受兩個(gè)參數(shù),第一個(gè)是 EventName,第二個(gè)是 Callback,但是我們的 Callback 以匿名函數(shù)的方式寫(xiě)在了 event.on(...) 的調(diào)用語(yǔ)句里面
好吧,那我們不得不修改一下語(yǔ)句的調(diào)用方式:
//page1.js
var event = require('../../utils/event.js');
Page({
onDataChanged: function(changedData) {
this.setData({
data: changedData
})
},
onLoad: function() {
event.on('DataChanged', this.onDataChanged);
},
onUnload: function() {
event.remove('DataChanged', this.onDataChanged);
}
});
這樣就 OK 了么?NO NO NO NO
熟悉 Javascript this 這個(gè)大坑的朋友們一定會(huì)知道,在 onDataChanged 這個(gè)函數(shù)中調(diào)用的 this 并不是我們 Page 中的那個(gè) this,所以根本不可能調(diào)用到 this.setData(....),于是我們用 bind 大法稍微調(diào)整一下:
onLoad: function() {
event.on('DataChanged', this.onDataChanged.bind(this));
}
onUnload: function() {
event.remove('DataChanged', this.onDataChanged.bind(this));
}
現(xiàn)在OK了么?NO NO NO NO!如果大伙敲代碼試試,就會(huì)發(fā)現(xiàn)依然還是不行!
因?yàn)?/p>
this.onDataChanged.bind(this)
會(huì)產(chǎn)生一個(gè)新的匿名函數(shù),即 bind的 返回值是一個(gè)函數(shù),那么在 onLoad 和 onUnload 里面,各自調(diào)用了 bind 大法,從而產(chǎn)生了各自的匿名函數(shù),也就是說(shuō) event.remove(...) 塞進(jìn)去的那個(gè)函數(shù),并不是 event.on(...) 塞進(jìn)去的那個(gè)函數(shù),這樣就造成了 remove 時(shí)無(wú)法正確匹配。removeFromCallbacks 的偽代碼大致如下:
function removeFromCallbacks(callbacks, callback) {
var newCallbacks = [];
for(var item in callbacks) {
if(item != callback) {
newCallbacks.push(item);
}
}
return newCallbacks;
}
所以我們會(huì)發(fā)現(xiàn) remove 傳入的 callback 永遠(yuǎn)無(wú)法在 callbacks 數(shù)組中被匹配到,從而也就無(wú)法正確移除了。
最終的代碼實(shí)現(xiàn)
當(dāng) EventName + Callback 無(wú)法唯一決定需要移除的監(jiān)聽(tīng)者時(shí),那么自然想到的就是再增加一個(gè) key 值,我們可以用Page自身的某個(gè)特性來(lái)做 key,比如 page name ,新的 remove 原型如下:
function remove(eventName, pageName, callback);
pageName 是一個(gè)字符串,如果開(kāi)發(fā)者不能做到全局內(nèi) page name 唯一的話(比如開(kāi)發(fā)者一不小心寫(xiě)錯(cuò)了),那就可能會(huì)出現(xiàn)后來(lái)監(jiān)聽(tīng)者沖掉前面監(jiān)聽(tīng)者的情況,從而造成無(wú)法收到通知的 bug。
所以這里看起來(lái)還是用 page 的 this 做 key 比較靠譜,修改后的函數(shù)原型如下:
function on(name, self, callback);
function remove(name, self, callback);
讓我們來(lái)看看內(nèi)部具體怎么實(shí)現(xiàn)。以下是一個(gè)完整的 on 函數(shù)實(shí)現(xiàn):
function on(name, self, callback) {
var tuple = [self, callback];
var callbacks = events[name];
if (Array.isArray(callbacks)) {
callbacks.push(tuple);
}
else {
events[name] = [tuple];
}
}
- 第二行我們將 self (即 page 的 this)和 callback 合并成一個(gè) tuple
- 第三行從 events 容器中,取出該 EventName 下的監(jiān)聽(tīng)者數(shù)組 callbacks
- 如果該數(shù)組存在,則將 tuple 加入數(shù)組;如果不存在,則新建一個(gè)數(shù)組。
remove的完整實(shí)現(xiàn):
function remove(name, self) {
var callbacks = events[name];
if (Array.isArray(callbacks)) {
events[name] = callbacks.filter((tuple) => {
return tuple[0] != self;
});
}
}
- 第二行從 events 容器中,取出該 EventName 下的監(jiān)聽(tīng)者數(shù)組 callbacks
- 如果 callbacks 不存在,則直接返回
- 如果存在,則調(diào)用 callbacks.filter(fn) 方法
filter 方法的含義是通過(guò) fn 來(lái)決定是否過(guò)濾掉 callbacks 中的每一個(gè)項(xiàng)。fn 返回 true 則保留,fn 返回 false 則過(guò)濾掉。所以我們調(diào)用 callbacks.filter(fn) 后,callbacks 中的每一個(gè) tuple 都會(huì)被依次判定。
fn的定義為:
(tuple) => {
return tuple[0] != self;
}
tuple 中的第一個(gè)元素 self 和 remove 傳入的 self 相比較,如果不相等則返回 true 被保留,如果相等則返回 false 被過(guò)濾掉。
callbacks.filter(fn) 會(huì)返回一個(gè)新的數(shù)組,然后重新寫(xiě)入 events[name],最終達(dá)到移除callbacks中某一項(xiàng)的邏輯。
最后再來(lái)看看emit的實(shí)現(xiàn):
function emit(name, data) {
var callbacks = events[name];
if (Array.isArray(callbacks)) {
callbacks.map((tuple) => {
var self = tuple[0];
var callback = tuple[1];
callback.call(self, data);
});
}
}
- 第二行從 events 容器中,取出該 EventName 下的監(jiān)聽(tīng)者數(shù)組 callbacks
- 如果 callbacks 不存在,則直接返回
- 如果存在,則調(diào)用 callbacks.map(fn) 方法
和 filter 的用法類(lèi)似,map 函數(shù)的作用相當(dāng)于 for 循環(huán),依次取出 callbacks 中的每一個(gè)項(xiàng),然后對(duì)其執(zhí)行 fn(tuple),從其名字就可以看出 map 就是映射變換的意思,將 item 變換為另外一種東西,這個(gè)映射關(guān)系就是fn。
fn 的定義為:
(tuple) => {
var self = tuple[0];
var callback = tuple[1];
callback.call(self, data);
}
對(duì)傳入的 tuple,分別取出 self 和 callback,然后調(diào)用 Javascript 的 call大法:
fn.call(this, args)
從而最終實(shí)現(xiàn)調(diào)用到監(jiān)聽(tīng)者的目的。
講到這里就基本上差不多了,因?yàn)?Event 模塊持有了 Page 的 this,所以一定要在 Page 的 Unload 函數(shù)中調(diào)用 event.remove(…),不然會(huì)造成內(nèi)存泄露。
源代碼
event.js 的完整源代碼和Demo請(qǐng)見(jiàn) https://github.com/dannnney/weapp-event