Maison  >  Article  >  interface Web  >  12 Questions d'entretien sur le principe haute fréquence Vue (avec analyse)

12 Questions d'entretien sur le principe haute fréquence Vue (avec analyse)

青灯夜游
青灯夜游avant
2020-07-13 16:17:2751870parcourir

12 Questions d'entretien sur le principe haute fréquence Vue (avec analyse)

Cet article partage 12 questions d'entretien sur les principes de Vue à haute fréquence, couvrant les principes fondamentaux de mise en œuvre de Vue. En fait, il est impossible d'expliquer les principes de mise en œuvre d'un framework dans un seul article. J'espère qu'à travers ces 12 Cette question permettra aux lecteurs d'avoir une certaine compréhension de leur propre maîtrise de Vue (numéro B), afin de combler leurs propres lacunes et de mieux maîtriser Vue.

[Recommandations associées : questions d'entretien vue(2021)]

1. Principe de réactivité de Vue

12 Questions dentretien sur le principe haute fréquence Vue (avec analyse)

Classe d'implémentation principale :

Observateur : sa fonction consiste à ajouter des getters et des setters aux propriétés de l'objet pour la collecte des dépendances et la distribution des mises à jour

Dep : utilisé pour collecter les dépendances de l'objet réactif actuel. Chaque objet réactif, y compris les sous-objets, a une instance Dep. (les sous-marins à l'intérieur se trouvent un tableau d'instances de Watcher). Lorsque les données changent, chaque observateur sera averti via dep.notify().

Observateur : Objet Observer, l'instance est divisée en trois types : observateur de rendu (observateur de rendu), observateur d'attributs calculés (observateur calculé), observateur d'écoute (observateur d'utilisateur)

Observateur et Dep Dep est instancié dans la relation

watcher et ajoute des abonnés à dep.subs dep parcourt dep.subs via notify pour notifier chaque mise à jour de l'observateur.

Collection de dépendances

  1. InitState, lorsque l'attribut calculé est initialisé, l'observateur calculé est déclenché.Collection de dépendances
  2. InitState, lorsque l'attribut d'écoute est initialisé, le L'observateur de l'utilisateur est déclenché. Le processus de collecte des dépendances
  3. render() déclenche l'observateur de rendu. Lorsque la collection des dépendances
  4. re-rend, vm.render() est à nouveau exécuté, ce qui supprimera tout. abonnements des observateurs dans les sous-marins et re-rendu.

Distribuer les mises à jour

  1. Les données de réponse sont modifiées dans le composant, et la logique de déclenchement du setter
  2. appelle dep.notify()
  3. Parcourez tous les sous-marins (instances Watcher) et appelez la méthode de mise à jour de chaque observateur.

Principe

Lors de la création d'une instance Vue, vue traversera les propriétés de l'option de données et utilisera Object.defineProperty pour ajouter des getters et des setters aux propriétés afin de détourner la lecture de les données (les getters sont utilisés (pour collecter les dépendances, les setters sont utilisés pour distribuer les mises à jour) et suivent les dépendances en interne, notifiant les changements lorsque les propriétés sont accédées et modifiées.

Chaque instance de composant aura une instance d'observateur correspondante, qui enregistrera tous les attributs de données des dépendances pendant le processus de rendu du composant (collecte de dépendances, ainsi que les instances d'observateur calculées et d'observateur utilisateur), puis les dépendances sont modifiées. Lorsque l'instance d'observateur dépend de ces données, la méthode de définition notifiera à l'instance d'observateur de recalculer (distribuer les mises à jour), restituant ainsi son composant associé.

Résumé en une phrase :

vue.js utilise le piratage de données combiné au mode publication-abonnement, détourne les setters et getters de chaque propriété via Object.defineproperty et publie des messages aux abonnés lorsque les données changements. , déclenchant le rappel d'écoute de la réponse

2. Le principe de mise en œuvre du calculé

calculé est essentiellement un observateur d'évaluation paresseux.

calculé implémente en interne un observateur paresseux, c'est-à-dire que l'observateur calculé n'évaluera pas immédiatement et conservera une instance dep.

Il utilise en interne l'attribut this.dirty pour indiquer si la propriété de calcul doit être réévaluée.

Lorsque l'état de dépendance du calcul change, cet observateur paresseux sera averti,

l'observateur calculé détermine s'il y a des abonnés via this.dep.subs.length,

Si oui, il sera recalculé, puis les nouvelles et anciennes valeurs seront comparées si cela change, il sera restitué. (Vue veut garantir que non seulement la valeur dont dépend la propriété calculée change, mais que lorsque la valeur calculée finale de la propriété calculée change, l'observateur de rendu sera déclenché pour effectuer un nouveau rendu, ce qui est essentiellement une optimisation. )

Sinon, définissez simplement this.dirty = true. (Lorsqu'un attribut calculé dépend d'autres données, l'attribut ne sera pas recalculé immédiatement. Il ne sera réellement calculé que lorsque d'autres endroits auront besoin de lire l'attribut plus tard, c'est-à-dire qu'il a des caractéristiques paresseuses (calcul paresseux). )

3. Quelles sont les différences et les scénarios d'application entre calculé et montre ?

Différence

calculé calculé propriété : dépend des autres valeurs d'attribut et la valeur calculée est mise en cache. Ce n'est que lorsque la valeur d'attribut dont elle dépend change que la valeur calculée sera recalculée la prochaine fois que la valeur calculée sera obtenue.

écouteur de surveillance : plutôt un rôle "d'observation", sans cache, similaire au rappel de surveillance de certaines données, le rappel sera exécuté à chaque fois que les données surveillées changent. Suivi.

Scénarios d'application

Scénarios d'application :

Lorsque nous devons effectuer des calculs numériques et dépendre d'autres données, calculé doit être utilisé car la fonction de cache de calculé peut être utilisée pour éviter Chaque fois qu'une valeur est obtenue, elle est recalculée.

Watch doit être utilisé lorsque nous devons effectuer des opérations asynchrones ou coûteuses lorsque les données changent. L'utilisation de l'option watch nous permet d'effectuer une opération asynchrone (accéder à une API), de limiter la fréquence à laquelle nous effectuons l'opération et le moment où. nous obtenons Avant le résultat final, définissons l'état intermédiaire. Ce sont des choses que les propriétés calculées ne peuvent pas faire.

4. Pourquoi Proxy est-il adopté dans Vue3.0 et Object.defineProperty abandonné ?

Object.defineProperty lui-même a une certaine capacité à surveiller les changements dans les indices du tableau, mais dans Vue, compte tenu du rapport coût-efficacité des performances/expérience, cette fonctionnalité a été abandonnée ( Pourquoi Vue ne peut-il pas détecter les changements de tableau ). Afin de résoudre ce problème, après le traitement interne de Vue, vous pouvez utiliser les méthodes suivantes pour surveiller le tableau
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

Puisque seules les 7 méthodes ci-dessus sont piratées, les attributs des autres tableaux ne peuvent toujours pas être détectés. certaines limites.

Object.defineProperty ne peut détourner que les propriétés d'un objet, nous devons donc parcourir chaque propriété de chaque objet. Dans Vue 2.x, la surveillance des données est réalisée par récursion + parcours de l'objet de données. Si la valeur de l'attribut est également un objet, alors un parcours approfondi est évidemment un meilleur choix si un objet complet peut être détourné.

Le proxy peut détourner l'intégralité de l'objet et renvoyer un nouvel objet. Le proxy peut non seulement proxy des objets, mais également des tableaux proxy. Vous pouvez également proxy des attributs ajoutés dynamiquement.

5. À quoi servent les clés dans Vue ?

key est l'identifiant unique donné à chaque vnode. En fonction de la clé, notre opération de comparaison peut être plus précise et plus rapide (les nœuds de comparaison sont également plus rapides pour le rendu simple des pages de liste, mais il y a. Il y aura des effets secondaires cachés, tels qu'il peut n'y avoir aucun effet de transition, ou il y aura un désalignement d'état lorsque certains nœuds ont des états de données (formulaire) liés)

L'algorithme Diff sera effectué en premier. anciens et nouveaux nœuds Lorsqu'il n'y a pas de correspondance, la clé du nouveau nœud sera comparée à l'ancien nœud pour trouver l'ancien nœud correspondant

est plus précis : car avec la clé, elle n'est pas réutilisée. en place, la réutilisation sur place peut être évitée dans la comparaison de la fonction sameNode a.key === b.key. Par conséquent, il sera plus précis. Si la clé n’est pas ajoutée, l’état du nœud précédent sera conservé, ce qui produira une série de bugs.

Plus rapide : le caractère unique de la clé peut être pleinement utilisé par la structure de données Map. Par rapport à la complexité temporelle de la recherche traversante O(n), la complexité temporelle de Map n'est que de O(1) du code source. est le suivant :

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}

6. Parlons du principe de nextTick

Mécanisme de fonctionnement JS

L'exécution JS est monothread et est basée sur une boucle d'événements. La boucle d'événements est grossièrement divisée en les étapes suivantes :

  1. Toutes les tâches de synchronisation sont exécutées sur le thread principal, formant une pile de contexte d'exécution.
  2. En plus du fil de discussion principal, il existe également une "file d'attente des tâches". Tant que la tâche asynchrone a des résultats en cours d'exécution, un événement est placé dans la « file d'attente des tâches ».
  3. Une fois que toutes les tâches de synchronisation de la « pile d'exécution » ont été exécutées, le système lira la « file d'attente des tâches » pour voir quels événements s'y trouvent. Les tâches asynchrones correspondantes mettent fin à l'état d'attente, entrent dans la pile d'exécution et démarrent l'exécution.
  4. Le fil principal ne cesse de répéter la troisième étape ci-dessus.

12 Questions dentretien sur le principe haute fréquence Vue (avec analyse)

Le processus d'exécution du thread principal est un tick, et tous les résultats asynchrones sont planifiés via la "file d'attente des tâches". La file d'attente de messages stocke les tâches une par une. La spécification stipule que les tâches sont divisées en deux catégories, à savoir les macro-tâches et les micro-tâches, et qu'après la fin de chaque macro-tâche, toutes les micro-tâches doivent être effacées.

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();

  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

Dans l'environnement du navigateur :

Les tâches de macro courantes incluent setTimeout, MessageChannel, postMessage, setImmediate

Les micro-tâches courantes incluent MutationObsever et Promesse.then

File d'attente de mise à jour asynchrone

Peut-être que vous ne l'avez pas encore remarqué, lorsque Vue met à jour le DOM, c'estExécuté de manière asynchrone. Tant qu'il écoute les modifications de données, Vue ouvrira une file d'attente et mettra en mémoire tampon toutes les modifications de données qui se produisent dans la même boucle d'événements.

Si le même observateur est déclenché plusieurs fois, il ne sera poussé qu'une seule fois dans la file d'attente. Cette déduplication lors de la mise en mémoire tampon est importante pour éviter les calculs et opérations DOM inutiles.

Ensuite, au prochain « tick » de la boucle d'événement, Vue vide la file d'attente et effectue le travail réel (dédupliqué).

Vue essaie en interne d'utiliser Promise.then natif, MutationObserver et setImmediate pour les files d'attente asynchrones si l'environnement d'exécution ne le prend pas en charge, setTimeout(fn, 0) sera utilisé à la place.

Dans le code source de vue2.5, les solutions de rétrogradation des macrotâches sont : setImmediate, MessageChannel, setTimeout

vue 的 nextTick 方法的实现原理:

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

7. vue 是如何对数组方法进行变异的 ?

我们先来看看源码

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

8. Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once

Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
class Vue {
  constructor() {
    //  事件通道调度中心
    this._events = Object.create(null);
  }
  $on(event, fn) {
    if (Array.isArray(event)) {
      event.map(item => {
        this.$on(item, fn);
      });
    } else {
      (this._events[event] || (this._events[event] = [])).push(fn);
    }
    return this;
  }
  $once(event, fn) {
    function on() {
      this.$off(event, on);
      fn.apply(this, arguments);
    }
    on.fn = fn;
    this.$on(event, on);
    return this;
  }
  $off(event, fn) {
    if (!arguments.length) {
      this._events = Object.create(null);
      return this;
    }
    if (Array.isArray(event)) {
      event.map(item => {
        this.$off(item, fn);
      });
      return this;
    }
    const cbs = this._events[event];
    if (!cbs) {
      return this;
    }
    if (!fn) {
      this._events[event] = null;
      return this;
    }
    let cb;
    let i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
  $emit(event) {
    let cbs = this._events[event];
    if (cbs) {
      const args = [].slice.call(arguments, 1);
      cbs.map(item => {
        args ? item.apply(this, args) : item.call(this);
      });
    }
    return this;
  }
}

10. 说说 Vue 的渲染过程

12 Questions dentretien sur le principe haute fréquence Vue (avec analyse)

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)
  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
  • generate 函数生成 render 函数字符串
  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

11. 聊聊 keep-alive 的实现原理和缓存策略

export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },

  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },

  destroyed() {
    for (const key in this.cache) {
      // 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },

  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }

      const { cache, keys } = this;
      // 获取键,优先获取组件的name字段,否则是组件的tag
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 不命中缓存,把 vnode 设置进缓存
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名
  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
  4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

12 Questions dentretien sur le principe haute fréquence Vue (avec analyse)

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

12. vm.$set()实现原理是什么?

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

export function set(target: Array<any> | Object, key: any, val: any): any {
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val);
    return val;
  }
  // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 进行响应式处理
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值
  3. 如果 target 本身就不是响应式,直接赋值
  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

本文转载自:https://segmentfault.com/a/1190000021407782

推荐教程:《JavaScript视频教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer