Как стать автором
Обновить
0
Constanta
Мы любим спорт и делаем его технологичным

8 худших вопросов на собеседовании по Vue.js

Время на прочтение11 мин
Количество просмотров66K
Привет, Хабр!

Вы любите собеседования? И часто проводите их? Если ответ на второй вопрос «Да», то среди кандидатов вам наверняка встречались отличные и умные люди, которые отвечали на все ваши вопросы и приближались к концу зарплатной вилки.

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

Если у вас с этим проблемы, то добро пожаловать под кат. Там вы найдете самые каверзные и извращенные вопросы по Vue, которые поставят любого кандидата на место и заставят сомневаться в своих профессиональных навыках.

image

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 не попадут в консоль.

Песочница с примером, чтобы убедиться в ответе

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.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 для каждого элемента

<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:

/* 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), но основной принцип должен быть понятен.

/* 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.freeze.

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 в начале функции:

<template>
  <!-- ... -->
</template>

<script>
export default {
  /* ... */

  methods: {
    logAndClean() {
      const { item } = this;

      if (!item) {
        return;
      }

      console.log(item.value);

      this.item = null;
    },
  },
};
</script>

Чтобы воспроизвести баг, можете перейти в песочницу с примером, выставить максимальный троттлинг CPU и быстро-быстро покликать на кнопку. У меня, например, получилось.



Ссылка на песочницу, чтобы убедиться в ответе

Спасибо, что дочитали статью до конца! Думаю, теперь вы точно сможете казаться умнее на собеседованиях и у ваших кандидатов сильно упадут зарплатные ожидания!
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 36: ↑33 и ↓3+30
Комментарии35

Публикации

Информация

Сайт
const.tech
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия

Истории