Faisons les pires Vue.js du monde

Il y a quelque temps, j'ai publié un article similaire sur React où, avec quelques lignes de code, nous avons créé un petit clone de React.js à partir de zéro. Mais React est loin d'être le seul outil du monde frontal moderne, Vue.js gagne rapidement en popularité. Jetons un coup d'œil au fonctionnement de ce framework et créons un clone primitif similaire à Vue.js à des fins éducatives.



Réactivité



Comme React.js, Vue est réactif, ce qui signifie que toutes les modifications de l'état de l'application sont automatiquement reflétées dans le DOM. Mais contrairement à React, Vue garde une trace des dépendances au moment du rendu et ne met à jour que les parties associées sans aucune «comparaison».



La clé de la réactivité de Vue.js est la méthode Object.defineProperty



. Il vous permet de spécifier une méthode getter / setter personnalisée sur un champ d'objet et d'intercepter chaque accès à celui-ci:



const obj = {a: 1};
Object.defineProperty(obj, 'a', {
  get() { return 42; },
  set(val) { console.log('you want to set "a" to', val); }
});
console.log(obj.a); // prints '42'
obj.a = 100;        // prints 'you want to set "a" to 100'
      
      





Avec cela, nous pouvons déterminer quand une propriété particulière est en cours d'accès, ou quand elle change, puis réévaluer toutes les expressions dépendantes après que la propriété a changé.



Expressions



Vue.js vous permet de lier une expression JavaScript à un attribut de nœud DOM à l'aide d'une directive. Par exemple, <div v-text="s.toUpperCase()"></div>



définira le texte à l'intérieur du div sur une valeur de variable majuscule s



.



L'approche la plus simple pour évaluer des chaînes, telles que s.toUpperCase()



, consiste à utiliser eval()



. Bien qu'éval n'ait jamais été considéré comme une solution sûre, nous pouvons essayer de l'améliorer un peu en l'enveloppant dans une fonction et en le passant dans un contexte global personnalisé:



const call = (expr, ctx) =>
  new Function(`with(this){${`return ${expr}`}}`).bind(ctx)();

call('2+3', null);                    // returns 5
call('a+1', {a:42});                  // returns 43
call('s.toUpperCase()', {s:'hello'}); // returns "HELLO"
      
      





C'est un peu plus sûr que le natif eval



et suffisant pour le cadre simple que nous construisons.



Procuration



Nous pouvons maintenant utiliser Object.defineProperty



pour envelopper chaque propriété de l'objet de données; peut être utilisé call()



pour évaluer des expressions arbitraires et pour indiquer les propriétés auxquelles l'expression a accédé directement ou indirectement. Nous devons également être en mesure de déterminer quand l'expression doit être réévaluée car l'une de ses variables a changé:



const data = {a: 1, b: 2, c: 3, d: 'foo'}; // Data model
const vars = {}; // List of variables used by expression
// Wrap data fields into a proxy that monitors all access
for (const name in data) {
  let prop = data[name];
  Object.defineProperty(data, name, {
    get() {
      vars[name] = true; // variable has been accessed
      return prop;
    },
    set(val) {
      prop = val;
      if (vars[name]) {
        console.log('Re-evaluate:', name, 'changed');
      }
    }
  });
}
// Call our expression
call('(a+c)*2', data);
console.log(vars); // {"a": true, "c": true} -- these two variables have been accessed
data.a = 5;  // Prints "Re-evaluate: a changed"
data.b = 7;  // Prints nothing, this variable does not affect the expression
data.c = 11; // Prints "Re-evaluate: c changed"
data.d = 13; // Prints nothing.
      
      





Directives



Nous pouvons maintenant évaluer des expressions arbitraires et garder une trace des expressions à évaluer lorsqu'une variable de données particulière change. Il ne reste plus qu'à attribuer des expressions à certaines propriétés du nœud DOM et à les modifier lorsque les données changent.



Comme dans Vue.js, nous utiliserons des attributs spéciaux tels que q-on:click



pour lier les gestionnaires d'événements, q-text



pour lier textContent, q-bind:style



pour lier le style CSS, etc. J'utilise le préfixe "q-" ici car "q" est similaire à "vue".



Voici une liste partielle des directives prises en charge possibles:



const directives = {
  // Bind innerText to an expression value
  text: (el, _, val, ctx) => (el.innerText = call(val, ctx)),
  // Bind event listener
  on: (el, name, val, ctx) => (el[`on${name}`] = () => call(val, ctx)),
  // Bind node attribute to an expression value
  bind: (el, name, value, ctx) => el.setAttribute(name, call(value, ctx)),
};
      
      





Chaque directive est une fonction qui prend un nœud DOM, un nom de paramètre facultatif pour des cas tels que q-on:click



(le nom sera "clic"). Il nécessite également une chaîne d'expression ( value



) et un objet de données à utiliser comme contexte d'expression.



Maintenant que nous avons tous les éléments de base, il est temps de tout coller ensemble!



Résultat final



const call = ....       // Our "safe" expression evaluator
const directives = .... // Our supported directives

// Currently evaluated directive, proxy uses it as a dependency
// of the individual variables accessed during directive evaluation
let $dep;

// A function to iterate over DOM node and its child nodes, scanning all
// attributes and binding them as directives if needed
const walk = (node, q) => {
  // Iterate node attributes
  for (const {name, value} of node.attributes) {
    if (name.startsWith('q-')) {
      const [directive, event] = name.substring(2).split(':');
      const d = directives[directive];
      // Set $dep to re-evaluate this directive
      $dep = () => d(node, event, value, q);
      // Evaluate directive for the first time
      $dep();
      // And clear $dep after we are done
      $dep = undefined;
    }
  }
  // Walk through child nodes
  for (const child of node.children) {
    walk(child, q);
  }
};

// Proxy uses Object.defineProperty to intercept access to
// all `q` data object properties.
const proxy = q => {
  const deps = {}; // Dependent directives of the given data object
  for (const name in q) {
    deps[name] = []; // Dependent directives of the given property
    let prop = q[name];
    Object.defineProperty(q, name, {
      get() {
        if ($dep) {
          // Property has been accessed.
          // Add current directive to the dependency list.
          deps[name].push($dep);
        }
        return prop;
      },
      set(value) { prop = value; },
    });
  }
  return q;
};

// Main entry point: apply data object "q" to the DOM tree at root "el".
const Q = (el, q) => walk(el, proxy(q));

      
      





Un framework réactif de type Vue.js à son meilleur. À quel point est-ce utile? Voici un exemple:



<div id="counter">
  <button q-on:click="clicks++">Click me</button>
  <button q-on:click="clicks=0">Reset</button>
  <p q-text="`Clicked ${clicks} times`"></p>
</div>

Q(counter, {clicks: 0});
      
      





Appuyer sur un bouton incrémente le compteur et actualise automatiquement le contenu <p>



. Cliquer sur un autre met le compteur à zéro et met également à jour le texte.



Comme vous pouvez le voir, Vue.js a l'air magique à première vue, mais à l'intérieur, c'est très simple et les fonctionnalités de base peuvent être implémentées en quelques lignes de code.



Prochaines étapes



Si vous souhaitez en savoir plus sur Vue.js, essayez d'implémenter "q-if" pour basculer la visibilité des éléments basés sur une expression, ou "q-each" pour lier des listes d'enfants en double (ce serait un bon exercice ).



La source complète du nanoframework Q se trouve sur Github . N'hésitez pas à faire un don si vous repérez un problème ou souhaitez suggérer une amélioration!



En conclusion, je dois mentionner que cela a Object.defineProperty



été utilisé dans Vue 2 Vue 3 et que les créateurs sont passés à une autre installation fournie par ES6, à savoir Proxy



et Reflect



... Proxy vous permet de passer un gestionnaire pour intercepter l'accès aux propriétés de l'objet, comme dans notre exemple, tandis que Reflect vous permet d'accéder aux propriétés de l'objet depuis le proxy et de garder l' this



objet intact (contrairement à notre exemple avec defineProperty).



Je laisse les deux Proxy / Reflect comme exercice pour le lecteur, donc quiconque demande à les utiliser correctement dans Q - je serai heureux de combiner cela. Bonne chance!



J'espère que vous avez apprécié l'article. Vous pouvez suivre l'actualité et partager des suggestions sur Github , Twitter ou vous abonner via rss .



All Articles