The rendering mechanism of Vue refers to how Vue converts a template from a single file component into an AST(syntax tree), converts the AST into a render function, and finally generates a virtual DOM node (JavaScript object that contains all the information to create the element node) and creates the element node to mount on the page. The basic process is shown as follows:This section begins with the template compilation process to generate the Render function.

Template compilation process

Compiling a template into a rendering function goes through three stages: parsing the template into an AST, traversing the AST to mark static nodes as well as static root nodes, and using the AST to generate the render function. Take the following template as an example:

<div id="app">{{ message }}</div>
Copy the code

First get the template content of the component

var template = options.template;
    if(template) {// Matches templates for string templates and selectorsif (typeof template === 'string') {// the selector matches the template toThe '#'Is the prefix selectorif (template.charAt(0) === The '#') {// Get the innerHTML template = idToTemplate(template); }}else if(template.nodeType) {// Get the innerHTML template of the matched DOM element = template.innerhtml; }else {
        {
        warn('invalid template option:' + template, this);
        }
        return this
    }
    } else ifTemplate = getOuterHTML(el) {// If no template is passed, the root node of the el element is used as the base template by default. }Copy the code

The core process after obtaining the template is as follows:

compileToFunctions(template, {
    outputSourceRange: "development"! = ='production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) ... var compiled = compile(template, options); . var compiled = baseCompile(template.trim(), finalOptions);Copy the code

The above code is creating the compiler, the actual compilation process: parsing, optimizing, and generating the render function, as follows:

var ast = parse(template.trim(), options);
if(options.optimize ! = =false) {
    optimize(ast, options);
}
var code = generate(ast, options);
Copy the code

The template parsing

The real parsing function is parseHTML, which takes template and an options object that contains functions for tags such as start, end, chars, and comment:

parseHTML(template, {
      warn: warn$2, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, outputSourceRange: Start tag start options. OutputSourceRange, / / handle:function start (tag, attrs, unary, startThe $1, end) { ... var element = createASTElement(tag, attrs, currentParent); . }, // To handle the end tag:function end (tag, start, endThe $1) {
        var element = stack[stack.length - 1];
        // pop stack
        stack.length -= 1;
        currentParent = stack[stack.length - 1];
        if (options.outputSourceRange) {
          element.end = endThe $1; } closeElement(element); }, // to process the text chars:functionchars (text, start, end) { ... }, //function comment (text, start, end) {
        // adding anyting as a sibling to the root node is forbidden
        // comments should still be allowed, but ignored
        if (currentParent) {
          var child = {
            type: 3,
            text: text,
            isComment: true
          };
          if(options.outputSourceRange) { child.start = start; child.end = end; } currentParent.children.push(child); }}});return root
}
Copy the code

The core content of the parseHTML function is:

while (html) {
      last = html;
      // Make sure we're not in a plaintext content element like script/style // lastTag || ! isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<'); If (textEnd === 0) {// Comment: if (comment.test(HTML)) {var commentEnd = html.indexof ('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } advance(commentEnd + 3); continue } } // Doctype: var doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); Continue} // End tag: processing End tag... // Start tag: }... } else {// the parent element is script, style, textarea processing logic... } if (html === last) { options.chars && options.chars(html); if (! stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length }); } break } }Copy the code

The basic process is as follows:

  1. html=<div id="app">{{ message }}</div>We get textEnd === 0, and then we know that the HTML starts with a div tag, and we parseStartTag
var startTagMatch = parseStartTag();
ifHandleStartTag (startTagMatch) {// Generate a key-value pair for the start tag attribute;if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
        advance(1);
    }
    continue
}
Copy the code

The return object is

{
    attrs: [" id="app""."id"."="."app", undefined, undefined, index: 0, input: " id="app">{{ message }}<button @click="update"> update < / button > < / div >", groups: undefined, start: 4, end: 13],
    end: 14,
    start: 0,
    tagName: "div",
    unarySlash: ""
}
Copy the code

UnarySlash indicates whether the label is closed. The start function is called after being processed by the handleStartTag function

if(! unary) { stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs,start: match.start, end: match.end }); lastTag = tagName; } // Attrs is generated from the object returned by parseStartTagif (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end);
}
Copy the code

The options.start function is generated after processing

{attrsList: [{
    end: 13
    name: "id"
    start: 5
    value: "app"
}],
attrsMap: {id: "app"},
children: [],
end: 14,
parent: undefined,
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}},
start: 0,
tag: "div".type: 1}
Copy the code

Check if the tag is closed, if it is, closeElement, if not, update currentParent and push the current element onto the stack.

if(! unary) { currentParent = element; stack.push(element); }else {
    closeElement(element);
}
Copy the code

Note that the stack maintains the DOM hierarchy and prevents HTML tag mismatches. 2. The first loop of the while ends and the HTML is truncated as {{message}}, and the textEnd is evaluated as 13, processing text elements

Var text = (void 0), rest = (void 0), next = (void 0);if (textEnd >= 0) {
    rest = html.slice(textEnd);
    while(! endTag.test(rest) && ! startTagOpen.test(rest) && ! comment.test(rest) && ! conditionalComment.test(rest) ) { // <in plain text, be forgiving and treat it as text
    next = rest.indexOf('<', 1);
    if (next < 0) { break }
    textEnd += next;
    rest = html.slice(textEnd);
    }
    text = html.substring(0, textEnd);
}

if (textEnd < 0) {
    text = html;
}

if (text) {
    advance(text.length);
}

if (options.chars && text) {
    options.chars(text, index - text.length, index);
}
Copy the code

The rest = , text = {{message}}, and the options.chars function processes the text

. var children = currentParent.children; .if (text) {
    if (!inPre && whitespaceOption === 'condense') {
        // condense consecutive whitespaces into single space
        text = text.replace(whitespaceREThe $1.' '); } var res; var child; // Text node with variable,type = 2
    if (!inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
        child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text: text
        };
    } else if(text ! = =' '| |! children.length || children[children.length - 1].text ! = =' ') {// Text node without variable,type = 3
        child = {
            type: 3,
            text: text
        };
    }
    if (child) {
        if(options.outputSourceRange) { child.start = start; child.end = end; } children.push(child); }}Copy the code

Via the parseText function

functionParseText (text, delimiters) {// Match {{message}} var tagRE = delimiters? buildRegex(delimiters) : defaultTagRE;if(! tagRE.test(text)) {return
    }
    var tokens = [];
    var rawTokens = [];
    var lastIndex = tagRE.lastIndex = 0;
    var match, index, tokenValue;
    while((match = tagRE.exec(text))) { index = match.index; // Push text tokens // Add {{text content to tokensif(index > lastIndex) { rawTokens.push(tokenValue = text.slice(lastIndex, index)); tokens.push(JSON.stringify(tokenValue)); Var exp = parseFilters(match[1].trim());} // tag token // add {{message}} to _s(message) var exp = parseFilters(match[1].trim()); tokens.push(("_s(" + exp + ")"));
      rawTokens.push({ '@binding': exp }); lastIndex = index + match[0].length; } // Add the text on the right of {{to tokensif (lastIndex < text.length) {
      rawTokens.push(tokenValue = text.slice(lastIndex));
      tokens.push(JSON.stringify(tokenValue));
    }
    return {
      expression: tokens.join('+'),
      tokens: rawTokens
    }
  }
Copy the code

Return after processing

{
    expression: "_s(message)",
    tokens: [{@binding: "message"}}]Copy the code

Push this node into children to generate

[
    {
        end: 27,
        expression: "_s(message)",
        start: 14,
        text: "{{ message }}",
        tokens: [{@binding: "message"}].type: 2}]Copy the code
  1. In the third loop, the HTML is</div>The computed textEnd = 0 matches the closing tag
var endTagMatch = html.match(endTag);
if (endTagMatch) {
    var curIndex = index;
    advance(endTagMatch[0].length);
    parseEndTag(endTagMatch[1], curIndex, index);
    continue
}
Copy the code

EndTagMatch for

[
    "</div>"."div", groups: undefined, index: 0, input: "</div>"
]
Copy the code

The stack is traversed to find the start tag matching the current end tag, which is handled by the options.end function

var element = stack[stack.length - 1];
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
    element.end = endThe $1;
}
closeElement(element);
Copy the code

When closeElement is popped, currentParent is the first element on the top of the stack, and element end is updated.

// Handle attributes of elements, such as ref, slot, is, attrsif (!inVPre && ! element.processed) { element = processElement(element, options); }... // Confirm the relationship between father and sonif(currentParent && ! element.forbidden) {if (element.elseif || element.else) {
        processIfConditions(element, currentParent);
    } else {
    if (element.slotScope) {
        // scoped slot
        // keep it in the children list so that v-else(-if) conditions can
        // find it as the prev node.
        var name = element.slotTarget || '"default"'; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element; } / / push the current element into the current parent node of the children in the array, the parent of the current element update currentParent. Children. Push (element); element.parent = currentParent; }}Copy the code

The generated AST is

{
    attrs: [{
        dynamic: undefined
        end: 13
        name: "id"
        start: 5
        value: ""app""
    }]
    attrsList: [{name: "id", value: "app", start: 5, end: 13}]
    attrsMap: {id: "app"}
    children: [{
        end: 27,
        expression: "_s(message)",
        start: 14,
        text: "{{ message }}",
        tokens: [{@binding: "message"}].type: 2
    }]
    end: 33
    parent: undefined
    plain: false
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
    start: 0
    tag: "div"
    type: 1}Copy the code

Finally, stack and lastTag are updated, stack=[],lastTag=’div’, and the loop ends, returning root as the generated AST.

The optimizer

And then we go into the optimization phase,

optimize(ast, options); // Generate static nodes and static root nodesfunction optimize (root, options) {
    if(! root) {return }
    isStaticKey = genStaticKeysCached(options.staticKeys || ' ');
    isPlatformReservedTag = options.isReservedTag || no;
    // first pass: mark all non-static nodes.
    markStaticThe $1(root);
    // second pass: mark static roots.
    markStaticRoots(root, false);
  }
Copy the code

First mark the AST as a non-static node

function markStaticThe $1(node) {// Check whether the node is a static node.if (node.type === 1) {
      // do not make component slot content static. this avoids
      // 1. components not able to mutate slot nodes
      // 2. static slot content fails for hot-reloading
      if(! isPlatformReservedTag(node.tag) && node.tag ! = ='slot' &&
        node.attrsMap['inline-template'] == null
      ) {
        return
      }
      for(var i = 0, l = node.children.length; i < l; i++) { var child = node.children[i]; // Recursive child nodes mark the static node markStaticThe $1(child); // If the child node is marked, check whether the child node is static. If it is not, the parent node cannot be static. In this case, set the parent node to static =false
        if(! child.static) { node.static =false; }}... }}Copy the code

Function that determines whether it is a static node

function isStatic (node) {
    if(node.type === 2) {// expression, expressionreturn false
    }
    if(node.type === 3) {// text Text nodereturn true} // If the element node does not have v-pre, both must be metreturn!!!!! (node.pre || ( ! node.hasBindings && // no dynamic bindings ! node.if && ! node.for && // not v-if or v-for or v-else ! IsBuiltInTag (node.tag) && // Not a built-in tag, such as slot, component isPlatformReservedTag(node.tag) && // Not a component, <list></list> is not a reserved tag! IsDirectChildOfTemplateFor (node) && / / parent node of the current node can't be with v - for instruction of the template tag Object. Keys (node). Every (isStaticKey) / /))}Copy the code

All static root nodes are then identified and marked

// second pass: mark static roots.
markStaticRoots(root, false);
Copy the code

Specific for

function markStaticRoots (node, isInFor) {
    if (node.type === 1) {
      if (node.static || node.once) {
        node.staticInFor = isInFor;
      }
      // For a node to qualify as a static root, it should have children that
      // are not just static text. Otherwise the cost of hoisting out will
      // outweigh the benefits and itBetter off to just always render it fresh. If (node.static && node.children. Length &&! ( node.children.length === 1 && node.children[0].type === 3 )) { node.staticRoot = true; return } else { node.staticRoot = false; } if (node.children) {for (var I = 0, l = node.children. Length; i < l; i++) { markStaticRoots(node.children[i], isInFor || !! node.for); }}... }}Copy the code

The AST returned is:

{
    attrs: [{
        dynamic: undefined
        end: 13
        name: "id"
        start: 5
        value: ""app""
    }]
    attrsList: [{name: "id", value: "app", start: 5, end: 13}]
    attrsMap: {id: "app"}
    children: [{
        end: 27,
        expression: "_s(message)",
        start: 14,
        static: false,
        text: "{{ message }}",
        tokens: [{@binding: "message"}].type: 2
    }]
    end: 33
    parent: undefined
    plain: false,
    rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
    start: 0,
    static: false,
    staticRoot: false,
    tag: "div".type: 1}Copy the code

Code generator

The ast generates the render function. Different nodes generate the render function in different ways. The specific code is as follows:

function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }
Copy the code

If the ast does not exist, the default is _c(‘div’). _c indicates that the ast does not exist

vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
Copy the code

If yes, the AST is processed

function genElement (el, state) {
    if(el.parent) { el.pre = el.pre || el.parent.pre; } // Handle static nodesif(el.staticRoot && ! el.staticProcessed) {return genStatic(el, state)
    } else if(el.once && ! El.onceprocessed) {// Process the V-once instructionreturn genOnce(el, state)
    } else if(el.for && ! El.forprocessed) {// Handles v-for directivesreturn genFor(el, state)
    } else if(el.if && ! El.ifprocessed) {// Process V-if instructionsreturn genIf(el, state)
    } else if (el.tag === 'template'&&! el.slotTarget && ! State.pre) {// Process the template tagreturn genChildren(el, state) || 'void 0'
    } else if (el.tag === 'slot') {// Handle slot built-in componentsreturn genSlot(el, state)
    } else {
      // component or element
      var code;
      if{// Handle component code = genComponent(el.component.el.component.el, state); }else{ var data; // Process elementsif(! el.plain || (el.pre && state.maybeComponent(el))) { data = genData$2(el, state); } var children = el.inlineTemplate? null : genChildren(el, state,true);
        code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : ' ') + (children ? ("," + children) : ' ') + ")";
      }
      // module transforms
      for (var i = 0; i < state.transforms.length; i++) {
        code = state.transforms[i](el, code);
      }
      return code
    }
  }
Copy the code

Enter the genData$2 function as

function genData$2 (el, state) {
    var data = '{';

    // directives first.
    // directives may mutate the el's other properties before they are generated. var dirs = genDirectives(el, state); if (dirs) { data += dirs + '.'; } // key if (el.key) { data += "key:" + (el.key) + ","; } // ref if (el.ref) { data += "ref:" + (el.ref) + ","; } if (el.refInFor) { data += "refInFor:true,"; } // pre if (el.pre) { data += "pre:true,"; } // record original tag name for components using "is" attribute if (el.component) { data += "tag:\"" + (el.tag) + "\", "; } // module data generation functions for (var i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el); } // Attributes, update attrs attribute form if (el.attrs) {data += "attrs:" + (genProps(el.attrs)) + ","; } // DOM props if (el.props) { data += "domProps:" + (genProps(el.props)) + ","; } // event handlers if (el.events) { data += (genHandlers(el.events, false)) + ","; } if (el.nativeEvents) { data += (genHandlers(el.nativeEvents, true)) + ","; } // slot target // only for non-scoped slots if (el.slotTarget && ! el.slotScope) { data += "slot:" + (el.slotTarget) + ","; } // scoped slots if (el.scopedSlots) { data += (genScopedSlots(el, el.scopedSlots, state)) + ","; } // component v-model if (el.model) { data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) +  ",expression:" + (el.model.expression) + "},"; } // inline-template if (el.inlineTemplate) { var inlineTemplate = genInlineTemplate(el, state); if (inlineTemplate) { data += inlineTemplate + ","; } } data = data.replace(/,$/, ''+'}'; // v-bind dynamic argument wrap // v-bind with dynamic arguments must be applied using the same v-bind object // merge helper so that class/style/mustUseProp attrs are handled correctly. if (el.dynamicAttrs) { data = "_b(" + data + ",\"" +  (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")"; } // v-bind data wrap if (el.wrapData) { data = el.wrapData(data); } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data); } return data }Copy the code

The function is to concatenate strings, assign a ‘{‘ to data, concatenate the attributes of the node to data, add a ‘}’, and return a complete data:

"{attrs:{"id":"app"}}"
Copy the code

The children of the element node are then processed

genChildren(el, state, true);
Copy the code

The specific function is:

function genChildren (
    el,
    state,
    checkSkip,
    altGenElement,
    altGenNode
  ) {
    var children = el.children;
    if (children.length) {
        ...
      var gen = altGenNode || genNode;
      return ("[" + (children.map(function (c) { return gen(c, state); }).join(', ')) + "]" + (normalizationTypeThe $1 ? ("," + normalizationTypeThe $1) : ' '))}}Copy the code

Generate different node strings according to different child node types and splicing them together. The genNode function is:

function genNode (node, state) {
    if (node.type === 1) {
      return genElement(node, state)
    } else if (node.type === 3 && node.isComment) {
      return genComment(node)
    } else {
      return genText(node)
    }
  }
Copy the code

Recursive child nodes to generate child nodes, finally spliced together to return. Processing of text nodes

function genText (text) {
    return ("_v(" + (text.type === 2
      ? text.expression // no need for () because already wrapped in _s()
      : transformSpecialNewlines(JSON.stringify(text.text))) + ")")}Copy the code

Express for dynamic text, text for static text, put the text in _v as argument, the generated code (render function) is:

"_c('div',{attrs:{"id":"app"}},[_v(_s(message))])"
Copy the code

Finally by

updateComponent = function () {
    vm._update(vm._render(), hydrating);
};
Copy the code

VNode is generated by the vm._render() function call in

 vnode = render.call(vm._renderProxy, vm.$createElement);
Copy the code

Call the generated render function, pointing to vm._renderProxy, and the with statement sets the scope of the code to a specific scope, this.

with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}
Copy the code

Where _s stands for toString(), and the generated VNode is

{
    asyncFactory: undefined
    asyncMeta: undefined
    children: [
    {
        asyncFactory: undefined,
        asyncMeta: undefined,
        children: undefined,
        componentInstance: undefined,
        componentOptions: undefined,
        context: undefined,
        data: undefined,
        elm: undefined,
        fnContext: undefined,
        fnOptions: undefined,
        fnScopeId: undefined,
        isAsyncPlaceholder: false,
        isCloned: false,
        isComment: false,
        isOnce: false,
        isRootInsert: true,
        isStatic: false,
        key: undefined
        ns: undefined,
        parent: undefined,
        raw: false,
        tag: undefined,
        text: "Hello Wolrd",
        child: undefined
    }],
    componentInstance: undefined
    componentOptions: undefined
    context: Vue {_uid: 0, _isVue: true.$options: {... }, _renderProxy: Proxy, _self: Vue,... } data: {attrs: {id:'app'}}
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isAsyncPlaceholder: false,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "div",
    text: undefined,
    child: undefined
}
Copy the code

At this point, the compilation process is complete, and the rendering process of VNode is described in the next section.