Привет, Хабр!
Вы любите собеседования? И часто проводите их? Если ответ на второй вопрос «Да», то среди кандидатов вам наверняка встречались отличные и умные люди, которые отвечали на все ваши вопросы и приближались к концу зарплатной вилки.
Но вы, конечно, не хотите платить профессионалам слишком много. И жизненно необходимо казаться умнее них, пускай только на время собеседования.
Если у вас с этим проблемы, то добро пожаловать под кат. Там вы найдете самые каверзные и извращенные вопросы по Vue, которые поставят любого кандидата на место и заставят сомневаться в своих профессиональных навыках.
Этот вопрос может показаться легким, но я гарантирую, на него не ответит ни один, даже самый прошареный разработчик. Можете задать его в начале собеседования, чтобы кандидат сразу почувствовал ваше превосходство.
Вопрос:
Есть компонент TestComponent, у которого есть переменная amount. Внутри основных хуков жизненного цикла мы задаем ей значение в числовом порядке от 1 до 6. На эту переменную стоит watcher, который выводит ее значение в консоль.
Мы создаем инстанс TestComponent и через несколько секунд удаляем. Необходимо сказать, что мы увидим в выводе консоли.
Код:
Дам подсказку: «2345» — неправильный ответ.
Песочница с примером, чтобы убедиться в ответе
Этот вопрос основан на редком поведении props во Vue. Все программисты, конечно, просто выставляют нужные валидации для prop'ов и никогда не сталкиваются с таким поведением. Но этого говорить кандидату не нужно. Лучше будет задать этот вопрос, бросить на него осуждающий взгляд после неправильного ответа и перейти к следующему.
Вопрос:
Чем поведение prop'а с типом Boolean отличается от остальных?
Песочница с примером, чтобы убедиться в ответе
Если ваш кандидат знает как работает фреймворк изнутри на уровне Эвана Ю, у вас все еще есть несколько козырей в рукаве: вы можете задать вопрос о незадокументированном и неочевидном поведении фреймворка.
Вопрос:
Во Vuex лежит массив объектов files, у каждого из объектов в массиве есть уникальные свойства name и id. Этот массив раз в несколько секунд обновляется, в нем удаляются и добавляются элементы.
У нас есть компонент, который выводит name каждого объекта массива с кнопкой, по клику на которую в консоль должен выводиться dom-элемент, связанный с текущим файлом:
Необходимо сказать, где здесь потенциальная ошибка и как ее исправить.
Вопрос:
У нас есть специальный компонент, который пишет в консоль каждый раз, когда вызывается хук mounted:
Этот компонент используется в компоненте TestComponent. Он имеет кнопку, по нажатию на которую на 1 секунду покажется надпись Top message.
Кликнем на кнопку и посмотрим, что будет в консоли:
Первый маунт был ожидаемый, но откуда еще два? Как это исправить?
Песочница с примером, чтобы понять ошибку и исправить ее
Задача:
Необходимо создать компонент, который принимает массив с данными и выводит их в виде таблицы. Необходимо давать возможность задавать колонки и вид ячейки.
Информация о колонках и виде ячейки должна передаваться через специальный компонент (так же, как и у element-ui):
В начале задача не содержала необходимости делать так же, как и у element-ui. Но оказалось, что некоторые люди способны выполнить задачу в первоначальной формулировке. Поэтому и добавилось требование передавать информацию о колонках и виде ячейки с помощью компонентов.
Уверен, ваши собеседуемые будут все время в ступоре. Можете дать им 30 минут на решение такой задачи.
Если ваш кандидат не справился с предыдущим заданием, ничего страшного: можете дать ему еще одно, не менее сложное!
Задача:
Создать компонент Portal и PortalTarget, как у библиотеки portal-vue:
Вопрос:
Вы получили от апи большой объект и отобразили его пользователю. Примерно так:
В этом коде есть проблема. У объекта item мы не меняем name, price, quality и остальные свойства. Но Vue об этом не знает и добавляет реактивность в каждое поле.
Как можно этого избежать?
Вопрос:
Есть компонент с методом, который выводит одно из свойств объекта item в консоль, а затем удаляет объект item:
Что здесь может пойти не так?
Ссылка на песочницу, чтобы убедиться в ответе
Спасибо, что дочитали статью до конца! Думаю, теперь вы точно сможете казаться умнее на собеседованиях и у ваших кандидатов сильно упадут зарплатные ожидания!
Вы любите собеседования? И часто проводите их? Если ответ на второй вопрос «Да», то среди кандидатов вам наверняка встречались отличные и умные люди, которые отвечали на все ваши вопросы и приближались к концу зарплатной вилки.
Но вы, конечно, не хотите платить профессионалам слишком много. И жизненно необходимо казаться умнее них, пускай только на время собеседования.
Если у вас с этим проблемы, то добро пожаловать под кат. Там вы найдете самые каверзные и извращенные вопросы по 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» — неправильный ответ.
Ответ
В консоли мы увидим только цифру 4.
Объяснение
В хуке beforeCreate еще не создан сам инстанс, watcher здесь работать не будет.
Watcher срабатывает на изменения в хуке created, beforeMount и mounted. Так как все эти хуки вызываются во время одного тика, Vue вызовет watcher один раз в самом конце, со значением 4.
Vue отпишется от наблюдения за изменением переменной перед вызовом хуков beforeDestroy и destroyed, поэтому 5 и 6 не попадут в консоль.
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 с типом Boolean отличается от всех остальных тем, что во Vue для него есть специальное приведение типов.
Если в качестве параметра будет передана пустая строка или название самого prop'а в kebab-case, то Vue преобразует это в true.
Пример:
У нас есть файл с Boolean prop'ом:
Ниже показаны все валидные варианты использования компонента TestComponent.
Если в качестве параметра будет передана пустая строка или название самого 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>
Необходимо сказать, где здесь потенциальная ошибка и как ее исправить.
Ответ
Проблема в том, что массив внутри $refs может идти не в том порядке, как и оригинальный массив (ссылка на issue). То есть, может произойти такая ситуация: кликаем на кнопку третьего элемента списка, а на консоль выводится dom-элемент второго.
Такое происходит только тогда, когда в массиве часто изменяются данные.
Методы решения написаны в issue на GitHub'е:
1. Создавать уникальный ref для каждого элемента
2. Дополнительный аттрибут
Такое происходит только тогда, когда в массиве часто изменяются данные.
Методы решения написаны в 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 пытается сопоставить старый Virtual DOM с новым, чтобы понять, что нужно удалить и добавить:
Удаленные элементы перечеркнуты красным, созданные — выделены зеленым
Vue не смог найти компонент TestMount, поэтому пересоздал его.
Аналогичная ситуация повторится через секунду после нажатия кнопки. В этот момент компонент TestMounted третий раз выведет на консоль информацию о своем создании.
Чтобы пофиксить проблему, достаточно поставить атрибут key к div'у с компонентом TestMounted:
Теперь Vue сможет однозначно сопоставить нужные элементы Virtual DOM'ов.
В самом начале наш 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 минут на решение такой задачи.
Решение
Основная идея состоит в том, чтобы в компоненте CustomColumn передать все данные компоненту CustomTable, а дальше он уже сам все срендерит.
Ниже дан пример реализации. Он не учитывает некоторых моментов (как, например, изменение label), но основной принцип должен быть понятен.
Ниже дан пример реализации. Он не учитывает некоторых моментов (как, например, изменение 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>
Решение
Для создания портала нужно реализовать три объекта:
Данное решение не поддерживает изменение атрибута to, не поддерживает анимации через transition и не имеет поддержки дефолтных значений, как portal-vue. Но общая идея должна быть понятна.
- Хранилище данных о порталах
- Компонент 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.freeze.
Vue проверит, заморожен ли объект с помощью метода Object.isFrozen. И если это так, то Vue не станет добавлять реактивные геттеры и сеттеры к свойствам объекта, так как их в любом случае невозможно изменить. При очень больших объектах эта оптимизация помогает сохранить до нескольких десятков миллисекунд.
Оптимизированный компонент будет выглядеть так:
Object.freeze замораживает только свойства самого объекта. Так что, если объект содержит в себе вложенные объекты, их тоже необходимо заморозить.
Обновление от 19.01.2019: По совету Дмитрия Злыгина глянул библиотеку vue-nonreactive и нашел еще один способ. Он отлично подойдет для ситуации, когда у вас много вложенных объектов.
Vue не станет добавлять реактивность в объект, если увидит, что он уже реактивен. Мы можем обмануть Vue, создав пустой Observer для объекта:
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 замораживает только свойства самого объекта. Так что, если объект содержит в себе вложенные объекты, их тоже необходимо заморозить.
Обновление от 19.01.2019: По совету Дмитрия Злыгина глянул библиотеку vue-nonreactive и нашел еще один способ. Он отлично подойдет для ситуации, когда у вас много вложенных объектов.
Vue не станет добавлять реактивность в объект, если увидит, что он уже реактивен. Мы можем обмануть Vue, создав пустой Observer для объекта:
/* ItemView.vue */
<template>
<!-- ... -->
</template>
<script>
import Vue from 'vue';
import getItemFromApi from 'path/to/getItemFromApi';
const Observer = new Vue()
.$data
.__ob__
.constructor;
export default {
/* .... */
async mounted() {
const item = await getItemFromApi();
// Добавляем пустой Observer для объекта
item.__ob__ = new Observer({});
this.item = item;
},
};
</script>
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>
Что здесь может пойти не так?
Ответ
Проблема в том, что после первого клика на кнопку Vue требуется некоторое время, чтобы обновить DOM для пользователя и убрать кнопку. Поэтому пользователь иногда может кликнуть два раза. Метод logAndClean отработает первый раз нормально, а второй раз крашнется, так как не сможет получить свойство value.
Я постоянно вижу такую проблему в трекере ошибок, особенно часто на дешевых мобильниках за 4-5к рублей.
Чтобы избежать ее, просто добавьте проверку на существование item в начале функции:
Чтобы воспроизвести баг, можете перейти в песочницу с примером, выставить максимальный троттлинг CPU и быстро-быстро покликать на кнопку. У меня, например, получилось.
Я постоянно вижу такую проблему в трекере ошибок, особенно часто на дешевых мобильниках за 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 и быстро-быстро покликать на кнопку. У меня, например, получилось.
Ссылка на песочницу, чтобы убедиться в ответе
Спасибо, что дочитали статью до конца! Думаю, теперь вы точно сможете казаться умнее на собеседованиях и у ваших кандидатов сильно упадут зарплатные ожидания!