Vue $dispatch 和 $broadcast 詳解

littleLane發表於2019-03-06

00 前言

$dispatch$broadcast 作為一對情侶 ?屬性,在 Vue 1.0 中主要用來實現基於元件樹結構的事件流通訊 —— 通過向上或向下以冒泡的形式傳遞事件流,以實現巢狀父子元件的通訊。但是由於其顯功能缺陷,在 Vue 2.0 中就被移除了。雖然 Vue 官網已經不再支援使用 $dispatch$broadcast 進行元件通訊,但是在很多基於 Vue 的 UI 框架中都有對其的封裝,包括 element-uiiview 等等。

那麼 $dispatch$broadcast 到底是怎麼工作,其底層又是怎麼實現的呢?接下來,我們就詳細的說一說!

01 $dispatch 詳解

為了追根溯源,我們還是先去 Vue 1.0 的文件你觀摩一下其概念吧!

概念:

Dispatch an event, first triggering it on the instance itself, and then propagates upward along the parent chain. The propagation stops when it triggers a parent event listener, unless that listener returns true. Any additional arguments will be passed into the listener’s callback function.

上面的一段英文定義來自 Vue 1.0 官方文件,其大致的意思是說:dispatch 是一個事件,首先會在自己例項本身上觸發,然後沿父鏈向上傳播。當它觸發父元件上的事件偵聽器時傳播即會停止,除非該偵聽器返回 true。 任何其他引數都將傳遞給偵聽器的回撥函式。

引數:

dispatch 會接收兩中引數:event 是事件名稱,[...args] 是觸發事件時傳遞給回撥函式的引數。

**例子:

// 建立一個 parent 元件
var parent = new Vue();

// 建立一個 child1 元件,其父元件指向 parent
var child1 = new Vue({ parent: parent });

// 建立一個 child2 元件,其父元件指向 child1
var child2 = new Vue({ parent: child1 });

// 在 parent 元件監聽名為 test 的事件,並繫結了一個回撥函式
parent.$on('test', function () {
  console.log('parent notified');
});

// 在 child1 元件監聽名為 test 的事件,並繫結了一個回撥函式
child1.$on('test', function () {
  console.log('child1 notified');
});

// 在 child2 元件監聽名為 test 的事件,並繫結了一個回撥函式
child2.$on('test', function () {
  console.log('child2 notified');
});
複製程式碼

說到這裡,parentchild1child2 三個元件之間的關係可以展示成如下的關係圖:

高階元件 (4).png

// 在 child2 元件中通過 dispatch 觸發 test 事件
child2.$dispatch('test');

// 事件執行會輸出如下結果
// -> "child2 notified"
// -> "child1 notified"
複製程式碼

當執行 child2.$dispatch('test'); 時,首先會觸發 child2 元件裡面監聽的 test 事件的回撥函式,輸出 'child2 notified',根據上面官方文件的定義,事件會沿著元件關係鏈一直向上傳遞,然後傳遞到 child1 元件,觸發監聽事件輸出 "child1 notified",但是該偵聽器沒有返回 true,所以事件傳遞到此就結束了,最終的輸出結果就只有 "child2 notified""child1 notified"

Vue 1.0 官方實現

在 Vue 1.0 版本中,$dispatch 實現的原始碼放在 /src/instance/api/events.js 檔案中,程式碼很簡單:

/**
 * Recursively propagate an event up the parent chain.
 * 遞迴地在父鏈上傳播事件。
 * @param {String} event
 * @param {...*} additional arguments
 */
// $dispatch 方法是定義在 Vue 的 prototype 上的
// 接受一個字串型別的事件名稱
Vue.prototype.$dispatch = function (event) {
  // 首先執行 $emit 觸發事件,將返回值儲存在 shouldPropagate 中
  var shouldPropagate = this.$emit.apply(this, arguments)
  
  // 如果首次執行的 $emit 方法返回的值不是 true 就直接返回
  // 如果返回值不是 true 就說明元件邏輯不希望事件繼續往父元件進行傳遞
  if (!shouldPropagate) return
  
  // 如果首次執行 $emit 方法返回值是 true 就獲取當前元件的 parent 元件例項
  var parent = this.$parent
  
  // 將函式接受的引數轉換成陣列
  var args = toArray(arguments)
  
  // use object event to indicate non-source emit on parents
  // 根據傳入的事件名稱的引數組裝成 object
  args[0] = { name: event, source: this }
  
  // 迴圈知道元件的父元件
  while (parent) {
    // 在父元件中執行 $emit 觸發事件
    shouldPropagate = parent.$emit.apply(parent, args)
    
    // 如果父元件 $emit 返回的是 true 就繼續遞迴祖父元件,否則就停止迴圈
    parent = shouldPropagate ? parent.$parent : null
  }
  
  // 最後返回當前元件例項
  return this
}
複製程式碼

element-ui 實現

在 element-ui 中,$dispatch 實現的原始碼放在 /src/mixins/emitter.js 檔案中,程式碼很簡單:

// 定義 dispatch 方法,接受三個引數,分別是:元件名稱、將要觸發的事件名稱、回撥函式傳遞的引數
dispatch(componentName, eventName, params) {
  // 獲取基於當前元件的父元件例項,這裡對父元件例項和根元件例項做了相容處理
  var parent = this.$parent || this.$root;
  
  // 通過父元件的 $option 屬性獲取元件的名稱
  var name = parent.$options.componentName;

  // 當相對當前元件的父元件例項存在,而且當父元件的名稱不存在或者父元件的名稱不等於傳入的元件名稱時,執行迴圈
  while (parent && (!name || name !== componentName)) {
    // 記錄父元件的父元件
    parent = parent.$parent;

    // 當父元件的父元件存在時,獲取祖父元件的名稱
    if (parent) {
      name = parent.$options.componentName;
    }
  }
  
  // 當迴圈結束是,parent 的值就是最終匹配的元件例項
  if (parent) {
    // 當 parent 值存在時呼叫 $emit 方法
    // 傳入 parent 例項、事件名稱與 params 引數組成的陣列
    // 觸發傳入事件名稱 eventName 同名的事件
    parent.$emit.apply(parent, [eventName].concat(params));
  }
}
複製程式碼

差異分析

仔細看完實現 $dispatch 方式的兩個版本的程式碼,大家是不是發現,兩個版本的實現和功能差異性還是很大的。

  • 1、接受引數:Vue 實現版本只會接受一個字串型別的事件名稱為引數,而 element-ui 實現的版本會接受三個引數,分別是:需要觸發事件的元件名稱、將要觸發的事件名稱、回撥函式傳遞的引數;

  • 2、實現功能:Vue 實現版本觸發事件一直會順著元件鏈向上進行傳遞,知道父元件中的偵聽器沒有返回 true,在這個期間所有的元件都會執行事件的響應,包括當前元件本身,而 element-ui 實現版本會不斷的基於當前元件向父元件進行遍歷,直至找到和接受的元件名稱匹配,就會停止遍歷,觸發匹配元件中的監聽事件。

10 $broadcast 詳解

上面詳細的說完 $dispatch 方法的實現和 Vue 實現版本與 element-ui 實現版本的區別,下面就該說說 $broadcast,畢竟他們是情侶屬性嘛。

概念

Broadcast an event that propagates downward to all descendants of the current instance. Since the descendants expand into multiple sub-trees, the event propagation will follow many different “paths”. The propagation for each path will stop when a listener callback is fired along that path, unless the callback returns true.

broadcast 是一個事件,它向下傳播到當前例項的所有後代。由於後代擴充套件為多個子樹,事件傳播將會遵循許多不同的“路徑”。 除非回撥返回 true,否則在沿該路徑觸發偵聽器回撥時,每個路徑的傳播將會停止。

引數

broadcast 會接收兩中引數:event 是事件名稱,[...args] 是觸發事件時傳遞給回撥函式的引數。

例子

// 建立 parent 元件例項
var parent = new Vue()

// 建立 child1 元件例項,其父元件指向 parent
var child1 = new Vue({ parent: parent })

// 建立 child2 元件例項,其父元件指向 parent
var child2 = new Vue({ parent: parent })

// 建立 child3 元件例項,其父元件指向 child2
var child3 = new Vue({ parent: child2 })

// 在 child1 元件監聽名為 test 的事件,並繫結了一個回撥函式
child1.$on('test', function () {
  console.log('child1 notified')
})

// 在 child2 元件監聽名為 test 的事件,並繫結了一個回撥函式
child2.$on('test', function () {
  console.log('child2 notified')
})

// 在 child3 元件監聽名為 test 的事件,並繫結了一個回撥函式
child3.$on('test', function () {
  console.log('child3 notified')
})
複製程式碼

parentchild1child2child3 四個元件之間的關係可以展示成如下的關係圖:

高階元件 (5).png

parent.$broadcast('test')
// -> "child1 notified"
// -> "child2 notified"
複製程式碼

當執行 parent.$broadcast('test'); 時,事件流會以 parent 元件為起點向 parent 的子元件進行傳遞,根據事件繫結的順序,雖然 parent 元件有兩個同級的 child1child2 ,但是事件流會先觸發 child1 裡面的繫結事件,此時會輸出 "child1 notified",然後事件流到達 child2 元件,會觸發 child2 元件中的繫結事件,輸出 "child2 notified"。到這時,child2 元件中的偵聽器並沒有返回 true,所以事件傳遞到此就結束了,最終的輸出結果就只有 "child1 notified""child2 notified"

Vue 1.0 官方實現

Vue 1.0 版本中,$broadcast 實現的原始碼放在 /src/instance/api/events.js 檔案中,程式碼很簡單:

/**
 * Recursively broadcast an event to all children instances.
 * 遞迴地向所有子例項廣播事件。
 * @param {String|Object} event
 * @param {...*} additional arguments
 */
// $dispatch 方法是定義在 Vue 的 prototype 上的
// 接受一個事件
Vue.prototype.$broadcast = function (event) {
  // 獲取傳入事件的型別,判斷是否為字串
  var isSource = typeof event === 'string'
  
  // 校正 event 的值,當接受 event 的型別為字串時就直接使用,如果不是字串就使用 event 上的 name 屬性 
  event = isSource ? event : event.name
  
  // if no child has registered for this event,
  // then there's no need to broadcast.
  // 如果當前元件的子元件沒有註冊該事件,就直接返回,並不用 broadcast
  if (!this._eventsCount[event]) return
  
  // 獲取當前元件的子元件
  var children = this.$children
  
  // 將函式接受的引數轉換成陣列
  var args = toArray(arguments)
  
  // 如果傳入事件為字串
  if (isSource) {
    // use object event to indicate non-source emit
    // on children
    // 根據傳入的事件名稱的引數組裝成 object
    args[0] = { name: event, source: this }
  }
  
  // 迴圈子元件
  for (var i = 0, l = children.length; i < l; i++) {
    var child = children[i]
    
    // 在每個子元件中呼叫 $emit 觸發事件
    var shouldPropagate = child.$emit.apply(child, args)
    
    // 判斷呼叫 $emit 返回的值是否為 true
    if (shouldPropagate) {
      // 如果呼叫 $emit 返回的值為 true,就遞迴孫子元件繼續廣播
      child.$broadcast.apply(child, args)
    }
  }
  
  // 最後返回當前元件的例項
  return this
}
複製程式碼

element-ui 實現

element-ui 中,$broadcast 實現的原始碼放在 /src/mixins/emitter.js 檔案中,程式碼很簡單:

// 定義 broadcast 方法,接受三個引數,分別是:元件名稱、將要觸發的事件名稱、回撥函式傳遞的引數
function broadcast(componentName, eventName, params) {
  // 依次迴圈當前元件的子元件
  this.$children.forEach(child => {
    // 獲取每個子元件的名字
    var name = child.$options.componentName;

    // 判斷子元件的名字是否等於傳入的元件名稱
    if (name === componentName) {
      // 如果子元件的名字等於傳入的元件名稱就呼叫 $emit 觸發事件
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      // 如果子元件的名字不等於傳入的元件名稱就遞迴遍歷呼叫 broadcast 孫子元件
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
複製程式碼

差異分析

和之前說到的 $dispatch 一樣,這裡的 $broadcast 的兩個實現版本也存在著巨大的差異:

  • 1、接受引數:Vue 實現版本只會接受一個字串型別的事件名稱為引數,而 element-ui 實現的版本會接受三個引數,分別是:需要觸發事件的元件名稱、將要觸發的事件名稱、回撥函式傳遞的引數;

  • 2、實現功能:Vue 實現的 $broadcast 觸發方式是預設只觸發子代元件,不觸發孫子代元件,如果子代建立了監聽且返回了true,才會向孫子代元件傳遞事件。而 element-ui 實現的版本是直接向所有子孫後代元件傳遞,直至獲取到的子元件名稱等於傳入的元件名稱相等,才會觸發當前子元件的監聽事件,期間也沒有返回值的判定。

11 總結

說到這裡,$dispatch$broadcast 的講解就結束了。可能大家已經知道了 Vue 2.0 版本為什麼會將這兩個屬性移除。首先我們引入官網的說法:

因為基於元件樹結構的事件流方式實在是讓人難以理解,並且在元件結構擴充套件的過程中會變得越來越脆弱。這種事件方式確實不太好,我們也不希望在以後讓開發者們太痛苦。並且 $dispatch$broadcast 也沒有解決兄弟元件間的通訊問題。

這樣來說 $dispatch$broadcast 確實會有這樣的問題。在前面的講解中,大家也不難發現 $dispatch 主要是事件流由當前元件往父元件流動,當滿足一定條件的時候就會觸發當前子元件的監聽事件,$broadcast 的功能是事件流由當前元件向子元件流動,當滿足一定條件的時候就會觸發當前子元件的監聽事件。也就是說 $dispatch$broadcast 主要解決了父子元件、巢狀父子元件的通訊,並沒有解決兄弟元件的通訊問題,另一個方面這樣的事件流動的方式是基於元件樹結構的,當業務越來越煩雜時,這種方式會顯得極其繁瑣,甚至會混亂到難以維護,所以 Vue 2.0 版本移除這兩個 API 是在意料之中的。

但是為什麼三方 UI 庫都會封裝類似的這樣一個元件通訊的方式呢?我的猜測可能是為了解決在父子層巢狀元件中,通過 $dispatch$broadcast 定向的向某個父或者子元件遠端呼叫事件,這樣就避免了通過傳 props 或者使用 refs 呼叫元件例項方法的操作。這樣說的話,$dispatch$broadcast 也就其存在的價值,而並不是一無是處的,還是那句話:技術沒有好與壞,只有合適不合適

相關文章