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 .