Vamos fazer o pior Vue.js do mundo

Há algum tempo, publiquei um artigo semelhante no React em que, com algumas linhas de código, criamos um pequeno clone do React.js do zero. Mas o React está longe de ser a única ferramenta no mundo do front-end moderno, o Vue.js está ganhando popularidade rapidamente. Vamos dar uma olhada em como essa estrutura funciona e criar um clone primitivo semelhante ao Vue.js para fins educacionais.



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 .



All Articles