作者 | Danny 動靜科技軟件總監
我們在處理業務需求的時候,常常會遇到一些情況:
在二級或者三級頁面進行某些操作或者變更後,需要將結果通知到上級頁面去。比如:
- 選擇了某些配置項,點擊保存後,外部頁面能夠立即變更
- 在頭像上傳頁面,上傳完畢後,外部頁面的頭像能夠立即顯示爲新頭像
這個時候就涉及到如何在頁面之間通信的問題了。跨頁面通信,其實就是一個程序內部的事件通知機制問題,在其他平臺或者 OS 上都一些相應的實現,比如:
- iOS SDK 自帶的 NotificationCenter
- Android 平臺著名的第三方庫 EventBus
目前,微信小程序官方 SDK 還沒有提供 Event API 來幫助開發者實現頁面間通信。所以,今天知曉程序邀請到「雲夢聲音嚮導」小程序的開發者 Danny,教大家如何實現這樣一個簡單的小工具。
想和本文作者直接交流?現在就加入知曉程序的開發交流羣吧。關注微信號 zxcx0101,點擊菜單欄的「加羣交流」即可。
Quick and Dirty
我們知道,在小程序裏面一個頁面的變化,是通過調用 setData
函數來實現的。
所以,想做到在二級頁面裏讓一級頁面產生變化,最 quick 也最 dirty 的做法就是,把一級頁面的 this
傳入到二級頁面去。這樣,我們在二級頁面調用 page1.setData(…)
就可以立即引發外部的變化。
但是這並不是一個好的方案,不僅產生了頁面的耦合,而且也並不能處理複雜的數據邏輯。因爲,二級頁面不併清楚也不應該關心一級頁面想怎麼處理當前數據。
所以,二級頁面只應該把變更後的數據通知給一級頁面即可,至於一級頁面是想刷新界面,還是想本地存儲或者發起網絡通信,都與其他頁面沒有關係了。
簡單的 Callback
如果只是想把數據通知給外部頁面,那應該怎麼做呢?現在,我們來考慮另一個方案。
如果需要產生一個通知,就需要用到 callback
機制。它需要做到:
- 瞭解數據變化的頁面,註冊一個
callback
函數到一個公共的地方(所有頁面可以訪問的地方) - 而數據變更者在變更數據後,將新的數據放入同一個公共的地方
- 在放入數據時,同時調用這個
callback
函數,讓callback
函數實現者接收到這個變化
那這個「公共的地方」在哪裏呢? 第一反應就是全局邏輯頁面 app.js
裏面。
小程序提供了一個叫做 getApp()
的 API,它可以在 page
初始化時,通過 var app = getApp()
來獲取 app 實例,從而實現全局的數據共享。
而且,微信也很貼心的在新建項目之後的 quick start 代碼裏,留了一個 globalData
字段,暗示開發者這裏是可以用來存儲全局數據的。
我們首先對全局邏輯代碼 app.js
動刀子,增加 addListener
方法。
//app.js
App({
addListener: function (callback) {
this.callback = callback;
},
setChangedData: function (data) {
this.data = data;
if (this.callback != null) {
this.callback(data);
}
}
})
然後,我們在其他頁面合適的位置調用 addListener
。
//page1.js
var app = getApp()
Page({
onLoad: function () {
app.addListener(function (changedData) {
that.setData({
data: changedData
});
});
}
})
//page2.js
var app = getApp()
Page({
onBtnPress: function () {
app.setChangedData('page2-data');
}
})
一個基本合格的方案
以上就是跨頁面通信的最基本原理,但這也是一個很 dirty 的方案。
因爲,上面的代碼只能支持一種 Event
的通知,而且也不能針對這個 Event
添加多個監聽者(比如有多個頁面需要同時知道某數據變更)。
一個基本合格的 Event
管理器應該具備怎樣的能力?
- 支持多種
Event
的通知 - 支持對某一
Event
可以添加多個監聽者 - 支持對某一
Event
可以移除某一監聽者 - 將
Event
的存儲和管理放在一個單獨模塊中,可以被所有文件全局引用
根據以上的描述,我們來設計一個新的 Event
模塊,它應該具有如下三個函數:
on
函數:用來向管理器中添加一個Event
的Callback
,且每一個Event
必須有全局唯一的EventName
,函數內部通過一個數組來保存同一Event
的多個Callback
remove
函數:用來向管理器移除一個Event
的Callback
emit
函數:用來觸發一個Event
我們現在就來動手構建這樣一個新的模塊。
首先,在小程序的 utils
目錄中,新建一個 event.js
文件。
//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;
之後在頁面中,我們就可以使用這個 Event
模塊。
首先,我們在二級頁面中觸發事件。
//page2.js
var event = require('../../utils/event.js');
Page({
onBtnPress: function () {
event.emit('DataChanged', 'page2-data');
}
});
//然後,我們在一級頁面中在 onLoad() 中監聽事件、在 onUnload() 中取消監聽:
//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', ...);
}
});
咦,似乎哪裏不對?
remove
需要接受兩個參數,第一個是 EventName
,第二個是 Callback
,但是我們的 Callback
以匿名函數的方式寫在了 event.on(...)
的調用語句裏面。
好吧,那我們不得不修改一下語句的調用方式:
//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
這個大坑的朋友們一定會知道,在 onDataChanged()
這個函數中調用的 this,
並不是我們 Page
中的那個 this
,所以根本不可能調用到 this.setData(....)
,於是我們用 bind
大法稍微調整一下:
onLoad: function () {
event.on('DataChanged', this.onDataChanged.bind(this));
}
onUnload: function () {
event.remove('DataChanged', this.onDataChanged.bind(this));
}
現在 OK 了麼?NO NO NO NO!如果大夥敲代碼試試,就會發現依然還是不行!
因爲 this.onDataChanged.bind(this)
會產生一個新的匿名函數,即 bind
的返回值是一個函數。
那麼,在 onLoad
和 onUnload
裏面,各自調用了 bind
大法,從而產生了各自的匿名函數。
也就是說 event.remove(...)
塞進去的那個函數,並不是 event.on(...)
塞進去的那個函數。這樣就造成了 remove
時無法正確匹配。
removeFromCallbacks
的僞代碼大致如下:
function removeFromCallbacks(callbacks, callback) {
var newCallbacks = [];
for (var item in callbacks) {
if (item != callback) {
newCallbacks.push(item);
}
}
return newCallbacks;
}
我們會發現 remove
傳入的 callback,
永遠無法在 callbacks
數組中被匹配到,從而也就無法正確移除了。
最終的代碼實現
當 EventName + Callback
無法唯一決定需要移除的監聽者時,那麼自然想到的就是再增加一個 key
值,我們可以用 Page
自身的某個特性來做 key
,比如 pageName
,新的 remove
原型如下:
function remove(eventName, pageName, callback);
pageName 是一個字符串,如果開發者不能做到全局內 page name 唯一的話(比如開發者一不小心寫錯了),那就可能會出現後來監聽者沖掉前面監聽者的情況,從而造成無法收到通知的 bug。
所以,這裏看起來還是用 page 的 this 做 key 比較靠譜,修改後的函數原型如下:
function on(name, self, callback);
function remove(name, self, callback);
//讓我們來看看內部具體怎麼實現。以下是一個完整的 on 函數實現:
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
合併成一個 tuple;
第三行從 events
容器中,取出該 EventName
下的監聽者數組 callbacks。
如果該數組存在,則將 tuple
加入數組;如果不存在,則新建一個數組。
remove
的完整實現:
function remove(name, self) {
var callbacks = events[name];
if (Array.isArray(callbacks)) {
events[name] = callbacks.filter((tuple) = & gt; {
return tuple[0] != self;
});
}
}
第二行從 events
容器中,取出該 EventName
下的監聽者數組 callbacks。
如果 callbacks
不存在,則直接返回;如果存在,則調用 callbacks.filter(fn)
方法。
filter
方法的含義是通過 fn
來決定是否過濾掉 callbacks
中的每一個項。fn
返回 true
則保留,fn
返回 false
則過濾掉。
所以,我們調用 callbacks.filter(fn)
後,callbacks
中的每一個 tuple
都會被依次判定。
fn
的定義爲:
(tuple) = & gt; {
return tuple[0] != self;
}
tuple
中的第一個元素 self
和 remove
傳入的 self
相比較。
如果不相等則返回 true
被保留,如果相等則返回 false
被過濾掉。
callbacks.filter(fn)
會返回一個新的數組,然後重新寫入 events[name]
,最終達到移除 callbacks
中某一項的邏輯。
最後再來看看 emit 的實現:
function emit(name, data) {
var callbacks = events[name];
if (Array.isArray(callbacks)) {
callbacks.map((tuple) = & gt; {
var self = tuple[0];
var callback = tuple[1];
callback.call(self, data);
});
}
}
第二行從 events
容器中,取出該 EventName
下的監聽者數組 callbacks。
如果 callbacks
不存在,則直接返回;如果存在,則調用 callbacks.map(fn)
方法。
和 filter
的用法類似,map
函數的作用相當於 for
循環,依次取出 callbacks
中的每一個項,然後對其執行 fn(tuple)。
從其名字就可以看出 map
就是映射變換的意思,將 item
變換爲另外一種東西,這個映射關係就是 fn
。
fn
的定義爲:
(tuple) = & gt; {
var self = tuple[0];
var callback = tuple[1];
callback.call(self, data);
}
對傳入的 tuple
,分別取出 self
和 callback
,然後調用 Javascript 的 call
大法:
fn.call(this, args)
從而最終實現調用到監聽者的目的。
講到這裏就基本上差不多了,因爲 Event
模塊持有了 Page
的 this
,所以一定要在 Page
的 Unload
函數中調用 event.remove(…)
,不然會造成內存泄露。
文章來源:https://github.com/danneyyang/weapp-event/blob/master/README.md