Notas de estudo VUE (3) Explicação detalhada do processo de renderização Vue


A renderização de um conteúdo no Vue terá as seguintes etapas e processos:

O primeiro passo é analisar a gramática e gerar AST

A segunda etapa é concluir a inicialização dos dados de acordo com o resultado do AST

A terceira etapa é gerar o DOM virtual de acordo com o resultado AST e a vinculação de dados DATA

A quarta etapa é inserir o DOM real gerado pelo DOM virtual na página para renderização da página.


Então, como entender esse processo?


1. Analisando a gramática para gerar AST


A árvore de sintaxe AST é, na verdade, uma árvore de sintaxe abstrata (Abstract Syntax Tree), que se refere ao mapeamento das instruções no código-fonte para cada nó na árvore por meio da construção de uma árvore de sintaxe.

A árvore de estrutura DOM também é uma espécie de AST, que analisa a sintaxe HTML DOM e gera a página final.


Vejamos esse processo em detalhes:


1. Sintaxe de captura

No processo de geração do AST, o princípio do compilador estará envolvido, e ele passará pelo seguinte processo:


(1), análise sintática


A tarefa da análise gramatical é combinar sequências de palavras em várias frases gramaticais com base na análise léxica. Tais como: procedimentos, declarações, expressões, etc. O analisador julga se o programa de origem está estruturalmente correto, como instruções como v-if` / v-for, também existem tags DOM personalizadas como `` e sintaxe de ligação simplificada como `click`/`props. Eles precisam ser analisados ​​um por um e processados ​​de acordo.

(2), análise semântica


A análise semântica serve para verificar se há erros semânticos no programa fonte e coletar informações de tipo para a fase de geração do código.Também será feita uma verificação geral de tipo neste processo. Se vincularmos uma variável ou evento que não existe, ou usarmos um componente personalizado indefinido, etc., um erro será reportado neste estágio.


(3) Gerar AST


No Vue, a análise sintática e a análise semântica são basicamente processadas de maneira regular. Gerar AST é, na verdade, processar os elementos analisados, instruções, atributos, relacionamentos de nó pai-filho, etc., para obter um objeto AST. A seguir, uma fonte de versão simplificada código:


/**
 *  HTML编译成AST对象
 */
export function parse(
  template: string,
  options: CompilerOptions
): ASTElement | void 
{
  
  // 返回AST对象
  // 篇幅原因,一些前置定义省略
  // 此处开始解析HTML模板
  parseHTML(template, {
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    start(tag, attrs, unary) {
      // 一些前置检查和设置、兼容处理此处省略
      // 此处定义了初始化的元素AST对象
      const element: ASTElement = {
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),
        parent: currentParent,
        children: []
      };
      // 检查元素标签是否合法(不是保留命名)
      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true;
        process.env.NODE_ENV !== "production" &&
          warn(
            "Templates should only be responsible for mapping the state to the " +
              "UI. Avoid placing tags with side-effects in your templates, such as " +
              `<${tag}>` +
              ", as they will not be parsed."
          );
      }
      // 执行一些前置的元素预处理
      for (let i = 0; i < preTransforms.length; i++) {
        preTransforms[i](element, options);
      }
      // 是否原生元素
      if (inVPre) {
        // 处理元素元素的一些属性
        processRawAttrs(element);
      } else {
        // 处理指令,此处包括v-for/v-if/v-once/key等等
        processFor(element);
        processIf(element);
        processOnce(element);
        processKey(element); // 删除结构属性

        // 确定这是否是一个简单的元素
        element.plain = !element.key && !attrs.length;

        // 处理ref/slot/component等属性
        processRef(element);
        processSlot(element);
        processComponent(element);
        for (let i = 0; i < transforms.length; i++) {
          transforms[i](element, options);
        }
        processAttrs(element);
      }

      // 后面还有一些父子节点等处理,此处省略
    }
    // 其他省略
  });
  return root;
}

2. Captura do elemento DOM

Se precisarmos capturar um <div>elemento, gere outro <div>elemento.


Existe um modelo, podemos capturá-lo:


<div>
  <a>111</a>
  <p>222<span>333</span> </p>
</div>

Após a captura, podemos obter um objeto como este:


divObj = {
  dom: {
    type: "dom",
    ele: "div",
    nodeIndex: 0,
    children: [
      {
        type: "dom",
        ele: "a",
        nodeIndex: 1,
        children: [{ type: "text", value: "111" }]
      },
      {
        type: "dom",
        ele: "p",
        nodeIndex: 2,
        children: [
          { type: "text", value: "222" },
          {
            type: "dom",
            ele: "span",
            nodeIndex: 3,
            children: [{ type: "text", value: "333" }]
          }
        ]
      }
    ]
  }
};


Este objeto contém algumas informações que precisamos:

  • Quais variáveis ​​precisam ser associadas no elemento HTML, porque o conteúdo do nó precisa ser atualizado quando a variável é atualizada.

  • De que maneira unir, se há instruções lógicas, como v-if, v-foretc.

  • Quais nós estão vinculados a quais eventos de escuta e se eles correspondem a algum suporte de capacidade de evento comumente usado

O Vue irá gerar um pedaço de código executável baseado no objeto AST, vamos dar uma olhada na implementação desta parte:


// 生成一个元素
function genElement(el: ASTElement): string {
  // 根据该元素是否有相关的指令、属性语法对象,来进行对应的代码生成
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el);
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el);
  } else if (el.for && !el.forProcessed) {
    return genFor(el);
  } else if (el.if && !el.ifProcessed) {
    return genIf(el);
  } else if (el.tag === "template" && !el.slotTarget) {
    return genChildren(el) || "void 0";
  } else if (el.tag === "slot") {
    return genSlot(el);
  } else {
    // component或者element的代码生成
    let code;
    if (el.component) {
      code = genComponent(el.component, el);
    } else {
      const data = el.plain ? undefined : genData(el);

      const children = el.inlineTemplate ? null : genChildren(el, true);
      code = `_c('${el.tag}'${
        data ? `,${data}` : "" // data
      }${
        children ? `,${children}` : "" // children
      })`;
    }
    // 模块转换
    for (let i = 0; i < transforms.length; i++) {
      code = transforms[i](el, code);
    }
    // 返回最后拼装好的可执行的代码
    return code;
  }
}

3. Capacitação do mecanismo de modelo


Por meio da introdução acima, você pode dizer que é originalmente um <div>, e um objeto é gerado por meio de AST e, finalmente, um é gerado <div>. Não é uma etapa redundante?


De fato, podemos alcançar algumas funções neste processo:

  • Excluir elementos DOM inválidos e relatar erros durante o processo de compilação

  • Ao usar componentes personalizados, eles podem ser combinados

  • Ligação de dados, ligação de eventos e outras funções podem ser facilmente realizadas

  • Prepare o caminho para o processo virtual DOM Diff

  • Escape de HTML para evitar vulnerabilidades de XSS


Um mecanismo de modelo de uso geral pode lidar com muitas tarefas ineficientes e repetitivas, como compatibilidade de navegador, gerenciamento unificado e manutenção de eventos globais, mecanismo DOM virtual para atualizações de modelo e componentes de gerenciamento de organização de árvore. Dessa forma, depois de sabermos o que o mecanismo de modelo faz, podemos distinguir os recursos fornecidos pelo framework Vue da lógica que precisamos lidar sozinhos e podemos nos concentrar mais no desenvolvimento de negócios.


2. DOM virtual


O DOM virtual pode ser dividido em três processos:

O primeiro passo é simular a árvore DOM com objetos JS para obter uma árvore DOM virtual.

A segunda etapa é gerar uma nova árvore DOM virtual quando os dados da página forem alterados e comparar as diferenças entre as árvores DOM virtuais antigas e novas.

Na terceira etapa, o diff é aplicado à árvore DOM real.


1. Simule a árvore DOM com objetos JS

Por que usar DOM virtual? Porque um elemento DOM real é muito grande e tem muitos valores de atributo, mas na verdade não usaremos todos eles, geralmente incluindo conteúdo do nó, posição do elemento, estilo, adição e exclusão de nó e outros métodos. Portanto, podemos reduzir bastante a quantidade de cálculo para comparar diferenças usando objetos JS para representar elementos DOM.


Vamos dar uma olhada no código-fonte do VNode, existem apenas cerca de 20 atributos abaixo:


tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context fordevtools
fnScopeId: ?string; // functional scope id support

2. Compare a diferença entre as árvores DOM virtuais antigas e novas


No DOM virtual, a comparação de diferenças é uma etapa crítica.Quando o estado muda, uma nova árvore de objetos é reconstruída. Em seguida, compare a nova árvore com a velha e registre as diferenças entre as duas árvores. Tais diferenças precisam ser documentadas:

  • Precisa substituir o nó original
  • Mover, excluir, adicionar nós filhos
  • Modifique as propriedades do nó
  • Para nós de texto, o conteúdo do texto muda

Na figura abaixo, comparamos as duas árvores DOM, e as diferenças são:

  • O elemento p insere um nó filho do elemento span

  • O nó de texto original é movido para baixo do nó filho do elemento span


insira a descrição da imagem aqui


3. Aplique a diferença à árvore DOM real


Através dos exemplos anteriores, sabemos que para aplicar os registros de diferença à árvore DOM real, algumas operações são necessárias, como substituição de nó, movimentação, exclusão e alteração de conteúdo de texto.


Como fazer DOM Diff no Vue? Basta olhar para este código e ter uma ideia. Embora muitas funções no código não sejam publicadas, na verdade, você pode entender aproximadamente o que elas fazem observando o nome da função, como , , e updateChildrenassim addVnodespor removeVnodesdiante setTextContent.


// 对比差异后更新
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
  for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
  if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
  if (isDef(oldCh) && isDef(ch)) {
    if (oldCh !== ch)
      updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
  } else if (isDef(ch)) {
    if (process.env.NODE_ENV !== "production") {
      checkDuplicateKeys(ch);
    }
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
  } else if (isDef(oldCh)) {
    removeVnodes(elm, oldCh, 0, oldCh.length - 1);
  } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, "");
  }
} else if (oldVnode.text !== vnode.text) {
  nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
  if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}

3. Vinculação de dados


No Vue, a sintaxe de modelo mais básica é a vinculação de dados.

Por exemplo:

<div>{
   
   { message }}</div>

Aqui, uma expressão de interpolação é usada { {}}para vincular uma messagevariável e o desenvolvedor datavincula a variável na instância do Vue:


new Vue({
  data: {
    message: "test"
  }
});

O conteúdo de exibição da página final é <div>test</div>. Então, como isso é feito?


1. Implementação da vinculação de dados


Essa maneira de usar colchetes duplos para vincular variáveis ​​é chamada vinculação de dados.

O processo de ligação de dados não é realmente complicado:
(1), analise a gramática para gerar AST
(2), gere DOM de acordo com o resultado AST
(3), atualize a ligação de dados para o modelo


Esse processo é o que o mecanismo de modelo no Vue está fazendo. Vamos dar uma olhada no trecho de código acima no Vue <div></div>. Podemos capturá-lo por meio do elemento DOM e obter um objeto AST após a análise:


divObj = {
  dom: {
    type: "dom",
    ele: "div",
    nodeIndex: 0,
    children: [{ type: "text", value: "" }]
  },
  binding: [{ type: "dom", nodeIndex: 0, valueName: "message" }]
};

Ao gerar o DOM, adicionamos o messagelistener correto, e quando os dados forem atualizados, encontraremos o nodeIndexvalor de atualização correspondente:


// 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听
function generateDOM(astObject) {
  const { dom, binding = [] } = astObject;
  // 生成DOM,这里假设当前节点是baseDom
  baseDom.innerHTML = getDOMString(dom);
  // 对于数据绑定的,来进行监听更新
  baseDom.addEventListener("data:change", (name, value) => {
    // 寻找匹配的数据绑定
    const obj = binding.find(x => x.valueName == name);
    // 若找到值绑定的对应节点,则更新其值。
    if (obj) {
      baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;
    }
  });
}

// 获取DOM字符串,这里简单拼成字符串
function getDOMString(domObj) {
  // 无效对象返回''
  if (!domObj) return "";
  const { type, children = [], nodeIndex, ele, value } = domObj;
  if (type == "dom") {
    // 若有子对象,递归返回生成的字符串拼接
    const childString = "";
    children.forEach(x => {
      childString += getDOMString(x);
    });
    // dom对象,拼接生成对象字符串
    return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;
  } else if (type == "text") {
    // 若为textNode,返回text的值
    return value;
  }
}

Desta forma, quando a variável é atualizada, podemos messageatualizar automaticamente o conteúdo exibido correspondente através da referência associada à variável. Para saber messagequando uma variável mudou, precisamos monitorar os dados.


2. Monitoramento de atualização de dados


Estilo negrito
Podemos ver que no processo de descrição de código simples acima, o método de monitoramento de dados usado é addEventListener("data:change", Function)o método usado.

No Vue, atualizações de templates, watch, computados, etc. são realizadas quando os dados são atualizados, principalmente por causa das dependências Getter/Setter. E o Vue3.0 usará Proxyo caminho para fazer isso:


Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  
  // getter
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val;
    if (Dep.target) {
      dep.depend();
      if (childOb) {
        childOb.dep.depend();
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
    }
    return value;
  },
  
  
  // setter最终更新后会通知
  set: function reactiveSetter(newVal) {
    const value = getter ? getter.call(obj) : val;
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return;
    }
    if (process.env.NODE_ENV !== "production" && customSetter) {
      customSetter();
    }
    if (getter && !setter) return;
    if (setter) {
      setter.call(obj, newVal);
    } else {
      val = newVal;
    }
    childOb = !shallow && observe(newVal);
    dep.notify();
  }
});

A maioria dos recursos do Vue depende do mecanismo de modelo, incluindo gerenciamento de componentes, gerenciamento de eventos, instância Vue, ciclo de vida etc. Acredito que, desde que você entenda os mecanismos relacionados a AST, DOM virtual e vinculação de dados, você pode leia o código-fonte Vue para entender Mais capacidade não é um problema.


Acho que você gosta

Origin blog.csdn.net/lizhong2008/article/details/130548132
Recomendado
Clasificación