Vue $dispatch и $broadcast в деталях

Vue.js

00 Предисловие

$dispatchа также$broadcastКак паре 💑 атрибут, вVue 1.0Он в основном используется для реализации связи с потоком события на основе структуры дерева компонентов - путем передачи потока событий в виде пузырька вверх или вниз, чтобы реализовать связь компонентов вложенных родительских детей. Однако из-за очевидных функциональных дефектов, вVue 2.0был удален. Хотя официальный сайт Vue больше не поддерживает использование$dispatchа также$broadcastКомпонентная связь, но она инкапсулирована во многих UI-фреймворках на основе Vue, в том числеelement-ui,iviewи т.п.

Так$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, Любые дополнительные аргументы будут переданы в функцию обратного вызова слушателя.

Приведенное выше английское определение происходит отVue 1.0Официальная документация, что примерно означает:диспетчеризация — это событие, которое сначала запускается в собственном экземпляре, а затем распространяется вверх по родительской цепочке. Распространение останавливается, когда он запускает прослушиватель событий в родительском компоненте, если только прослушиватель не возвращает значение 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');
});

Было сказано, что,parent,child1а такжеchild2Отношения между тремя компонентами можно показать в виде следующей диаграммы:

高阶组件 (4).png

// 在 child2 组件中通过 dispatch 触发 test 事件
child2.$dispatch('test');

// 事件执行会输出如下结果
// -> "child2 notified"
// -> "child1 notified"

при исполненииchild2.$dispatch('test');, первые триггерыchild2Функция обратного вызова тестового события, отслеживаемого в компоненте, вывод'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
}

реализация элемента пользовательского интерфейса

В элементе пользовательского интерфейса$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Ведь они пара.

концепция

Широковещательная передача события, которое распространяется вниз ко всем потомкам текущего экземпляра. Поскольку потомки расширяются в несколько поддеревьев, распространение события будет следовать многим различным «путям». Распространение для каждого пути остановится, когда по этому пути будет запущен обратный вызов прослушивателя. путь, если обратный вызов не возвращает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')
})

parent,child1,child2а такжеchild3Отношения между четырьмя компонентами можно показать на следующей диаграмме:

高阶组件 (5).png

parent.$broadcast('test')
// -> "child1 notified"
// -> "child2 notified"

при исполненииparent.$broadcast('test');, поток событий начинается сparentкомпонент в качестве отправной точкиparentдочерние компоненты доставляются в соответствии с порядком, в котором связаны события, хотяparentКомпонент имеет двух братьев и сестерchild1а такжеchild2, но поток событий срабатывает первым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середина,$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.

Но почему сторонние библиотеки пользовательского интерфейса инкапсулируют аналогичный способ взаимодействия компонентов? Мое предположение может состоять в том, чтобы разрешить вложенные компоненты уровня родитель-потомок,$dispatchа также$broadcastНаправлено на родительский или дочерний компонентудаленный вызовсобытие, которое позволяет избежать вызова методов экземпляра компонента путем передачи свойств или использования ссылок. Говоря так,$dispatchа также$broadcastЭто еще и ценность его существования, а не бесполезности того или иного предложения:Нет хороших или плохих технологий, есть подходящие и неподходящие!