如何在微信小程序里面實(shí)現(xiàn)跨頁(yè)面通信?

我們?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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容