От автора: очень распространенной практикой в веб-разработке является указание элемента DOM (объектная модель документа) (то есть все HTML-элементы и логическая структура, которую они представляют) и манипулирование им каким-либо образом. В этой статье мы рассмотрим возможности ref и некоторые из случаев его применения. Давайте приступим.
Рыцари Старой Вьюпублики
Для тех из нас, кто переходит от старых технологий, иначе jQuery, мы раньше часто использовали выбор элемента DOM на странице, чтобы изменить его или использовать любым способом. На самом деле это было почти неизбежно в тех случаях, когда вы хотели использовать любой тип плагина, который бы использовал элемент на странице.
В jQuery, вы бы выбрали элемент, указав на него через функцию $(), и это открывало широкий спектр методов для управления этим объектом. Возьмем пример div, для которого вы хотите переключать видимость, изменяя значение свойства CSS display. Давайте рассмотрим следующую разметку для нашего примера.
<body> <div id="datOneDiv" class="myCoolClass" style="display: none;">I is hidden</div> <div>I is shown</div> <div>I is dog</div> </body>
В jQuery, это будет выглядеть следующим образом.
$('#datOneDiv').css('display', 'block');
Несколько интересных вещей, которые стоит упомянуть. Прежде всего, заметим, что мы указываем очень специфичный div в нашем документе, тот который имеет идентификатор datOneDiv, как видно из селектора #datOneDiv (# здесь работает точно так же, как селектор CSS, это означает идентификатор).
Второе, на что следует обратить внимание, это то, что, насколько это было легко. И из-за этой легкости люди когда-то не брались за изучение собственно JavaScript, что со временем становилось проблемой.
А вы знаете JS? В реальном vanilla JavaScript тот же результат может быть достигнут с помощью querySelector и некоторых манипуляций со свойствами.
document.querySelector('#datOneDiv').style.display = 'block';
Ключевой момент, на который следует обратить внимание в этом примере, заключается в том, что мы снова используем id для указания div внутри документа. Конечно, мы могли бы также указать div через его класс .myCoolClass, но это, как вы узнаете, будет представлять ту же проблему.
Пробуждение Vue
Мы собираемся сегодня совершить убийство ситхов. Не волнуйтесь, никакие реальные рогатые классные парни при создании этой статьи не пострадали. Рассмотрим следующий компонент Vue Sith.vue.
<template> <div> <p class="sithLord">I is Sith</p> <button @click="keelItWithFire">Kill the Sith DED!</button> </div> </template> <script> export default { methods: { keelItWithFire() { document.querySelector(".sithLord").style.display = "none"; } } }; </script>
Я ЗНАЮ, Я ЗНАЮ. Я должна использовать динамические классы, этот пример плох. Однако давайте представим, что мы не знали обо всем этом совершенстве Vue и что мы действительно пытались указать элемент DOM таким образом, чтобы внести в него некоторые изменения. (Кроме шуток, если есть способ динамически применять классы или стили, вам следует ВСЕГДА делать это с помощью динамических свойств! Мы делаем это просто в качестве простого примера.)
Если мы создадим экземпляр этого компонента в App.vue или основной точке входа приложения и нажмем кнопку, вы заметите, что это действительно работает. Так почему же нам время от времени говорят, что ОЧЕНЬ ПЛОХО выбирать элемент DOM прямо во Vue, как мы делаем здесь?
Попробуйте изменить основной шаблон (или где вы тестируете эти компоненты), чтобы фактически сразиться с двумя или более владыками ситхов, вот так.
<template> <div id="app"> <Sith/> <hr> <Sith/> <hr> </div> </template>
Теперь нажмите на второго ситха, чтобы убить его. Хех. Силы не достаточно. Вы знаете, что случилось? Когда запускается метод компонента keelItWithFire для второго компонента метод, querySelector проходит DOM и пытается найти первый экземпляр элемента с классом sithLord, и, конечно же, он его находит!
Основная проблема с выбором в DOM непосредственно во Vue заключается, прежде всего, в том, что компоненты предназначены для многократного использования и динамичны, поэтому мы не можем гарантировать, что класс здесь будет уникален.
Ну, мы можем использовать id, как вы видели! И частично вы правы: назначение атрибута id шаблону во Vue будет в некотором роде гарантировать его уникальность, доказано, что вы не создаете экземпляры более чем одного из этих компонентов во всем приложении (иначе вы в основном столкнетесь с той же проблемой, что и выше).
Второе предостережение заключается в том, что вам также нужно будет гарантировать, что никакая другая вещь в приложении, никакой другой разработчик и никакая другая библиотека не собираются создавать элемент, который потенциально может содержать тот же самый id.
Путь Вьюдая
Использовать Vue или нет, тут и гадать нечего. Во Vue у нас есть множество инструментов для динамического изменения шаблона с помощью вычисляемых свойств, локального состояния, динамических привязок и многого другого. Но придет время, когда вы столкнетесь с необходимостью указать элемент в DOM. В качестве пары распространенных причин можно назвать реализацию внешнего подключаемого модуля, который не является специфичным для Vue, или для указания поля в форме и его выделения фокусом.
Когда возникает такой случай, у нас есть довольно крутой атрибут, который мы можем присвоить элементам — ref. Вы можете ознакомиться с официальной документацией здесь.
Мы собираемся создать новый компонент, на этот раз Jedi.vue, и на этот раз мы будем делать так, как должно быть во Vue.
<template> <div> <p ref="jedi">I is Jedi</p> <button @click="keelItWithFire">Kill the Jedi DED!</button> </div> </template> <script> export default { methods: { keelItWithFire() { this.$refs.jedi.style.display = "none"; } } }; </script>
Теперь важно понять, что происходит, когда мы добавляем атрибут ref к одному из элементов <template>. Проще говоря, каждый экземпляр компонента теперь будет содержать закрутую ссылку, указывающую на собственный тег <p>, который мы можем указать, как видно в функции keelItWithFire через свойство экземпляра $refs.
Помимо проблем, возникающих с указанием классов и идентификаторов, крайне важно знать, что самая основная проблема заключается в том, что непосредственное изменение DOM может привести к тому, что эти изменения будут перезаписаны Vue при цикле повторного рендеринга DOM. Либо для этого компонента, либо для его родительского элемента.
Поскольку, когда мы выбираем элемент DOM напрямую, Vue не знает об этом, он не будет обновлять виртуальную «копию», которую сохранил — и когда ему придется выполнять перестроение, все эти изменения будут полностью потеряны.
Если вы не хотите, чтобы определенный фрагмент DOM постоянно перерисовывался Vue, вы можете применить к нему атрибут v-once — таким образом, Vue не будет пытаться перерисовать эту конкретную часть.
Пока этот пример не кажется супер захватывающим, но прежде чем мы перейдем к реальному сценарию, я хочу упомянуть о некоторых предостережениях.
Предостережение 1
Если вы используете ref поверх компонента Vue, такого как <Jedi ref=»jedi»>, тогда то, что вы получите из this.$refs.jedi, будет экземпляром компонента, а не элементом, как у нас здесь в случае с тегом <p>. Это означает, что у вас будет доступ ко всем свойствам и методам класса Vue, но вам также потребуется доступ к корневому элементу этого компонента через $el, если вам необходимо внести прямые изменения DOM.
Предостережение 2
$ref регистрируются после выполнения функции render компонента. Это означает, что вы НЕ сможете использовать хуки $ref, которые запускаются до вызова render, например on created(); у вас, однако, он будет доступен для mounted().
Есть способ подождать, пока будет выполнено created(), чтобы элементы стали доступными, и использовать функцию this.$nextTick.
this.$nextTick откладывает выполнение переданной вами функции до следующего обновления DOM от Vue. Рассмотрим пример.
<template> <div> <p ref="myRef">No</p> </div> </template> <script> export default { created() { if (!this.$refs.myRef) { console.log("This doesn't exist yet!"); } this.$nextTick(() => { if (this.$refs.myRef) { console.log("Now it does!"); } }); }, mounted() { this.$refs.myRef.innerHTML = "avocado"; console.log("Now its mounted"); } }; </script>
У нас есть тег <p> с ref из myRef, ничего необычного. Но в хуке created()происходит пара вещей.
Во-первых, мы проверяем, доступен ли нам this.$refs.myRef, и, как и ожидалось, этого не произойдет, потому что DOM на этом этапе еще не визуализирован — поэтому будет выполнен console.log.
После этого мы устанавливаем для вызова анонимную функцию $nextTick, которая будет выполняться после следующего цикла обновления DOM. Всякий раз, когда это происходит, мы регистрируем в консоли “Now it does!”.
В хуке mounted() мы на самом деле используем ref, чтобы изменить внутренний текст тега <p> на что-то более стоящее. Имейте в виду, что вы действительно получите console.log в следующем порядке:
Теперь он установлен
Теперь это делает!
mounted() на самом деле будет срабатывать раньше nextTick, потому что nextTick запускается в конце цикла рендеринга.
Темная сторона
Ну, теперь, когда у вас есть вся потрясающая теория, что мы можем на самом деле с этим сделать? Давайте рассмотрим общий пример, включающий стороннюю библиотеку flatpickr, в один из наших компонентов. Вы можете прочитать больше о flatpickr здесь. Рассмотрим следующий компонент.
<template> <input ref="datepicker" /> </template> <script> import flatpickr from 'flatpickr'; import 'flatpickr/dist/themes/airbnb.css'; export default { mounted () { const self = this; flatpickr(this.$refs.datepicker, { mode: 'single', dateFormat: 'YYYY-MM-DD HH:mm' }); } }; </script>
Сначала мы импортируем библиотеку и некоторые стили, но затем пакет требует, чтобы мы указали определенный элемент в DOM, к которому нужно ее присоединить. Мы используем здесь ref, чтобы указать библиотеке правильный элемент с помощью this.$refs.datepicker. Эта техника будет работать даже для плагинов jQuery.
Но остерегайтесь темной стороны. Angerlar, JFear, Reactgression; темная сторона Силы — они. (Отказ от ответственности, это шутка. На самом деле мне не нравятся другие фреймворки. За исключением, может быть, jQuery. JQuery — это зло.)
Завершение
Надеюсь, вам было интересно. ref — это неправильно понятый и недостаточно используемый инструмент, который поможет избежать неприятностей при использовании в нужный момент!
Примеры кода, использованные в этой статье, можно найти на Codesandbox по следующей ссылке: https://codesandbox.io/s/target-dom-in-vue-r9imj.
Как всегда, спасибо за чтение.
Автор: Marina Mosti
Источник: https://www.telerik.com
Редакция: Команда webformyself.