Bonjour, Habr!
Aimez-vous les entretiens d'embauche? Et souvent les dépenser? Si la réponse à la deuxième question est «Oui», alors parmi les candidats, vous avez probablement rencontré des personnes excellentes et intelligentes qui ont répondu à toutes vos questions et ont approché la fin de la paie.
Mais vous, bien sûr, ne voulez pas trop payer les professionnels. Et il est vital de paraître plus intelligent qu'eux, ne les laissez que pour la durée de l'entretien.
Si vous avez des problèmes avec cela, alors bienvenue au chat. Vous y trouverez les questions les plus délicates et les plus perverses sur Vue, ce qui mettra tout candidat en place et vous fera douter de vos compétences professionnelles.

1. Déclencheur Watcher à l'intérieur des crochets du cycle de vie
Cette question peut sembler facile, mais je vous garantis que personne, même le développeur le plus avancé, n'y répondra. Vous pouvez lui demander au début de l'entretien pour que le candidat ressent immédiatement votre supériorité.
Question:Il existe un composant TestComponent qui a une variable de montant. À l'intérieur des principaux crochets du cycle de vie, nous le définissons sur une valeur dans l'ordre numérique de 1 à 6. Watcher, qui affiche sa valeur dans la console, se tient sur cette variable.
Nous créons une instance TestComponent et la supprimons après quelques secondes. Il faut dire que nous verrons dans la sortie console.
Code:
/* 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>
Je vais vous donner un indice: "2345" est la mauvaise réponse.
La réponseDans la console, nous ne verrons que le chiffre 4.
ExplicationL'instance elle-même n'a pas encore été créée dans le hook beforeCreate, watcher ne fonctionnera pas ici.
Watcher déclenche des modifications sur les crochets créés, beforeMount et montés. Étant donné que tous ces crochets sont appelés pendant un tick, Vue appellera watcher une fois à la fin, avec une valeur de 4.
Vue se désabonnera de la surveillance du changement de la variable avant d'appeler les hooks beforeDestroy et destroy, donc 5 et 6 n'atteindront pas la console.
Sandbox avec un exemple pour vous assurer que la réponse2. Comportement implicite des accessoires
Cette question est basée sur le comportement des accessoires rares dans Vue. Tous les programmeurs, bien sûr, exposent simplement les validations nécessaires pour prop'ov et ne rencontrent jamais un tel comportement. Mais le candidat n'a pas besoin de le dire. Il serait préférable de poser cette question, d'y jeter un coup d'œil condamnant après une réponse incorrecte et de passer à la suivante.
Question:En quoi l'hélice avec un type booléen diffère-t-elle des autres?
/* SomeComponent.vue */ <template> </template> <script> export default { props: { testProperty: { type: Boolean, }, }, }; </script>
La réponseProp avec un type booléen diffère de tous les autres en ce que Vue a un casting de type spécial pour cela.
Si une chaîne vide ou le nom de l'accessoire lui-même dans kebab-case est passé en paramètre, Vue le convertira en vrai.
Un exemple:
Nous avons un fichier avec prop booléen:
/* TestComponent.vue */ <template> <div v-if="canShow"> I'm TestComponent </div> </template> <script> export default { props: { canShow: { type: Boolean, required: true, }, }, }; </script>
Tous les cas d'utilisation valides pour le composant TestComponent sont indiqués ci-dessous.
/* TestWrapper.vue */ <template> <div> <TestComponent canShow="" /> <TestComponent canShow /> <TestComponent canShow="can-show" /> </div> </template> <script> import TestComponent from 'path/to/TestComponent'; export default { components: { TestComponent, }, }; </script>
Sandbox avec un exemple pour vous assurer que la réponse3. Utilisation d'un tableau dans $ refs
Si votre candidat sait comment fonctionne le cadre de l'intérieur vers le niveau Evan Yu, vous avez encore quelques atouts dans votre manche: vous pouvez poser une question sur le comportement non documenté et non évident du cadre.
Question:Vuex contient un tableau d'objets de fichiers, chacun des objets du tableau a des propriétés de nom et id uniques. Ce tableau est mis à jour toutes les quelques secondes, des éléments sont supprimés et ajoutés.
Nous avons un composant qui affiche le nom de chaque objet tableau avec un bouton, en cliquant sur lequel l'élément dom associé au fichier courant doit être affiché dans la console:
/* 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>
Il faut dire où est l'erreur potentielle et comment y remédier.
La réponseLe problème est que le tableau à l'intérieur de $ refs peut ne pas aller dans le même ordre que le tableau d'origine (
lien vers le problème ). Autrement dit, une telle situation peut se produire: nous cliquons sur le bouton du troisième élément de la liste, et l'élément dom du second est affiché sur la console.
Cela se produit uniquement lorsque les données du tableau changent fréquemment.
Les méthodes de la solution sont écrites sur GitHub:
1. Créez une référence unique pour chaque article
<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. Attribut supplémentaire
<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. Loisirs de composants étranges
Question:Nous avons un composant spécial qui écrit sur la console chaque fois que le crochet monté est appelé:
/* TestMount.vue */ <template> <div> I'm TestMount </div> </template> <script> export default { mounted() { console.log('TestMount mounted'); }, }; </script>
Ce composant est utilisé dans le composant TestComponent. Il a un bouton, en appuyant sur lequel pendant 1 seconde le message Top message apparaîtra.
/* 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>
Cliquez sur le bouton et voyez ce qui se passe dans la console:

La première monture était attendue, mais où sont les deux autres? Comment y remédier?
Sandbox avec un exemple pour comprendre l'erreur et la corrigerLa réponseLe problème vient ici des particularités de la recherche de différences de DOM virtuels dans Vue.
Au tout début, notre DOM virtuel ressemble à ceci:
Après avoir cliqué sur le bouton, cela ressemble à ceci:
Vue essaie de faire correspondre l'ancien DOM virtuel avec le nouveau pour comprendre ce qui doit être supprimé et ajouté:
Les éléments supprimés sont barrés en rouge, les éléments créés sont surlignés en vertVue n'a pas pu trouver le composant TestMount, il l'a donc recréé.
Une situation similaire se répétera une seconde après avoir appuyé sur le bouton. À ce stade, le composant TestMounted affiche des informations sur sa création sur la console pour la troisième fois.
Pour résoudre le problème, placez simplement l'attribut key sur le div avec le composant TestMounted:
/* TestComponent.vue */ <template> <div> <div key="container"> <TestMount /> </div> </div> </template> /* ... */
Maintenant, Vue pourra cartographier sans ambiguïté les éléments nécessaires des DOM virtuels.
5. Création d'un composant de table
Défi:Il est nécessaire de créer un composant qui prend un tableau avec des données et les affiche dans un tableau. Il est nécessaire de donner la possibilité de spécifier les colonnes et le type de cellule.
Les informations sur les colonnes et le type de cellule doivent être transmises via un composant spécial (identique à
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>
Au début, la tâche ne contenait pas la nécessité de faire la même chose que element-ui. Mais il s'est avéré que certaines personnes sont en mesure de terminer la tâche dans le libellé d'origine. Par conséquent, l'exigence a été ajoutée pour transmettre des informations sur les colonnes et le type de cellule utilisant des composants.
Je suis sûr que vos interlocuteurs seront tout le temps dans la stupeur. Vous pouvez leur donner 30 minutes pour résoudre un tel problème.
SolutionL'idée principale est de transférer toutes les données vers le composant CustomTable dans le composant CustomColumn, puis il restituera tout lui-même.
Voici un exemple d'implémentation. Il ne prend pas en compte certains points (comme le changement d'étiquette), mais le principe de base doit être clair.
export default { render() { return null; }, props: { label: { type: String, required: true, }, }, mounted() {
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. Création d'un portail
Si votre candidat n'a pas terminé la tâche précédente, il n'y a rien à craindre: vous pouvez lui en donner une de plus, pas moins difficile!
Défi:Créez un composant Portal et PortalTarget, comme la bibliothèque
portal-vue :
/* FirstComponent.vue */ <template> <div> <Portal to="title"> Super header </Portal> </div> </template>
/* SecondComponent.vue */ <template> <div> <PortalTarget name="title" /> </div> </template>
SolutionPour créer un portail, vous devez implémenter trois objets:
- Entrepôt de données du portail
- Composant de portail qui ajoute des données au magasin
- Le composant PortalTarget qui récupère les données du magasin et les affiche
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;
import dataBus from './dataBus'; let currentId = 0; export default { props: { to: { type: String, required: true, }, }, computed: {
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); }, }, };
Cette solution ne prend pas en charge la modification de l'attribut to, ne prend pas en charge les animations pendant la transition et ne prend pas en charge les valeurs par défaut, comme portal-vue. Mais l'idée générale doit être claire.
7. Prévention de la réactivité
Question:Vous avez reçu un grand objet de l'API et l'avez affiché à l'utilisateur. Quelque chose comme ça:
/* 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>
Il y a un problème dans ce code. Nous ne modifions pas le nom, le prix, la qualité et les autres propriétés de l'objet article. Mais Vue ne le sait pas et ajoute de la réactivité à chaque champ.
Comment éviter cela?
La réponsePour éviter de changer les propriétés en réactif, vous devez figer l'objet avant de l'ajouter dans Vue à l'aide de la méthode Object.freeze.
Vue vérifiera si l'objet est gelé à l'aide de la méthode Object.isFrozen. Et si c'est le cas, Vue n'ajoutera pas de getters et setters réactifs aux propriétés de l'objet, car ils ne peuvent en aucun cas être modifiés. Avec de très gros objets, cette optimisation permet d'économiser jusqu'à plusieurs dizaines de millisecondes.
Le composant optimisé ressemblera à ceci:
/* 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 fige uniquement les propriétés de l'objet lui-même. Ainsi, si un objet contient des objets imbriqués, ils doivent également être gelés.
Mise à jour du 19 janvier 2019 : Sur les conseils de
Dmitry Zlygin, j'ai regardé la
bibliothèque vue- nonreactive et j'ai trouvé un autre moyen. Il est parfait pour les situations où vous avez beaucoup d'objets imbriqués.
Vue n'ajoutera pas de réactivité à un objet s'il voit qu'il est déjà réactif. Nous pouvons tromper Vue en créant un observateur vide pour l'objet:
/* 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(); </script>
8. Erreurs des appareils lents
Question:Il existe un composant avec une méthode qui affiche l'une des propriétés de l'objet élément dans la console, puis supprime l'objet élément:
/* 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>
Qu'est-ce qui pourrait mal tourner ici?
La réponseLe problème est qu'après le premier clic sur le bouton Vue, il faut un certain temps pour mettre à jour le DOM pour l'utilisateur et supprimer le bouton. Par conséquent, l'utilisateur peut parfois double-cliquer. La méthode logAndClean fonctionne normalement la première fois et se bloque une deuxième fois, car elle ne peut pas obtenir la propriété value.
Je vois constamment un tel problème dans le traqueur d'erreurs, en particulier souvent sur les téléphones portables bon marché pour 4-5k roubles.
Pour l'éviter, il suffit d'ajouter une vérification de l'existence de l'élément au début de la fonction:
<template> </template> <script> export default { methods: { logAndClean() { const { item } = this; if (!item) { return; } console.log(item.value); this.item = null; }, }, }; </script>
Pour reproduire le bug, vous pouvez vous rendre dans le bac à sable avec un exemple, définir la limitation maximale du CPU et cliquer rapidement sur le bouton. Par exemple, je l'ai fait.
Lien Sandbox pour vous assurer que la réponseMerci d'avoir lu l'article jusqu'au bout! Je pense que maintenant vous pouvez certainement paraître plus intelligent lors des entretiens et les salaires de vos candidats vont baisser de manière significative!