[Flutter]EventBus的使用和底層實現(xiàn)分析

什么是EventBus

EventBus是全局事件總線,底層通過Stream來實現(xiàn);它可以實現(xiàn)不同頁面的跨層訪問,通過Stream的機制來實現(xiàn)不同widget之間的狀態(tài)共享.

這里我們使用的是官方提供了一個Event_Bus庫.
在pubsec.yaml文件導(dǎo)入:

event_bus: ^1.1.0

在使用的文件內(nèi):

import 'package:event_bus/event_bus.dart';
使用方式

這里舉個例子:有三個界面A,B,C, 從A進入B, B再進入C. 在C中點擊按鈕,然后A的背景色會變成紅色.這里實際上就形成了跨層的訪問C->A.

首先我們需要定義一個EventBus對象來訂閱事件流.這里我們通過一個工廠方法來創(chuàng)建一個唯一的EventBus對象.

class GlobalEventBus{
  EventBus event;
  factory GlobalEventBus() => _getInstance();

  static GlobalEventBus get instance => _getInstance();

  static GlobalEventBus _instance;

  GlobalEventBus._internal() {
    // 創(chuàng)建對象
    event = EventBus();
  }

  static GlobalEventBus _getInstance() {
    if (_instance == null) {
      _instance = GlobalEventBus._internal();
    }
    return _instance;
  }
}

用一個全局對象的好處就是不需要將eventbus當(dāng)做參數(shù)進行傳遞了,而且更符合全局總線的概念.

1.首先,我們需要先創(chuàng)建一個事件用來監(jiān)聽,我們將這個事件命名為BackgroundColorChangeEvent,這個事件其實是一個類,用來當(dāng)做共享數(shù)據(jù)的載體,我們可以在這個類中加入不同的屬性.例如:

class BackgroundColorChangeEvent{
  Color color;
  BackgroundColorChangeEvent(this.color);
}

這里我們就可以把color當(dāng)做參數(shù)通過BackgroundColorChangeEvent對象傳遞到A中,然后改變A的背景色.

2.然后在A訂閱事件:

GlobalEventBus().event.on<BackgroundColorChangeEvent>().listen((event) {
      Color color = event.color;
      setState(){
        _backgroundColor = color;
      }
});

以上就完成了事件的創(chuàng)建和訂閱.

3.接著我們就需要在C中發(fā)布這個事件,Event_Bus提供了一個fire方法來分發(fā)事件.

FlatButton(){
    onPress:(){
       GlobalEventBus().event.fire(BackgroundColorChangeEvent(Colors.red)) ;
    }
}

這里我們通過全局event對象,分發(fā)了BackgroundColorChangeEvent事件,并且攜帶了一個color參數(shù)過去.此時A已經(jīng)訂閱了這個事件,于是背景色就改變了.

需要注意到的是在使用之后需要關(guān)閉event事件流,不然會造成內(nèi)存泄漏,調(diào)用如下代碼即可:

GlobalEventBus().event.destroy();
EventBus的底層實現(xiàn)

以上就是EventBus的使用方式,非常的簡單.但它底層是怎么實現(xiàn)的呢?我們一步步來分析.

1.創(chuàng)建EventBus

首先我們從EventBus的初始化開始,進入event_bus.dart源文件中:

EventBus({bool sync = false})
      : _streamController = StreamController.broadcast(sync: sync);

我們可以看出,EventBus對象初始化實際上初始化了一個_streamController對象,而這個對象是通過StreamControllerbroadcast(sync: sync)方法初始化的,這里大致可以看出來,EventBus的底層實際上就是通過Stream來實現(xiàn)的.這里默認帶了sync參數(shù),表示是否同步,默認為async.

而進入broadcast(sync: sync)方法中:

factory StreamController.broadcast(
      {void onListen(), void onCancel(), bool sync: false}) {
    return sync
        ? new _SyncBroadcastStreamController<T>(onListen, onCancel) // 同步廣播
        : new _AsyncBroadcastStreamController<T>(onListen, onCancel); // 異步廣播
  }

可以看到sync參數(shù)作用就是返回一個是同步或異步廣播流控制器.
這里同步和異步的區(qū)別是:

如果sync為true,則事件將通過fire方法直接傳遞給流的監(jiān)聽者;如果為false,在創(chuàng)建EventBus之后,才將事件延后傳遞給監(jiān)聽者。

那么事件是怎么被監(jiān)聽到的呢?

2. EventBus進行訂閱監(jiān)聽

事件我們是通過event.on().listen()方法來監(jiān)聽的,進入on()的dart源碼中,

Stream<T> on<T>() {
    if (T == dynamic) {
      return streamController.stream;
    } else {
      return streamController.stream.where((event) => event is T).cast<T>();
    }
  }

我們可以看到on()返回了一個Stream,并且用泛型約定了事件類型.如果不傳泛型約定,那么就默認監(jiān)聽所有的事件; 如果添加了泛型T,那么就只會監(jiān)聽T這個事件.

然后我們在看listen()方法:

StreamSubscription<T> listen(void onData(T event),
      {Function onError, void onDone(), bool cancelOnError});

在方法參數(shù)中,我們可以發(fā)現(xiàn)onData(T event)這個方法回調(diào),正是這個方法參數(shù)回調(diào)了我們需要監(jiān)聽訂閱的事件T.至于怎么接收到這個onData()回調(diào)的,我們后面再看,先了解下fire()方法的實現(xiàn).

3. EventBus發(fā)布事件

我們進入fire()方法中,

void fire(event) {
    streamController.add(event);
  }

streamController添加了這個事件,平淡無奇,繼續(xù)看下去.

void add(T value) {
    if (!_mayAddEvent) throw _badEventState();
    _add(value);
  }

_mayAddEvent表示流關(guān)閉之后或者正在add stream期間,可能無法添加新事件.

在看看_add(value):

void _add(T value) {
    if (hasListener) {
      _sendData(value);
    } else if (_isInitialState) {
      _ensurePendingEvents().add(new _DelayedData<T>(value));
    }
  }

如果hasListener已經(jīng)有被訂閱了,那么就發(fā)送這個事件,如果是_isInitialState初始化狀態(tài),就掛起這個事件,再add()一個_DelayedData;

簡單說下_DelayedData,這是一個繼承自_DelayedEvent的對象,它其實保存了我們這個事件value,然后利用perform方法在某個時機進行事件發(fā)放.它的實現(xiàn)如下,

class _DelayedData<T> extends _DelayedEvent<T> {
  final T value;
  // 保存這個事件
  _DelayedData(this.value);
  // perform方法用于某個時機發(fā)放這個事件
  void perform(_EventDispatch<T> dispatch) {
    dispatch._sendData(value);
  }
}

再進入_sendData()方法

void _sendData(T data) {
    assert(!_isCanceled);
    assert(!_isPaused);
    assert(!_inCallback);
    bool wasInputPaused = _isInputPaused;
    _state |= _STATE_IN_CALLBACK;
    _zone.runUnaryGuarded(_onData, data);
    _state &= ~_STATE_IN_CALLBACK;
    _checkState(wasInputPaused);
  }

要看懂這段代碼,我們首先要明白幾個變量和方法.

(1). _zone: 是一個Zone對象,通過源碼注釋我們可以大概了解一下它的作用:

A zone represents an environment that remains stable across asynchronous calls.

Code is always executed in the context of a zone, available as [Zone.current]. 
The initial main function runs in the context of the default zone ([Zone.root]). 
Code can be run in a different zone using either [runZoned], 
to create a new zone, or [Zone.run] to run code in the context of 
an existing zone likely created using [Zone.fork].

Developers can create a new zone that overrides some of the functionality of an existing zone. 
For example, custom zones can replace of modify the behavior of print, timers, 
microtasks or how uncaught errors are handled.

zone表示在異步過程保持穩(wěn)定的一個環(huán)境,類似于iOS沙箱機制.Dart代碼通常都是在這個環(huán)境的上下文中執(zhí)行,在這個環(huán)境中你可以對里面的代碼做很多操作,例如異常捕捉等.而此時這個zone取值是Zone.current,就是當(dāng)前的環(huán)境.

(2). runUnaryGuarded(_onData, data):這個方法的作用就是在此環(huán)境中用參數(shù)執(zhí)行給定的方法,并捕獲同步錯誤.有點類似于iOS中performSelector:withObject:方法.指定的方法就是_onData,參數(shù)就是data,也就是我們傳遞過來的事件.

(3). _onData: 這個方法就是我們回調(diào)事件的方法,它是通過上述zone_zone.registerUnaryCallback<dynamic, T>(handleData)方法拿到的.

void onData(void handleData(T event)) {
    handleData ??= _nullDataHandler;
    // TODO(floitsch): the return type should be 'void', and the type
    // should be inferred.
    _onData = _zone.registerUnaryCallback<dynamic, T>(handleData);
  }

從上面的事件訂閱監(jiān)聽我們知道是在listen()方法中的onData()回調(diào)中監(jiān)聽到事件,那很明顯這個_onDatalisten中的onData肯定有某種關(guān)聯(lián),知道了這個關(guān)聯(lián),那整個監(jiān)聽流程就通了.

實際上底層有對listen()中的onData()進行一層轉(zhuǎn)換,

// onData() ->>> onData(void handleData(T event))

/**
   * Replaces the data event handler of this subscription.
   *
   * The [handleData] function is called for each element of the stream
   * after this function is called.
   * If [handleData] is `null`, further elements are ignored.
   *
   * This method replaces the current handler set by the invocation of
   * [Stream.listen] or by a previous call to [onData].
   */
  void onData(void handleData(T data));

從源碼的注釋可以看出這里的onData()確實是被替換成了onData(void handleData(T event)).
那么_zone.registerUnaryCallback<dynamic, T>(handleData)拿到的_onData就是就是我們在listen中傳入的onData()回調(diào)了.

到這里,訂閱和發(fā)布事件就形成了一個閉環(huán).這也就是EventBus實現(xiàn)的原理. 其實Flutter很多工具底層都是基于Stream,后面會專門分析一下Stream的實現(xiàn)原理.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內(nèi)容