Обработка крайних случаев

Подразумевается, что вы уже изучили и разобрались с разделом Основы компонентов. Если нет — прочитайте его сначала.

Все возможности, описанные на этой странице, описывают способы обработки крайних случаев, что означает необычные ситуации, которые иногда требуют исключений в правилах Vue. Однако обратите внимание, что все они имеют недостатки или ситуации, когда они могут быть опасными. Они отмечены в каждом случае, поэтому помните о них, когда решаете использовать каждую из возможностей.

Доступ к элементу и компоненту

В большинстве случаев лучше избегать связи с другими экземплярами компонентов или вручную манипулировать элементами DOM. Однако есть случаи, когда это может быть уместно.

Доступ к корневому экземпляру

В каждом дочернем компоненте экземпляра new Vue, к этому корневому экземпляру можно получить доступ через свойство $root. Например, для этого корневого экземпляра:

// Корневой экземпляр Vue
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})

Теперь все дочерние компоненты смогут получить доступ к этому экземпляру и использовать его в качестве глобального хранилища:

// Получение данных из корневого экземпляра
this.$root.foo

// Установка данных в корневом экземпляре
this.$root.foo = 2

// Использование вычисляемых свойств корневого экземпляра
this.$root.bar

// Вызов методов корневого экземпляра
this.$root.baz()

Это может быть удобно для демонстраций или очень маленьких приложений с несколькими компонентами. Однако этот паттерн плохо масштабируется для средних или крупных приложений, поэтому мы настоятельно рекомендуем использовать Vuex для управления состоянием в большинстве случаев.

Доступ к экземпляру родительского компонента

Подобно $root, свойство $parent можно использовать для доступа к родительскому экземпляру из дочернего. Это может быть заманчивым для использования в качестве ленивой альтернативы передачи данных с помощью входных параметров.

В большинстве случаев обращение к родительскому компоненту делает ваше приложение более проблематичным в отладке и поддержке, особенно если вы изменяете данные в родителе. Когда вы вернётесь к этому компоненту позднее, будет очень сложно выяснить, откуда происходит изменение данных.

Однако есть случаи, в частности библиотек общих компонентов, когда это может быть подходящим. Например, в абстрактных компонентах, которые взаимодействуют через JavaScript с API вместо отрисовки HTML, как например эти гипотетические компоненты Google Maps:

<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

Компонент <google-map> может определять свойство map, к которому должны иметь доступ все подкомпоненты. В этом случае <google-map-markers> может получить доступ к карте с помощью this.$parent.getMap, чтобы добавить на карту набор маркеров. Вы можете увидеть этот шаблон в действии здесь.

Однако помните, что компоненты, построенные с использованием этого шаблона являются хрупкими. Например, представьте, что мы добавляем новый компонент <google-map-region> и когда в нём появляется <google-map-markers>, то он должен отображать только маркеры, попадающие в регион:

<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>

Затем внутри <google-map-markers> вы можете застать себя за созданием хаков наподобие такого:

var map = this.$parent.map || this.$parent.$parent.map

Это может быстро выйти из-под контроля. Поэтому, чтобы предоставлять контекстную информацию для компонентов-потомков на любую глубину вложенности, мы вместо этого рекомендуем инъекцию зависимостей.

Доступ к экземплярам дочерних компонентов и элементов

Несмотря на наличие входных параметров и событий, иногда вам может потребоваться прямой доступ к дочернему компоненту в JavaScript. Для этого вы можете назначить ссылочный ID дочернему компоненту с помощью атрибута ref. Например:

<base-input ref="usernameInput"></base-input>

Теперь в компоненте, где вы определили этот ref, вы можете использовать:

this.$refs.usernameInput

для доступа к экземпляру <base-input>. Это может быть полезно, если вы хотите например, программно добавить фокус на поле из родителя. В этом случае компонент <base-input> может аналогичным образом использовать ref чтобы обеспечить доступ к определённым элементам внутри него, например:

<input ref="input">

И даже определить методы для использования родителем:

methods: {
// Используется родителем для фокуса на input
focus: function () {
this.$refs.input.focus()
}
}

Таким образом мы позволим родительскому компоненту добавлять фокус на input внутри <base-input> с помощью:

this.$refs.usernameInput.focus()

Когда ref используется вместе с v-for, то ref будет массивом, содержащим дочерние компоненты, отображаемых от источника данных.

$refs заполняются только после того, как компонент был отрисован, и они не реактивны. Это подразумевается только как обходной путь для прямого манипулирования потомками — вам следует избегать доступа к $refs из шаблонов или вычисляемых свойств.

Внедрение зависимостей

Ранее, когда мы обсуждали доступ к экземпляру родительского компонента, мы показали пример:

<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>

В этом компоненте все потомки <google-map> нуждались в доступе к методу getMap, чтобы узнать с какой картой им взаимодействовать. К сожалению, использование свойства $parent плохо масштабируется для более глубоко вложенных компонентов. Вот где внедрение зависимостей может быть полезным, используя два новых свойства экземпляра: provide и inject.

Опция provide позволяет нам указать данные/методы, которые мы хотим предоставить всем компонентам-потомкам. В этом случае, это метод getMap внутри <google-map>:

provide: function () {
return {
getMap: this.getMap
}
}

Затем в любых потомках мы можем воспользоваться свойством inject для получения специальных свойств, которые мы хотели бы добавить к этому экземпляру:

inject: ['getMap']

Вы можете увидеть полный пример здесь. Преимуществом использования в отличие от $parent в том, что мы можем получить доступ к getMap в любом компоненте-потомке, без раскрытия всего экземпляра <google-map>. Это позволяет нам безопаснее продолжать разработку этого компонента, не опасаясь, что мы можем изменить/удалить что-то, на что полагается дочерний компонент. Интерфейс между этими компонентами остаётся чётко определённым, как и с props.

Фактически, мы можете думать о внедрении зависимостей как о входных параметрах “дальнего действия”, за исключением:

Тем не менее, есть недостатки внедрения зависимостей. Она связывает компоненты в вашем приложении с тем, как они в настоящее время организованы, что затрудняет рефакторинг. Свойства указанные в provide также не будут реактивными. Так и было задумано, потому что использование их для создания централизованного хранилища так же плохо, как и использование $root в тех же целях. Если свойства, которые вы хотите использовать, являются специфическими для вашего приложения, а не общего назначения, или если вы захотите обновить инъектируемые данные внутри родителей, то это хороший признак того, что вам понадобится решение для управления состоянием, например Vuex.

Подробнее об инъекции зависимостей можно прочитать на странице API.

Программное добавление прослушивателей событий

До сих пор вы видели использование $emit, и прослушивание с помощью v-on, но экземпляры Vue также предоставляют другие методы для интерфейса событий. Мы можем:

Обычно вам не придётся использовать их, но они доступны для случаев, когда вам необходимо вручную прослушивать события на экземпляре компонента. Они также могут быть полезны в качестве инструмента организации кода. Например, вы часто можете увидеть этот шаблон для интеграции сторонней библиотеки:

// Добавляем datepicker на input
// когда он будет примонтирован в DOM.
mounted: function () {
// Pikaday — сторонняя библиотека для выбора дат
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// Перед уничтожением компонента,
// также уничтожаем и datepicker.
beforeDestroy: function () {
this.picker.destroy()
}

Здесь есть две потенциальных проблемы:

Вы можете решить обе проблемы с помощью программного прослушивания события:

mounted: function () {
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}

Используя эту стратегию, мы могли бы даже использовать Pikaday с несколькими элементами ввода, причём каждый новый экземпляр автоматически очищался после себя:

mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}

Посмотрите этот fiddle для полного кода. Обратите внимание, что если вам приходится делать много установок и очисток в рамках одного компонента, то лучшим решением будет, как правило, создание более модульных компонентов. В этом случае мы рекомендуем создать переиспользуемый компонент <input-datepicker>.

Чтобы узнать больше о программных прослушивателях событий, ознакомьтесь на странице API с разделом Методы экземпляра — события.

Обратите внимание, что система событий Vue отличается от браузерного EventTarget API. Хотя они работают аналогично, $emit, $on, и $off не являются псевдонимами для dispatchEvent, addEventListener, и removeEventListener.

Циклические ссылки

Рекурсивные компоненты

Компоненты могут рекурсивно вызывать себя в своём собственном шаблоне. Однако, они могут делать это только с помощью опции name:

name: 'unique-name-of-my-component'

Когда вы регистрируете компонент глобально с помощью Vue.component, глобальный ID будет автоматически устанавливаться как параметр опции name компонента.

Vue.component('unique-name-of-my-component', {
// ...
})

Если вы не будете осторожны, рекурсивные компоненты также могут привести к бесконечным циклам:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

Компонент, указанный выше, приведёт к ошибке “max stack size exceeded”, поэтому убедитесь, что рекурсивный вызов определяется по условию (т.е. использует v-if, который в конечном итоге будет false).

Циклические ссылки между компонентами

Предположим, что вы создаёте дерево каталога файлов, как например в Finder или File Explorer. У вас может быть компонент tree-folder с таким шаблоном:

<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>

Затем компонент tree-folder-contents с этим шаблоном:

<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>

Когда вы присмотритесь, вы увидите, что эти компоненты фактически будут потомком _и_ предком в дереве отрисовки — парадокс! При регистрации компонентов глобально с помощью Vue.component этот парадокс разрешается автоматически за вас. Если это вы, можете не читать дальше.

Однако, если вы используете require/import для компонентов с помощью модульной системы, например через Webpack или Browserify, вы получите сообщение об ошибке:

Failed to mount component: template or render function not defined.

Чтобы объяснить, что здесь происходит, давайте назовём наши компоненты A и B. Система модулей видит, что ей нужен A, но сначала A нуждается в B, но B нуждается в A, но A нуждается в B, и т.д. Она застревает в цикле не зная как полностью разрешить любой компонент без предварительного разрешения другого. Чтобы исправить это, нам нужно дать модульной системе точку, в которой она может сказать “A нуждается в B иногда, но нет необходимости разрешать B сначала.”

В нашем случае давайте сделаем эту точку компонентом tree-folder. Мы знаем, что потомок, создающий парадокс, является компонентом tree-folder-contents, поэтому мы подождём, пока не будет вызван хук жизненного цикла beforeCreate для его регистрации:

beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

Или вы можете использовать асинхронный import Webpack при локальной регистрации компонента:

components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}

Проблема решена!

Альтернативные определения шаблонов

Inline-шаблоны

Если у компонента-потомка присутствует специальный атрибут inline-template, то содержимое элемента будет использовано не для распределения контента, а в качестве шаблона этого компонента. Это позволяет более гибко использовать шаблоны.

<my-component inline-template>
<div>
<p>Этот шаблон будет скомпилирован в области видимости компонента-потомка.</p>
<p>Доступа к данным родителя нет.</p>
</div>
</my-component>

Тем не менее, inline-template усложняют понимание области видимости вашего шаблона. В качестве наилучшей практики рекомендуется определять шаблоны внутри компонента с помощью опции template или в теге <template> в файле .vue.

X-Templates

Другой способ определения шаблонов — указывать их внутри тега script с типом text/x-template, а затем ссылаться на шаблон по id. Например:

<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
template: '#hello-world-template'
})

Это может быть полезным для демонстраций с большими шаблонами или в очень маленьких приложениях, но в остальных случаях их следует избегать, поскольку они отделяют шаблоны от остального определения компонента.

Контролирование обновлений

Благодаря системе реактивности Vue, она всегда знает, когда нужно выполнять обновления (если вы используете её правильно). Однако есть крайние случаи, когда вам может потребоваться принудительное обновление, несмотря на то, что никаких реактивных данных не изменилось. Также есть другие случаи, когда вы можете предотвратить ненужные обновления.

Принудительное обновление

Если вам необходимо принудительное обновление во Vue, в 99.99% случаев вы где-то совершили ошибку.

Возможно, вы не учли предостережения об обнаружении изменений в массивах или в объектах, или вы можете полагаться на состояние, которое не отслеживается системой реактивности Vue, например с помощью data.

Однако, если вы исключили вышеизложенные варианты и оказались в крайне редкой ситуации, связанной с необходимостью принудительного обновления вручную, то вы можете сделать это с помощью $forceUpdate.

“Дешёвые” статические компоненты с помощью v-once

Отрисовка простых элементов HTML во Vue происходит очень быстро, но иногда встречаются компоненты, в которых очень много статического контента. В таких случаях, вы можете убедиться что он будет выполнен один раз и затем закэширован, добавив директиву v-once на корневой элемент, например:

Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... много-много статического контента ...
</div>
`
})

Ещё раз, попробуйте не злоупотреблять этим шаблоном. Хотя это удобно в тех редких случаях, когда вам необходимо отображать много статического контента, это просто не нужно пока вы не заметите замедление при отрисовке — плюс, это может вызвать много путаницы позднее. Например, представьте себе другого разработчика, который не знаком с директивой v-once или просто пропустил её наличие в шаблоне. Могут уйти часы, пока удастся выяснить, почему шаблон не обновляется правильно.