Reatividade
Como React.js, Vue é reativo, o que significa que todas as alterações no estado do aplicativo são refletidas automaticamente no DOM. Mas, ao contrário do React, o Vue rastreia as dependências no momento da renderização e apenas atualiza as partes relacionadas sem nenhuma "comparação".
A chave para a reatividade do Vue.js é o método
Object.defineProperty
. Ele permite que você especifique um método getter / setter personalizado em um campo de objeto e intercepte todos os acessos a ele:
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'
Com isso, podemos determinar quando uma determinada propriedade está sendo acessada ou quando ela muda e, em seguida, reavaliar todas as expressões dependentes após a alteração da propriedade.
Expressões
Vue.js permite vincular uma expressão JavaScript a um atributo de nó DOM usando uma diretiva. Por exemplo,
<div v-text="s.toUpperCase()"></div>
definirá o texto dentro do div para um valor de variável em maiúsculas
s
.
A abordagem mais simples para avaliar strings, como
s.toUpperCase()
, é usar
eval()
. Embora eval nunca tenha sido considerada uma solução segura, podemos tentar torná-la um pouco melhor envolvendo-a em uma função e passando em um contexto global personalizado:
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"
Este é um pouco mais seguro do que o nativo
eval
e é suficiente para o framework simples que estamos construindo.
Proxy
Agora podemos usar
Object.defineProperty
para envolver cada propriedade do objeto de dados; pode ser usado
call()
para avaliar expressões arbitrárias e dizer quais propriedades a expressão acessou direta ou indiretamente. Também precisamos ser capazes de determinar quando a expressão deve ser reavaliada porque uma de suas variáveis mudou:
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.
Diretivas
Agora podemos avaliar expressões arbitrárias e controlar quais expressões avaliar quando uma determinada variável de dados muda. Tudo o que resta é atribuir expressões a certas propriedades do nó DOM e realmente alterá-las quando os dados forem alterados.
Como em Vue.js, usaremos atributos especiais, como
q-on:click
vincular manipuladores de eventos,
q-text
vincular textContent,
q-bind:style
vincular estilo CSS e assim por diante. Eu uso o prefixo "q-" aqui porque "q" é semelhante a "vue".
Aqui está uma lista parcial de possíveis diretivas com suporte:
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)),
};
Cada diretiva é uma função que leva um nó DOM, um nome de parâmetro opcional para casos como
q-on:click
(o nome será "clique"). Também requer uma string de expressão (
value
) e um objeto de dados a serem usados como o contexto da expressão.
Agora que temos todos os blocos de construção, é hora de colar tudo junto!
Resultado 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));
Uma estrutura reativa do tipo Vue.js no seu melhor. Quão útil é isso? Aqui está um exemplo:
<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});
Pressionar um botão incrementa o contador e atualiza automaticamente o conteúdo
<p>
. Clicar em outro define o contador para zero e também atualiza o texto.
Como você pode ver, Vue.js parece mágico à primeira vista, mas por dentro é muito simples e a funcionalidade básica pode ser implementada em apenas algumas linhas de código.
Próximas etapas
Se você estiver interessado em aprender mais sobre Vue.js, tente implementar "q-if" para alternar a visibilidade dos elementos com base em uma expressão ou "q-each" para vincular listas de filhos duplicados (este seria um bom exercício )
A fonte completa do nanoframework Q está no Github . Sinta-se à vontade para doar se detectar um problema ou quiser sugerir uma melhoria!
Em conclusão, devo mencionar que
Object.defineProperty
foi usado no Vue 2 Vue 3 e os criadores mudaram para outra facilidade fornecida ES6, ou seja,
Proxy
e
Reflect
... O proxy permite que você passe um manipulador para interceptar o acesso às propriedades do objeto, como em nosso exemplo, enquanto o Reflect permite que você acesse as propriedades do objeto de dentro do proxy e mantenha o
this
objeto intacto (ao contrário do nosso exemplo com defineProperty).
Deixo Proxy / Reflect como um exercício para o leitor, então quem fizer uma solicitação para usá-los corretamente no Q - ficarei feliz em combinar isso. Boa sorte!
Espero que você tenha gostado do artigo. Você pode acompanhar as notícias e compartilhar sugestões no Github , Twitter ou se inscrever via rss .