Привет!
Вы любите собеседования? И часто проводите их? Если ответ на второй вопрос «Да», то среди кандидатов вам наверняка встречались отличные и умные люди, которые отвечали на все ваши вопросы и приближались к концу зарплатной вилки.
Но вы, конечно, не хотите платить профессионалам слишком много. И жизненно необходимо казаться умнее них, пускай только на время собеседования.
Если у вас с этим проблемы, то добро пожаловать по кат. Там вы найдете самые каверзные и извращенные вопросы по Vue, которые поставят любого кандидата на место и заставят сомневаться в своих профессиональных навыках.
1. Триггер watcher'ов внутри хуков жизненного цикла
Этот вопрос может показаться легким, но я гарантирую, на него не ответит ни один, даже самый прошареный разработчик. Можете задать его в начале собеседования, чтобы кандидат сразу почувствовал ваше превосходство.
Вопрос:
Есть компонент TestComponent, у которого есть переменная amount. Внутри основных хуков жизненного цикла мы задаем ей значение в числовом порядке от 1 до 6. На эту переменную стоит watcher, который выводит ее значение в консоль.
Мы создаем инстанс TestComponent и через несколько секунд удаляем. Необходимо сказать, что мы увидим в выводе консоли.
Код:
/* TestComponent.vue */
<template>
<span>
I'm Test component
</span>
</template>
<script>
export default {
data() {
return {
amount: 0,
};
},
watch: {
amount(newVal) {
console.log(newVal);
},
},
beforeCreate() { this.amount = 1; },
created() { this.amount = 2; },
beforeMount() { this.amount = 3; },
mounted() { this.amount = 4; },
beforeDestroy() { this.amount = 5; },
destroyed() { this.amount = 6; },
};
</script>
Дам подсказку: «2345» — неправильный ответ.
Watcher срабатывает на изменения в хуке created, beforeMount и mounted. Так как все эти хуки вызываются во время одного тика, Vue вызовет watcher один раз в самом конце, со значением 4.
Vue отпишется от наблюдения за изменением переменной перед вызовом хуков beforeDestroy и destroyed, поэтому 5 и 6 не попадут в консоль.
Песочница с примером, чтобы убедиться в ответе
2. Неявное поведение props
Этот вопрос основан на редком поведении props во Vue. Все программисты, конечно, просто выставляют нужные валидации для prop'ов и никогда не сталкиваются с таким поведением. Но этого говорить кандидату не нужно. Лучше будет задать этот вопрос, бросить на него осуждающий взгляд после неправильного ответа и перейти к следующему.
Вопрос:
Чем поведение prop'а с типом Boolean отличается от остальных?
/* SomeComponent.vue */
<template>
/* ... */
</template>
<script>
export default {
/* ... */
props: {
testProperty: {
type: Boolean,
},
},
};
</script>
Если в качестве параметра будет передана пустая строка или название самого prop'а в kebab-case, то Vue преобразует это в true.
Пример:
У нас есть файл с Boolean prop'ом:
/* TestComponent.vue */
<template>
<div v-if="canShow">
I'm TestComponent
</div>
</template>
<script>
export default {
props: {
canShow: {
type: Boolean,
required: true,
},
},
};
</script>
Ниже показаны все валидные варианты использования компонента TestComponent.
/* TestWrapper.vue */
<template>
<div>
<!-- В этом случае canShow будет равен true внутри TestComponent -->
<TestComponent canShow="" />
<!-- Этот пример аналогичен предыдущему, vue-template-compiler выставит пустую строку для нашего prop'а -->
<TestComponent canShow />
<!-- Тут canShow тоже равен true -->
<TestComponent canShow="can-show" />
</div>
</template>
<script>
import TestComponent from 'path/to/TestComponent';
export default {
components: {
TestComponent,
},
};
</script>
Песочница с примером, чтобы убедиться в ответе
3. Использование массива в $refs
Если ваш кандидат знает как работает фреймворк изнутри на уровне Эвана Ю, у вас все еще есть несколько козырей в рукаве: вы можете задать вопрос о незадокументированном и неочевидном поведении фреймворка.
Вопрос:
Во Vuex лежит массив объектов files, у каждого из объектов в массиве есть уникальные свойства name и id. Этот массив раз в несколько секунд обновляется, в нем удаляются и добавляются элементы.
У нас есть компонент, который выводит name каждого объекта массива с кнопкой, по клику на которую в консоль должен выводиться dom-элемент, связанный с текущим файлом:
/* FileList.vue */
<template>
<div>
<div
v-for="(file, idx) in files"
:key="file.id"
ref="files"
>
{{ file.name }}
<button @click="logDOMElement(idx)">
Log DOM element
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState('files'),
},
methods: {
logDOMElement(idx) {
console.log(this.$refs.files[idx]);
},
},
};
</script>
Необходимо сказать, где здесь потенциальная ошибка и как ее исправить.
Такое происходит только тогда, когда в массиве часто изменяются данные.
Методы решения написаны в issue на GitHub'е:
1. Создавать уникальный ref для каждого элемента
<template>
<div>
<div
v-for="(file, idx) in files"
:key="file.id"
:ref="`file_${idx}`"
>
{{ file.name }}
<button @click="logDOMElement(idx)">
Log DOM element
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState('files'),
},
methods: {
logDOMElement(idx) {
console.log(this.$refs[`file_{idx}`]);
},
},
};
</script>
2. Дополнительный аттрибут
<template>
<div>
<div
v-for="(file, idx) in files"
:key="file.id"
:data-file-idx="idx"
>
{{ file.name }}
<button @click="logDOMElement(idx)">
Log DOM element
</button>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState('files'),
},
methods: {
logDOMElement(idx) {
const fileEl = this.$el.querySelector(`*[data-file-idx=${idx}]`);
console.log(fileEl);
},
},
};
</script>
4. Странное пересоздание компонента
Вопрос:
У нас есть специальный компонент, который пишет в консоль каждый раз, когда вызывается хук mounted:
/* TestMount.vue */
<template>
<div>
I'm TestMount
</div>
</template>
<script>
export default {
mounted() {
console.log('TestMount mounted');
},
};
</script>
Этот компонент используется в компоненте TestComponent. Он имеет кнопку, по нажатию на которую на 1 секунду покажется надпись Top message.
/* TestComponent.vue */
<template>
<div>
<div v-if="canShowTopMessage">
Top message
</div>
<div>
<TestMount />
</div>
<button
@click="showTopMessage()"
v-if="!canShowTopMessage"
>
Show top message
</button>
</div>
</template>
<script>
import TestMount from './TestMount';
export default {
components: {
TestMount,
},
data() {
return {
canShowTopMessage: false,
};
},
methods: {
showTopMessage() {
this.canShowTopMessage = true;
setTimeout(() => {
this.canShowTopMessage = false;
}, 1000);
},
},
};
</script>
Кликнем на кнопку и посмотрим, что будет в консоли:
Первый маунт был ожидаемый, но откуда еще два? Как это исправить?
Песочница с примером, чтобы понять ошибку и исправить ее
В самом начале наш Virtual DOM выглядит так:
После клика на кнопку он выглядит так:
Vue пытается сопоставить старый Virtual DOM с новым, чтобы понять, что нужно удалить и добавить:
Удаленные элементы перечеркнуты красным, созданные — выделены зеленым
Vue не смог найти компонент TestMount, поэтому пересоздал его.
Аналогичная ситуация повторится через секунду после нажатия кнопки. В этот момент компонент TestMounted третий раз выведет на консоль информацию о своем создании.
Чтобы пофиксить проблему, достаточно поставить атрибут key к div'у с компонентом TestMounted:
/* TestComponent.vue */
<template>
<div>
<!-- ... -->
<div key="container">
<TestMount />
</div>
<!-- ... -->
</div>
</template>
/* ... */
Теперь Vue сможет однозначно сопоставить нужные элементы Virtual DOM'ов.
5. Создание компонента-таблицы
Задача:
Необходимо создать компонент, который принимает массив с данными и выводит их в виде таблицы. Необходимо давать возможность задавать колонки и вид ячейки.
Информация о колонках и виде ячейки должна передаваться через специальный компонент (так же, как и у element-ui):
/* SomeComponent.vue */
<template>
<CustomTable :items="items">
<CustomColumn label="Name">
<template slot-scope="item">
{{ item.name }}
</template>
</CustomColumn>
<CustomColumn label="Element Id">
<template slot-scope="item">
{{ item.id }}
</template>
</CustomColumn>
</CustomTable>
</template>
В начале задача не содержала необходимости делать так же, как и у element-ui. Но оказалось, что некоторые люди способны выполнить задачу в первоначальной формулировке. Поэтому и добавилось требование передавать информацию о колонках и виде ячейки с помощью компонентов.
Уверен, ваши собеседуемые будут все время в ступоре. Можете дать им 30 минут на решение такой задачи.
Ниже дан пример реализации. Он не учитывает некоторых моментов (как, например, изменение label), но основной принцип должен быть понятен.
/* CustomColumn.js */
export default {
render() {
return null;
},
props: {
label: {
type: String,
required: true,
},
},
mounted() {
// Передаем в компонент CustomTable необходимые данные
this.$parent.setColumnData({
label: this.label,
createCell: this.$scopedSlots.default,
});
},
};
/* CustomTable.js */
/* Использется JSX, так как в template не получится использовать метод createCell, переданный из CustomColumn.js */
export default {
render() {
const { columnsData, items } = this;
const { default: defaultSlot } = this.$slots;
return (
<div>
// Создаем элементы CustomColumn
{defaultSlot}
<table>
// Создаем хедер
<tr>
{columnsData.map(columnData => (
<td key={columnData.label}>
{columnData.label}
</td>
))}
</tr>
// Создаем строки таблицы
{items.map(item => (
<tr>
{columnsData.map(columnData => (
<td key={columnData.label}>
{columnData.createCell(item)}
</td>
))}
</tr>
))}
</table>
</div>
);
},
props: {
items: {
type: Array,
required: true,
},
},
data() {
return {
columnsData: [],
};
},
methods: {
setColumnData(columnData) {
this.columnsData.push(columnData);
},
},
};
6. Создание портала
Если ваш кандидат не справился с предыдущим заданием, ничего страшного: можете дать ему еще одно, не менее сложное!
Задача:
Создать компонент Portal и PortalTarget, как у библиотеки portal-vue:
/* FirstComponent.vue */
<template>
<div>
<Portal to="title">
Super header
</Portal>
</div>
</template>
/* SecondComponent.vue */
<template>
<div>
<PortalTarget name="title" />
</div>
</template>
- Хранилище данных о порталах
- Компонент Portal, который добавляет данные в хранилище
- Компонент PortalTarget, который извлекает данные из хранилища и отображает их
/* dataBus.js */
/* Файл содержит реактивное хранилище данных */
import Vue from 'vue';
const bus = new Vue({
data() {
return {
portalDatas: [],
};
},
methods: {
setPortalData(portalData) {
const { portalDatas } = this;
const portalDataIdx = portalDatas.findIndex(
pd => pd.id === portalData.id,
);
if (portalDataIdx === -1) {
portalDatas.push(portalData);
return;
}
portalDatas.splice(portalDataIdx, 1, portalData);
},
removePortalData(portalDataId) {
const { portalDatas } = this;
const portalDataIdx = portalDatas.findIndex(
pd => pd.id === portalDataId,
);
if (portalDataIdx === -1) {
return;
}
portalDatas.splice(portalDataIdx, 1);
},
getPortalData(portalName) {
const { portalDatas } = this;
const portalData = portalDatas.find(pd => pd.to === portalName);
return portalData || null;
},
},
});
export default bus;
/* Portal.vue */
/* Этот компонент передает данные в dataBus */
import dataBus from './dataBus';
let currentId = 0;
export default {
props: {
to: {
type: String,
required: true,
},
},
computed: {
// Уникальный id компонента.
// Нужен для идентификации данных в dataBus
id() {
return currentId++;
},
},
render() {
return null;
},
created() {
this.setPortalData();
},
// Подхватываем изменение слотов
updated() {
this.setPortalData();
},
methods: {
setPortalData() {
const { to, id } = this;
const { default: portalEl } = this.$slots;
dataBus.setPortalData({
to,
id,
portalEl,
});
},
},
beforeDestroy() {
dataBus.removePortalData(this.id);
},
};
/* PortalTarget.vue */
/* Компонент извлекает и отображает данные */
import dataBus from './dataBus';
export default {
props: {
name: {
type: String,
required: true,
},
},
render() {
const { portalData } = this;
if (!portalData) {
return null;
}
return (
<div class="portal-target">
{portalData.portalEl}
</div>
);
},
computed: {
portalData() {
return dataBus.getPortalData(this.name);
},
},
};
Данное решение не поддерживает изменение атрибута to, не поддерживает анимации через transition и не имеет поддержки дефолтных значений, как portal-vue. Но общая идея должна быть понятна.
7. Предотвращение создания реактивности
Вопрос:
Вы получили от апи большой объект и отобразили его пользователю. Примерно так:
/* ItemView.vue */
<template>
<div v-if="item">
<div> {{ item.name }} </div>
<div> {{ item.price }} </div>
<div> {{ item.quality }} </div>
<!-- И еще много полей -->
</div>
</template>
<script>
import getItemFromApi from 'path/to/getItemFromApi';
export default {
data() {
return {
item: null,
};
},
async mounted() {
this.item = await getItemFromApi();
},
};
</script>
В этом коде есть проблема. У объекта item мы не меняем name, price, quality и остальные свойства. Но Vue об этом не знает и добавляет реактивность в каждое поле.
Как можно этого избежать?
Vue проверит, заморожен ли объект с помощью метода Object.isFrozen. И если это так, то Vue не станет добавлять реактивные геттеры и сеттеры к свойствам объекта, так как их в любом случае невозможно изменить. При очень больших объектах эта оптимизация помогает сохранить до нескольких десятков миллисекунд.
Оптимизированный компонент будет выглядеть так:
/* ItemView.vue */
<template>
<!-- ... -->
</template>
<script>
import getItemFromApi from 'path/to/getItemFromApi';
export default {
/* .... */
async mounted() {
const item = await getItemFromApi();
Object.freeze(item);
this.item = item;
},
};
</script>
Object.freeze замораживает только свойства самого объекта. Так что, если объект содержит в себе вложенные объекты, их тоже необходимо заморозить.
8. Ошибки медленных девайсов
Вопрос:
Есть компонент с методом, который выводит одно из свойств объекта item в консоль, а затем удаляет объект item:
/* SomeComponent.vue */
<template>
<div v-if="item">
<button @click="logAndClean()">
Log and clean
</button>
</div>
</template>
<script>
export default {
data() {
return {
item: {
value: 124,
},
};
},
methods: {
logAndClean() {
console.log(this.item.value);
this.item = null;
},
},
};
</script>
Что здесь может пойти не так?
Я постоянно вижу такую проблему в трекере ошибок, особенно часто на дешевых мобильниках за 4-5к рублей.
Чтобы избежать ее, просто добавьте проверку на существование item в начале функции:
<template>
<!-- ... -->
</template>
<script>
export default {
/* ... */
methods: {
logAndClean() {
const { item } = this;
if (!item) {
return;
}
console.log(item.value);
this.item = null;
},
},
};
</script>
Чтобы воспроизвести баг, можете перейти в песочницу с примером, выставить максимальный троттлинг CPU и быстро-быстро покликать на кнопку. У меня, например, получилось.
Ссылка на песочницу, чтобы убедиться в ответе
Спасибо, что дочитали статью до конца! Думаю, теперь вы точно сможете казаться умнее на собеседованиях и у ваших кандидатов сильно упадут зарплатные ожидания!
Автор: AndreasCag