Machen wir die schlechtesten Vue.js der Welt

Vor einiger Zeit habe ich einen ähnlichen Artikel über React veröffentlicht , in dem wir mit ein paar Codezeilen einen winzigen Klon von React.js von Grund auf neu erstellt haben. Aber React ist bei weitem nicht das einzige Tool in der modernen Front-End-Welt. Vue.js gewinnt schnell an Popularität. Lassen Sie uns einen Blick darauf werfen, wie dieses Framework funktioniert, und einen primitiven Klon erstellen, der Vue.js für Bildungszwecke ähnelt.



Reaktivität



Wie React.js ist Vue reaktiv, was bedeutet, dass alle Änderungen am Status der Anwendung automatisch im DOM angezeigt werden. Im Gegensatz zu React verfolgt Vue jedoch die Abhängigkeiten beim Rendern und aktualisiert nur verwandte Teile ohne "Vergleiche".



Der Schlüssel zu Vue.js Reaktivität ist die Methode Object.defineProperty



. Sie können eine benutzerdefinierte Getter / Setter-Methode für ein Objektfeld angeben und jeden Zugriff darauf abfangen:



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'
      
      





Auf diese Weise können wir bestimmen, wann auf eine bestimmte Eigenschaft zugegriffen wird oder wann sie sich ändert, und dann alle abhängigen Ausdrücke neu auswerten, nachdem sich die Eigenschaft geändert hat.



Ausdrücke



Mit Vue.js können Sie einen JavaScript-Ausdruck mithilfe einer Direktive an ein DOM-Knotenattribut binden. Setzt beispielsweise <div v-text="s.toUpperCase()"></div>



den Text innerhalb des div auf einen variablen Wert in Großbuchstaben s



.



Der einfachste Ansatz zum Auswerten von Zeichenfolgen, z. B. s.toUpperCase()



, ist die Verwendung eval()



. Obwohl eval nie als sichere Lösung angesehen wurde, können wir versuchen, es ein wenig besser zu machen, indem wir es in eine Funktion einschließen und in einem benutzerdefinierten globalen Kontext übergeben:



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"
      
      





Dies ist etwas sicherer als das native eval



und reicht für das einfache Framework aus, das wir erstellen.



Proxy



Jetzt können wir Object.defineProperty



jede Eigenschaft des Datenobjekts umbrechen. kann verwendet werden call()



, um beliebige Ausdrücke auszuwerten und um festzustellen, auf welche Eigenschaften der Ausdruck direkt oder indirekt zugegriffen hat . Wir müssen auch bestimmen können, wann der Ausdruck neu bewertet werden soll, da sich eine seiner Variablen geändert hat:



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.
      
      





Richtlinien



Wir können jetzt beliebige Ausdrücke auswerten und verfolgen, welche Ausdrücke ausgewertet werden sollen, wenn sich eine bestimmte Datenvariable ändert. Sie müssen lediglich bestimmten Eigenschaften des DOM-Knotens Ausdrücke zuweisen und diese tatsächlich ändern, wenn sich die Daten ändern.



Wie in Vue.js verwenden wir spezielle Attribute, z. B. q-on:click



zum Binden von Ereignishandlern, q-text



zum Binden von textContent, q-bind:style



zum Binden des CSS-Stils usw. Ich benutze hier das Präfix "q-", weil "q" ähnlich wie "vue" ist.



Hier ist eine unvollständige Liste möglicher unterstützter Richtlinien:



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)),
};
      
      





Jede Direktive ist eine Funktion, die einen DOM-Knoten verwendet, einen optionalen Parameternamen für Fälle wie q-on:click



(der Name lautet "click"). Außerdem müssen eine Ausdruckszeichenfolge ( value



) und ein Datenobjekt als Ausdruckskontext verwendet werden.



Jetzt, da wir alle Bausteine ​​haben, ist es Zeit, alles zusammenzukleben!



Endergebnis



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));

      
      





Ein reaktives, Vue.js-ähnliches Framework vom Feinsten. Wie nützlich ist es? Hier ist ein Beispiel:



<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});
      
      





Durch Drücken einer Taste wird der Zähler erhöht und der Inhalt automatisch aktualisiert <p>



. Wenn Sie auf einen anderen klicken, wird der Zähler auf Null gesetzt und der Text aktualisiert.



Wie Sie sehen können, sieht Vue.js auf den ersten Blick wie Magie aus, ist jedoch sehr einfach und die grundlegende Funktionalität kann in nur wenigen Codezeilen implementiert werden.



Weitere Schritte



Wenn Sie mehr über Vue.js erfahren möchten, versuchen Sie, "q-if" zu implementieren, um die Sichtbarkeit von Elementen basierend auf einem Ausdruck umzuschalten, oder "q-each", um Listen doppelter untergeordneter Kinder zu binden (dies wäre eine gute Übung ).



Die vollständige Quelle für das Q-Nanoframework ist Github . Sie können gerne spenden, wenn Sie ein Problem entdecken oder eine Verbesserung vorschlagen möchten!



Abschließend sollte ich erwähnen, dass Object.defineProperty



im Vue 2 Vue 3 verwendet wurde und die Entwickler zu einer anderen Einrichtung gewechselt sind, die ES6 zur Verfügung stellt, nämlich Proxy



und Reflect



... Mit Proxy können Sie einen Handler übergeben, um den Zugriff auf Objekteigenschaften abzufangen, wie in unserem Beispiel, während Sie mit Reflect über den Proxy auf Objekteigenschaften zugreifen und das this



Objekt intakt halten können (im Gegensatz zu unserem Beispiel mit defineProperty).



Ich überlasse beide Proxy / Reflect als Übung für den Leser. Wer also eine Anfrage zieht, um sie in Q richtig zu verwenden - ich werde das gerne kombinieren. Viel Glück!



Ich hoffe, Ihnen hat der Artikel gefallen. Sie können den Nachrichten folgen und Vorschläge auf Github , Twitter oder über RSS abonnieren .



All Articles