Vue $dispatch 和 $broadcast 詳解

00 前言

$dispatch$broadcast 做爲一對情侶 💑屬性,在 Vue 1.0 中主要用來實現基於組件樹結構的事件流通訊 —— 經過向上或向下以冒泡的形式傳遞事件流,以實現嵌套父子組件的通訊。可是因爲其顯功能缺陷,在 Vue 2.0 中就被移除了。雖然 Vue 官網已經再也不支持使用 $dispatch$broadcast 進行組件通訊,可是在不少基於 Vue 的 UI 框架中都有對其的封裝,包括 element-uiiview 等等。javascript

那麼 $dispatch$broadcast 究竟是怎麼工做,其底層又是怎麼實現的呢?接下來,咱們就詳細的說一說! html

01 $dispatch 詳解

爲了追根溯源,咱們仍是先去 Vue 1.0 的文檔你觀摩一下其概念吧!vue

概念:java

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.git

上面的一段英文定義來自 Vue 1.0 官方文檔,其大體的意思是說:dispatch 是一個事件,首先會在本身實例自己上觸發,而後沿父鏈向上傳播。當它觸發父組件上的事件偵聽器時傳播即會中止,除非該偵聽器返回 true。 任何其餘參數都將傳遞給偵聽器的回調函數。github

參數:element-ui

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

**例子:數組

// 建立一個 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 三個組件之間的關係能夠展現成以下的關係圖:
app

高階組件 (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 方式的兩個版本的代碼,你們是否是發現,兩個版本的實現和功能差別性仍是很大的。

  • 一、接受參數:Vue 實現版本只會接受一個字符串類型的事件名稱爲參數,而 element-ui 實現的版本會接受三個參數,分別是:須要觸發事件的組件名稱、將要觸發的事件名稱、回調函數傳遞的參數;

  • 二、實現功能: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 的兩個實現版本也存在着巨大的差別:

  • 一、接受參數:Vue 實現版本只會接受一個字符串類型的事件名稱爲參數,而 element-ui 實現的版本會接受三個參數,分別是:須要觸發事件的組件名稱、將要觸發的事件名稱、回調函數傳遞的參數;

  • 二、實現功能: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 也就其存在的價值,而並非一無可取的,仍是那句話:技術沒有好與壞,只有合適不合適

相關文章
相關標籤/搜索