VUE study notes (3) Detailed explanation of Vue rendering process


Rendering a piece of content in Vue will have the following steps and processes:

The first step is to parse the grammar and generate AST

The second step is to complete the data initialization according to the AST result

The third step is to generate virtual DOM according to the AST result and DATA data binding

The fourth step is to insert the real DOM generated by the virtual DOM into the page for page rendering.


So how to understand this process?


1. Parsing grammar to generate AST


The AST syntax tree is actually an abstract syntax tree (Abstract Syntax Tree), which refers to mapping the statements in the source code to each node in the tree by building a syntax tree.

The DOM structure tree is also a kind of AST, which parses the HTML DOM syntax and generates the final page.


Let's look at this process in detail:


1. Capture syntax

In the process of generating AST, the principle of the compiler will be involved, and it will go through the following process:


(1), syntax analysis


The task of grammatical analysis is to combine word sequences into various grammatical phrases on the basis of lexical analysis. Such as: procedures, statements, expressions, etc. The parser judges whether the source program is structurally correct, such as instructions like v-if` / v-for, there are also custom DOM tags like ``, and simplified binding syntax like `click`/`props. They need to be parsed out one by one and processed accordingly.

(2), semantic analysis


Semantic analysis is to check whether there are semantic errors in the source program, and collect type information for the code generation phase. General type checking will also be carried out in this process. If we bind a variable or event that does not exist, or use an undefined custom component, etc., an error will be reported at this stage.


(3) Generate AST


In Vue, syntax analysis and semantic analysis are basically processed in a regular way. Generating AST is actually processing the parsed elements, instructions, attributes, parent-child node relationships, etc. to obtain an AST object. The following is a simplified version source code:


/**
 *  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. DOM element capture

If we need to capture an <div>element, generate another <div>element.


There is a template, we can capture it:


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

After capturing we can get an object like this:


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" }]
          }
        ]
      }
    ]
  }
};


This object holds some information we need:

  • Which variables need to be bound in the HTML element, because the content of the node needs to be updated when the variable is updated.

  • In what way to splice, whether there are logical instructions, such as v-if, v-foretc.

  • Which nodes are bound to what listening events, and whether they match some commonly used event capability support

Vue will generate a piece of executable code based on the AST object, let's take a look at the implementation of this part:


// 生成一个元素
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. Template engine empowerment


Through the above introduction, you may say that it is originally one <div>, and an object is generated through AST, and finally one is generated <div>. Isn't this a redundant step?


In fact, we can achieve some functions in this process:

  • Exclude invalid DOM elements and report errors during the build process

  • When using custom components, it can be matched

  • Data binding, event binding and other functions can be easily realized

  • Pave the way for the virtual DOM Diff process

  • HTML Escaping to Prevent XSS Vulnerabilities


A general-purpose template engine can handle many inefficient and repetitive tasks, such as browser compatibility, unified management and maintenance of global events, virtual DOM mechanism for template updates, and tree organization management components. In this way, after we know what the template engine does, we can distinguish the capabilities provided by the Vue framework from the logic we need to handle by ourselves, and we can focus more on business development.


2. Virtual DOM


Virtual DOM can be roughly divided into three processes:

The first step is to simulate the DOM tree with JS objects to obtain a virtual DOM tree.

The second step is to generate a new virtual DOM tree when the page data changes, and compare the differences between the old and new virtual DOM trees.

In the third step, the diff is applied to the real DOM tree.


1. Simulate the DOM tree with JS objects

Why use virtual DOM? Because a real DOM element is very large and has many attribute values, but in fact we will not use all of them, usually including node content, element position, style, node addition and deletion and other methods. Therefore, we can greatly reduce the amount of calculation for comparing differences by using JS objects to represent DOM elements.


Let's take a look at the VNode source code, there are only about 20 attributes below:


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 the difference between the old and new virtual DOM trees


In virtual DOM, difference comparison is a critical step. When the state changes, a new object tree is reconstructed. Then compare the new tree with the old tree, and record the differences between the two trees. Such differences need to be documented:

  • Need to replace the original node
  • Move, delete, add child nodes
  • Modify the properties of the node
  • For text nodes the text content changes

In the figure below, we compare the two DOM trees, and the differences are:

  • The p element inserts a span element child node

  • The original text node is moved below the child node of the span element


insert image description here


3. Apply the difference to the real DOM tree


Through the previous examples, we know that to apply the difference records to the real DOM tree, some operations are required, such as node replacement, movement, deletion, and text content changes.


How to do DOM Diff in Vue? Just look at this code and get a feel. Although many functions in the code are not posted, in fact, you can roughly understand what they do by looking at the function name, such as , , , updateChildrenand addVnodesso removeVnodeson 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. Data Binding


In Vue, the most basic template syntax is data binding.

For example:

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

Here, an interpolation expression is used { {}}to bind a messagevariable, and the developer databinds the variable in the Vue instance:


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

The final page display content is <div>test</div>. So how is this done?


1. Implementation of data binding


This way of using double braces to bind variables is called data binding.

The process of data binding is actually not complicated:
(1), parse the grammar to generate AST
(2), generate DOM according to the AST result
(3), update the data binding to the template


This process is what the template engine in Vue is doing. Let's take a look at the above code snippet in Vue <div></div>. We can capture it through the DOM element and get such an AST object after parsing:


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

When we generate the DOM, we add the right messagelistener, and when the data is updated, we will find the corresponding nodeIndexupdate value:


// 假设这是一个生成 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;
  }
}

messageIn this way, when the variable is updated, we can automatically update the corresponding displayed content through the reference associated with the variable. To know messagewhen a variable has changed, we need to monitor the data.


2. Data update monitoring


Bold style
We can see that in the above simple code description process, the data monitoring method used is addEventListener("data:change", Function)the method used.

In Vue, template updates, watch, computed, etc. are performed when data is updated, mainly because of dependencies Getter/Setter. And Vue3.0 will use Proxythe way to do it:


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

Most of the capabilities in Vue depend on the template engine, including component management, event management, Vue instance, life cycle, etc. I believe that as long as you understand the mechanisms related to AST, virtual DOM, and data binding, you can read the Vue source code to understand More capacity is not a problem.


Guess you like

Origin blog.csdn.net/lizhong2008/article/details/130548132