Blunt a cup of American ☕️, read and compile true classics, or not fast zai?

🍪 (for example, ☕️ and 🍪 are more suitable! The whole article will be analyzed around this DEMO) :

<div id="app">
    <! This is a comment node -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
    <div class="abc"></div>
</div>
Copy the code
const Child = Vue.extend({
  name: 'Child'.props: {
    name: String.age: Number
  },

  render (h) {
    return h('div'.null, [
      h('span'.null.this.name),
      h('span'.null.this.age),
    ])
  }
})

new Vue({
  el: '#app'.components: {
    Child
  },

  data () {
    return {
      isShow: true.inputValue: '123123'}; }})Copy the code

🍪 contains nodes for template compilation — comment nodes, start tags, props properties, DOM properties, and self-closing tags.

Pick up :coffee:, let’s see where template compilation starts. Recall the Vue execution process, where there is a judgment of whether render exists in options. If you write the render function by hand, such as the Child component in 🍪, there is no need to go through the template compilation process; If SFC or template is written, the render function is generated by template compilation.

This section of code is found in SRC \platforms\web\entry-runtime-with-compiler.js

/** * mount component, compile with template */
Vue.prototype.$mount = function (el? : string | Element, hydrating? : boolean// Related to server rendering, not considered
) :Component {

  // Mount the DOM. Query makes some judgments about it. The DOM returns directly, and the string fetches the DOM via querySelector
  el = el && query(el)

  // Configuration information
  const options = this.$options

  // resolve template/el and convert to render function
  // There is no render function, process the template content, convert to render function
  if(! options.render) {/ /... Omits part of getting the template string
    }
    if (template) {
      // ...
      // Execute template compilation and the final result returns render and staticRenderFns
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      // ...}}/* Call const mount = vue.prototype.$mount saved without compilation */
  return mount.call(this, el, hydrating)
}
Copy the code

As you can see, the result of template compilation is the render and staticRenderFns function. 😵 doesn’t only need render?

To get compileToFunctions, do the following 5 steps:

  1. SRC \ platforms \ web \ compiler \ index of js createCompiler (baseOptions);

  2. SRC \ compiler \ the create – compiler. Js createCompilerCreator;

  3. The SRC \ compiler \ to – function. Js createCompileToFunctionFn (* compile * : function);

  4. const compiled = baseCompile(template, finalOptions)
    Copy the code
  5. export const createCompiler = createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
      // Build the AST
      const ast = parse(template.trim(), options)
    
      if(options.optimize ! = =false) {
        /** * Optimize AST * optimization goal: generate template AST to detect static subtrees that do not require DOM changes. * Once these static trees are detected, we can do the following: * 1. Make them constants so that we no longer need to create new nodes every time we re-render. * 2. The patch process is directly skipped. * /
        optimize(ast, options)
      }
    
      // Generate the required code according to the AST (internal includes render and staticRenderFns)
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })
    Copy the code

Extend much of the configuration on baseOptions before performing compilation. At the same time, at the beginning of the compilation, it determines the current compilation environment, and then updates with this compilation environment, so it also makes the compiler cache.

Ready to go, step into the parsing phase.

parse

This stage can be summarized as “the process of matching the start tag, attribute, note, and close tag in string with various regular expressions, and finally producing the AST”.

First of all, amway has a small regex tool: Regex101. Every section of the page is extremely useful, too delicious.

  • Have detailed regular explanation;
  • Real-time input to view the matching results;
  • If you forget the basics of regex, there’s a quick reference module;
  • Can output all matched grouping results;
  • Keep the test results and synchronize them to other partners via the link (click on ⚠️ to see the regees).

To begin, let’s look at a function advance that will be called regardless of any match:

function advance (n) {
    index += n
    html = html.substring(n)
}
Copy the code

Unambiguably, the result of a match is removed from the string and the HTML is reset.

In this section, we will look at how the AST is generated using the template in 🍪 :

<div id="app">
    <! This is a comment node -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
  	<div class="abc"></div>
</div>
Copy the code

Follow the template above to explain the matching process step by step:

  1. Start tag

    :
    function parseStartTag () {
      const start = html.match(startTagOpen)
      if (start) {
        const match = {
          tagName: start[1].attrs: [].start: index
        }
        advance(start[0].length)
        let end, attr
        while(! (end = html.match(startTagClose)) && (attr = html.match(attribute))) { advance(attr[0].length)
          match.attrs.push(attr)
        }
        if (end) {
          match.unarySlash = end[1]
          advance(end[0].length)
          match.end = index
          return match
        }
      }
    }
    Copy the code
    1. Match the start tag name, at which point a match object is created;

    2. Match attributes in the start tag and add the result of attribute match to attrs in match.

    3. Match the end > character of the start tag, and record the matching grouping information and the end position into mate.unaryslash and mate.end, respectively.

    4. The next step is to call handleStartTag to match:

      function handleStartTag (match) {
        const tagName = match.tagName
        const unarySlash = match.unarySlash
      
        if (expectHTML) {
          if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
            parseEndTag(lastTag)
          }
          if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
            parseEndTag(tagName)
          }
        }
      
        // Check whether the tag is unary. The input in the example will be true, see later
        constunary = isUnaryTag(tagName) || !! unarySlash// Walk through all attrs
        const l = match.attrs.length
        const attrs = new Array(l)
        for (let i = 0; i < l; i++) {
          const args = match.attrs[i]
          // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
          if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('" "') = = = -1) {
            if (args[3= = =' ') { delete args[3]}if (args[4= = =' ') { delete args[4]}if (args[5= = =' ') { delete args[5]}}const value = args[3] || args[4] || args[5] | |' '
          const shouldDecodeNewlines = tagName === 'a' && args[1= = ='href'
          ? options.shouldDecodeNewlinesForHref
          : options.shouldDecodeNewlines
          
          // Encode attribute values, XSS attack
          attrs[i] = {
            name: args[1].value: decodeAttr(value, shouldDecodeNewlines)
          }
        }
      
        // Push the tag name and other information into the stack without unary tags, and assign the current tag name to lastTag, which is used for subsequent tag stack matching
        if(! unary) { stack.push({tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
          lastTag = tagName
        }
      	
        // Call start to generate ASTElement
        if (options.start) {
          options.start(tagName, attrs, unary, match.start, match.end)
        }
      }
      Copy the code

      HandleStartTag determines whether the current tag is unary and then handles the value on attrs, such as encoding. If it is not a unary tag, place the tag information in the stack and call start to generate rootElement:

      start (tag, attrs, unary) {
        // ...
      
        / / create ASTElement
        let element: ASTElement = createASTElement(tag, attrs, currentParent)
      
        // ...
      
        // apply pre-transforms
        for (let i = 0; i < preTransforms.length; i++) {
          element = preTransforms[i](element, options) || element
        }
        // ...
      
        if(! root) { root = element// Check. Do not use slot, template, or v-for for root nodes, as these can produce multiple root nodes
          checkRootConstraints(root)
        } else {
          // ...
        }
        
        // ...
        // Instead of unary tags, push the current ASTElement into the stack
        if(! unary) { currentParent = element stack.push(element) }else {
          closeElement(element)
        }
      },
      Copy the code

      For rootElement in 🍪, it is relatively simple to paste the result diagram directly without any other logical branches:

    At this point the start tag

    is resolved. The HTML is now processed in advance and looks like this:
        <! This is a comment node -->
        <Child name="yjc" :age="12" v-if="isShow"></Child>
        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    Copy the code
  2. Before parsing the comment node, we can see that there are a series of whitespace characters. This is also a simple way to look at the current textEnd (🍪 < position) and determine if it is greater than 0.

    let text, rest, next
    
    // In the demo, this is 4, which is greater than 0
    if (textEnd >= 0) {
      
      /** * go straight here, rest is * <! <child name="yjc" :age="12" V-if ="isShow"></child> <input type="text" V-model ="inputValue"> <div class="abc"></div> </div> */
      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)
      advance(textEnd)
    }
    Copy the code

    This time the callback is options.chars:

    chars (text: string) {
    
      // ...
      const children = currentParent.children
      text = inPre || text.trim()
        ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
      // only preserve whitespace if its not right after a starting tag
      : preserveWhitespace && children.length ? ' ' : ' '
      if (text) {
        let res
        if(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) {
          children.push({
            type: 2.expression: res.expression,
            tokens: res.tokens,
            text
          })
        } else if(text ! = =' '| |! children.length || children[children.length -1].text ! = =' ') {
          children.push({
            type: 3,
            text
          })
        }
      }
    },
    Copy the code

    The whitespace character comes in full circle and is back to the main flow of parseHTML because trim is gone. :sunglasses:

  3. Next comes a comment node

    if (comment.test(html)) 
      // Computes the end position of the comment node
      const commentEnd = html.indexOf('-->')
    
      if (commentEnd >= 0) {
        
        // Whether to save the comment node
        if (options.shouldKeepComment) {
          options.comment(html.substring(4, commentEnd))
        }
        
        // Step by step, remove comment nodes from HTML
        advance(commentEnd + 3)
        continue}}Copy the code
    1. Matches the beginning of the comment node;

    2. Determine if you want to keep the comment node (⚠️ this configuration is read from the configuration, you can configure it as follows), then proceed with the HTML template, otherwise the AST will add a comment text node:

      new Vue({
        el: '#app'.components: {
          Child
        },
      
        // Note: This can be configured to save comment information
        comments: true,
      
        data () {
          return {
            isShow: true.inputValue: ' '}; }})Copy the code

    After processing the comment node, the template becomes:

        <Child name="yjc" :age="12" v-if="isShow"></Child>
        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    Copy the code
  4. To process whitespace, repeat Step 2.

  5. :

    1. The match result after processing is as follows:

    2. Then execute the options.start function, the same logic as the div above is not described here. A Child differs from a div in several ways:

      1. Child has a v-if directive. GetAndRemoveAttr removes the V-if attribute from the attrsList and adds if and ifCondition to the Child AST.

        function processIf (el) {
          // Get the value of the V-if directive, in this case isShow
          const exp = getAndRemoveAttr(el, 'v-if')
          if (exp) {
            el.if = exp
            addIfCondition(el, {
              exp: exp,
              block: el
            })
          } else {
            if (getAndRemoveAttr(el, 'v-else') != null) {
              el.else = true
            }
            const elseif = getAndRemoveAttr(el, 'v-else-if')
            if (elseif) {
              el.elseif = elseif
            }
          }
        }
        Copy the code
      2. function processAttrs (el) {
          // Get the property list
          const list = el.attrsList
          let i, l, name, rawName, value, modifiers, isProp
          for (i = 0, l = list.length; i < l; i++) {
            name = rawName = list[i].name
            value = list[i].value
        
            /* Matches v-, @, and:, and handles the special attribute */ of el
            if (dirRE.test(name)) {
              // mark element as dynamic
              /* Marks this ele as dynamic */
              el.hasBindings = true
              // modifiers
              {b: true, c: true, d:true}*/
              modifiers = parseModifiers(name)
              if (modifiers) {
                /* get the first level, for example A.B.C.D get a, which is the operation above to get all the children, this one to get the first level */
                name = name.replace(modifierRE, ' ')}/* If the attribute is v-bind's */
              if (bindRE.test(name)) { // v-bind
                name = name.replace(bindRE, ' ')
                value = parseFilters(value)
                isProp = false
                if (modifiers) {
                  / * * * * https://cn.vuejs.org/v2/api/#v-bind here to deal with v - bind modifier * /
                  /*.prop - is used to bind DOM properties. * /
                  if (modifiers.prop) {
                    isProp = true
                    /* Change the string originally concatenated with - to hump aaA-bbb-ccc => aaaBbbCcc*/
                    name = camelize(name)
                    if (name === 'innerHtml') name = 'innerHTML'
                  }
                  /*. Camel - (2.1.0+) converts the kebab-case feature name to camelCase. (supported since 2.1.0)*/
                  if (modifiers.camel) {
                    name = camelize(name)
                  }
                  //.sync (2.3.0+) syntax sugar, which expands into a V-on listener that updates the binding value of the parent component.
                  if (modifiers.sync) {
                    addHandler(
                      el,
                      `update:${camelize(name)}`,
                      genAssignmentCode(value, `$event`))}}if(isProp || ( ! el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) {/* Put the property in the props property of el */
                  addProp(el, name, value)
                } else {
                  /* Put the attribute into the attr attribute of el */
                  addAttr(el, name, value)
                }
              } else if (onRE.test(name)) { // v-on
                /* Put the attribute into the attr attribute of el */
                name = name.replace(onRE, ' ')
                addHandler(el, name, value, modifiers, false, warn)
              } else { // normal directives
                /* Remove @, :, v-*/
                name = name.replace(dirRE, ' ')
                // parse arg
                const argMatch = name.match(argRE)
                /* Fun ="functionA"*/
                const arg = argMatch && argMatch[1]
                if (arg) {
                  name = name.slice(0, -(arg.length + 1))}/* Join the parameters to the EL directives */
                addDirective(el, name, rawName, value, arg, modifiers)
                if(process.env.NODE_ENV ! = ='production' && name === 'model') {
                  checkForAliasModel(el, value)
                }
              }
            } else {
              // ...
              /* Put the attribute into the attr attribute of el */
              addAttr(el, name, JSON.stringify(value))
              // #6887 firefox doesn't update muted state if set via attribute
              // even immediately after element creation
              if(! el.component && name ==='muted' &&
                  platformMustUseProp(el.tag, el.attrsMap.type, name)) {
                addProp(el, name, 'true')}}}}Copy the code

        ParseAttrs iterates over attrsList, handling various attributes, such as V-bind, @, value expressions, modifiers, etc., rather than executing each one logically. Just look at 🍪 where name= “yjc” and :age=”12″. For plain text, add attrs to AST by addAttr(el, name, json.stringify (value)); The latter is removed by dirRE and bindRE: the symbol is then added to attrs.

      3. When compiling Child, the root node exists, and the relation between parent and children is constructed:

        When parsed to Child, currentParent points to the div node
        if(currentParent && ! element.forbidden) {if (element.elseif || element.else) {
            processIfConditions(element, currentParent)
          } else if (element.slotScope) { // scoped slot
            currentParent.plain = false
            const name = element.slotTarget || '"default"'; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element }else {
            // add the children field to the Child AST
            currentParent.children.push(element)
            // Parent of Child AST is assigned to div AST
            element.parent = currentParent
          }
        }
        Copy the code

    Result after processing the Child node:

    </Child>
        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    Copy the code
  6. Closing the tag

    1. Matches are lazily made with a closed tag re, which is a slash added to the opening tag re.

    2. Then advance is used to remove the closing tag;

    3. Update the tag and AST stack with parseEndTag and options.end;

        function parseEndTag (tagName, start, end) {
          let pos, lowerCasedTagName
          if (start == null) start = index
          if (end == null) end = index
      
          if (tagName) {
            lowerCasedTagName = tagName.toLowerCase()
          }
      
          // Find the closest opened tag of the same type
          if (tagName) {
            for (pos = stack.length - 1; pos >= 0; pos--) {
              if (stack[pos].lowerCasedTag === lowerCasedTagName) {
                break}}}else {
            // If no tag name is provided, clean shop
            pos = 0
          }
      
          if (pos >= 0) {
            // Close all the open elements, up the stack
            for (let i = stack.length - 1; i >= pos; i--) {
              // ...
              if (options.end) {
                options.end(stack[i].tag, start, end)
              }
            }
      
            // Set the array length to the current position, extract the last label on the stack, and update lastTag
            stack.length = pos
            lastTag = pos && stack[pos - 1].tag
          } 
          // ...
        }
      Copy the code

      ParseEndTag compares the tag to the uppermost element on the stack, which is why

      does not report a tag mismatch. Then call options.end to update the AST Stack:

      end () {
        // handle trailing whitespace
        const element = stack[stack.length - 1]
        const lastNode = element.children[element.children.length - 1]
        if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
          element.children.pop()
        }
        // The last AST message pops out of the stack and updates the current currentParent node
        stack.length -= 1
        currentParent = stack[stack.length - 1]
        
        // Updated the status of inVPre and inPrV, 🌰 is not required to know
        closeElement(element)
      },
      Copy the code

    Result after processing :

        <input type="text" v-model="inputValue" />
    		<div class="abc"></div>
    </div>
    Copy the code
  7. For the next input node, we’ll look at v-model and self-closing tags:

    1. ParseStartTag is the same as before;

    2. Perform to handleStartTag const unary = isUnaryTag (tagName) | |!! When unarySlash, this returns true; Self-closing labels do not need to be pushed because they do not match closing labels. Start;

    3. When generating an AST, 90% of the process is the same. V-model =”inputValue” will call addDirective when processElement -> processAttrs is executed:

      export function addDirective (el: ASTElement, name: string, rawName: string, value: string, arg: ? string, modifiers: ? ASTModifiers) {
        (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
        el.plain = false
      }
      Copy the code

      The CACHE array is added to the AST node and both Model and inputValue are advanced into that array. The AST generated by the final input is as follows:

    After parsing the input node, the HTML is left with:

    		<div class="abc"></div>
    </div>
    Copy the code
  8. The remaining templates are as simple as repeating the previous steps. I’m not going to write it here. This node is a setup for optimize. 😁 😁)

  9. When only “” is left in the HTML, parseEndTag is eventually executed again to clean up the remaining tags on the stack.

summary

The parse process matches the Template string with regular expressions (complex re’s are parsed with the Help of the Regex101 tool, which can sort out matching scenarios) to match start tags, close tags, comment nodes, tag attributes, and so on. Add a label stack matching procedure:

The respective callback functions are then called during the matching process to generate the AST. After each node is parsed, advance is advanced. Finally parses the entire string, returning the AST to the next segment, optimize. Before we start analyzing Optimize, there is one detail we haven’t covered in generating the AST, the Type field in the AST. The meaning of type (⚠️ magic number carefully used to reduce the cost of understanding) :

  • 1 is a normal element;

  • 2 represents an expression.

  • 3 indicates plain text.

optimize

Objectives of this section:

  1. What is the purpose of optimization?
  2. What is a static node?
  3. What criteria does a node meet to be a static root node?

With the above three problems, start to take the “optimization” of the truth. At the entrance there is a judgment:

  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
Copy the code

Is there any other situation where you don’t optimize? In the case of the Web, this is undefined, undefined! == false is true, so it needs to be optimized. For WEEx, options.optimize is explicitly false. See optimize:

/** * Goal of the optimizer: walk the generated template AST tree * and detect sub-trees that are purely static, i.e. parts of * the DOM that never needs to change. * * Once we detect these sub-trees, we can: * * 1. Hoist them into constants, so that we no longer need to * create fresh nodes for them on each re-render; * 2. Completely skip them in the patching process. */
export function optimize (root: ? ASTElement, options: CompilerOptions) {
  if(! root)return
  isStaticKey = genStaticKeysCached(options.staticKeys || ' ')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)}Copy the code

The optimize note already answers the first question:

  • One is to promote them to static constants so that no new static nodes need to be created each time you re-render.
  • The second is thepatchYou can skip them altogether;

markStatic

See the first main process markStatic(root) :

function markStatic (node: ASTNode) {
  node.static = isStatic(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 (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if(! child.static) { node.static =false}}if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if(! block.static) { node.static =false
        }
      }
    }
  }
}

function isStatic (node: ASTNode) :boolean {
  // The expression must not be static
  if (node.type === 2) { // expression
    return false
  }
  // Plain text nodes must be static
  if (node.type === 3) { // text
    return true
  }
  // Vpre or tag with no binding value, no V-if, no V-for, not slot, template node, HTML or SVG reserved (non-component)
  // is not a child of the v-for template
  // Any property satisfies the static case
  return!!!!! (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
    isPlatformReservedTag(node.tag) && // not a component! isDirectChildOfTemplateFor(node) &&Object.keys(node).every(isStaticKey)
  ))
}
Copy the code

Here is the answer to the second question (what counts as a static node) :

  • Plain text;
  • node.prev-preThe contents of the instruction are static nodes;
  • No binding values, nonev-if, nov-forAnd is notslot,templateNodes, ishtmlsvgReserved label (non-component), notv-fortemplateChild nodes, any property, are static;
  • For any node, if the child node is not static, then it is not static.

Go back to 🍪 :

<div id="app">
    <! This is a comment node -->
    <Child name="yjc" :age="12" v-if="isShow"></Child>
    <input type="text" v-model="inputValue" />
    <div class="abc"></div>
</div>
Copy the code

According to the category of static nodes above, there are three static nodes:

markStaticRoots

The second main process is to tag a static root node. What is a static root node? Let’s start with the functional logic:

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 it's better 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);
      }
    }
    if (node.ifConditions) {
      for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
        markStaticRoots(node.ifConditions[i$1].block, isInFor); }}}}Copy the code

The function recursively calls markStaticRoots with the tag Node. staticInFor = isInFor if the node is static and node.once (the node where v-once is used). A node is a static root node if it satisfies that it is static and ordinary, and if its children are not all text nodes (type === 3). ⚠️ you can see a comment on the code above, noting that static root nodes in this condition have refresh performance. There is no such node in 🍪. So all normal nodes (type === 1) will be marked staticRoot = False.

summary

Optimize marks each node with the static field recursively, marking static: true for nodes that meet static criteria. On a static node basis, if a normal node contains a static node with non-plain text, then the node is marked as staticRoot and marked staticRoot:true.

generate

Everything is ready except the east wind. Advised a lot of online compilation of the article, to this step may be tired to write, are hastily put the generated render code posted on the summary. The generate process is summed up in a sentence as “identifying the fields in the AST and turning them into a render function after a series of processes.” There are a lot of criteria for this process. Here, we follow the AST in 🍪 to complete the generate process step by step.

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
) :CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}} `.staticRenderFns: state.staticRenderFns
  }
}
Copy the code

Create an instance of CodegenState, state, which will be used later. Then call genElement to generate the final code:

export function genElement (el: ASTElement, state: CodegenState) :string {
  if(el.staticRoot && ! el.staticProcessed) {// Static root node
    return genStatic(el, state)
  } else if(el.once && ! el.onceProcessed) {// v-once
    return genOnce(el, state)
  } else if(el.for && ! el.forProcessed) {// v-for
    return genFor(el, state)
  } else if(el.if && ! el.ifProcessed) {// v-if
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) { // template
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {       // slot
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {

      // Generate the root node
      const data = el.plain ? undefined : genData(el, state)

      // Generate the child node
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : ' ' // data
      }${
        children ? `,${children}` : ' ' // children
      }) `
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}
Copy the code

GenElement determines each field on the node and then does different genXXX processing. The AST generated by 🍪 is as follows:

The AST property of the root node is executed to const data = el.plain? Undefined: genData(el, state)

export function genData (el: ASTElement, state: CodegenState) :string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ', '

  / /... A bunch of ifs that are discarded if the current AST cannot execute
  // attributes
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}}, `
  }
  / /... A bunch of ifs that are discarded if the current AST cannot execute
  data = data.replace($/ /,.' ') + '} '
  // ...
  return data
}
Copy the code

The root node so easy, there is only id = app attrs. Return “{attrs:{\”id\ :\”app\”}}”. The next step is to iterate over children to generate the render function of the child node, which executes to

const children = el.inlineTemplate ? null : genChildren(el, state, true)
Copy the code

🍪 is not an inline template, so go to genChildren(el, state, true) :

export function genChildren (el: ASTElement, state: CodegenState, checkSkip? : boolean, altGenElement? :Function, altGenNode? :Function
) :string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1&& el.for && el.tag ! = ='template'&& el.tag ! = ='slot'
    ) {
      return (altGenElement || genElement)(el, state)
    }
    /** * Get the normalized type * 0 without normalization * 1 simple normalization (possibly a nested array of one level) --> child V-if exists component * 2 full normalization --> child V-if with v-for, or template, or The tag label * /
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return ` [${children.map(c => gen(c, state)).join(', ')}]${
      normalizationType ? `,${normalizationType}` : ' '
    }`}}Copy the code

🍪 has a child component, so the planning type is 1. What’s the use of this? Keep it in suspense!

Each child component then loops through the genNode function to generate its own render function.

function genNode (node: ASTNode, state: CodegenState) :string {
  // Common node
  if (node.type === 1) {
    return genElement(node, state)
  // Comment the node
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  // Text node
  } else {
    return genText(node)
  }
}
Copy the code

The first node is child, which has the V-if instruction. This is a bit special.

GenNode -> genElement:

// ... 
// V-if exists and is not marked
else if(el.if && ! el.ifProcessed) {// v-if
     return genIf(el, state)
}
// ...
Copy the code

Enter the genIf:

export function genIf (el: any, state: CodegenState, altGen? :Function, altEmpty? : string) :string {
  // Mark to avoid recursion
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
Copy the code

Enter the genIfConditions:

function genIfConditions (conditions: ASTIfConditions, state: CodegenState, altGen? :Function, altEmpty? : string) :string {
  if(! conditions.length) {return altEmpty || '_e()'
  }

  const condition = conditions.shift()
  if (condition.exp) {
    return ` (${condition.exp})?${ genTernaryExp(condition.block) }:${ genIfConditions(conditions, state, altGen, altEmpty) }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)? _m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}
Copy the code

The condition. Exp in 🍪 is isShow, so the if logic is invoked to call genTernaryExp and genIfConditions.

GenTernaryExp, which executes genElement (except that el.ifProcessed is already true) -> genData, generates the code:

"_c('child',{attrs:{"name":"yjc","age": 12}})"
Copy the code

Finally, genIfConditions, condition in 🍪 is 0 at this time. So just return _e(). The final code generated by this node:

isShow ? _c('Child', {
    attrs: {
        "name": "yjc"."age": 12
    }
}) : _e()
Copy the code

The second child node is the space node:

{
    text: "".type: 3.static: true
}
Copy the code

Execute to genText:

export function genText (text: ASTText | ASTExpression) :string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  }) `
}
Copy the code

Generated code:

"_v(\" \")"
Copy the code

The third child node is also more distinctive, with v-model instructions, this processing can be described as very complex. Without further ado, take a look at AST:

GenNode -> genElement -> genData, both of which are the same. When getData is reported, the gencache will be executed because there are directives:

function genDirectives (el: ASTElement, state: CodegenState) :string | void {
  const dirs = el.directives
  if(! dirs)return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
      
    // Modal definition, defined in SRC \platforms\web\ Compiler \directives\model.js
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.needRuntime = !! gen(el, dir, state.warn) }if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:The ${JSON.stringify(dir.value)}` : ' '
      }${
        dir.arg ? `,arg:"${dir.arg}"` : ' '
      }${
        dir.modifiers ? `,modifiers:The ${JSON.stringify(dir.modifiers)}` : ' '
      }}, `}}if (hasRuntime) {
    return res.slice(0, -1) + '] '}}Copy the code

See the definition of the gen function, which is the function definition of the modal instruction:

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type

  // ...
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  }
  // ...
  return true
}
Copy the code

Omit the judgment whether component V-model, whether input and checkbox, radio, file combination, whether select. To see our input in 🍪, go to genDefaultModel:

function genDefaultModel (el: ASTElement, value: string, modifiers: ? ASTModifiers): ?boolean {
  const type = el.attrsMap.type

  // ...
  const { lazy, number, trim } = modifiers || {}
  constneedCompositionGuard = ! lazy && type ! = ='range'
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  let valueExpression = '$event.target.value'
  
  // v-model.trim removes whitespace modifiers
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
      
  // v-model.number
  if (number) {
    valueExpression = `_n(${valueExpression}) `
  }

  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }

  addProp(el, 'value'.` (${value}) `)
  addHandler(el, event, code, null.true)
  if (trim || number) {
    addHandler(el, 'blur'.'$forceUpdate()')}}Copy the code

Lazy, number, and trim modifiers are handled first, and value and input events are added to AST via addProp and addHandler. V-model is grammar sugar for this reason:

export function addProp (el: ASTElement, name: string, value: string) {
  (el.props || (el.props = [])).push({ name, value })
  el.plain = false
}


export function addHandler (el: ASTElement, name: string, value: string, modifiers: ? ASTModifiers, important? : boolean, warn? :Function
) {
  modifiers = modifiers || emptyObject

  // ...

  let events
  if (modifiers.native) {
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }

  // ...  
  const handlers = events[name]
  /* istanbul ignore if */
  if (Array.isArray(handlers)) {
    important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  } else if (handlers) {
    events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

Copy the code

Remove the non-critical modifiers and logging logic, and the logic of the above two functions is simple. The generated AST is as follows:

After the AST is processed and returned to the genDirectives, the res returned by this function is the following string:

"directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}]"
Copy the code

Back up to genData, which handles the props and Events fields:

// DOM props
if (el.props) {
    data += "domProps:{" + (genProps(el.props)) + "},";
}
// event handlers
if (el.events) {
    data += (genHandlers(el.events, false, state.warn)) + ",";
}
Copy the code

Same as attrs for props, look at genHandlers:

function genHandlers (events, isNative, warn) {
  var res = isNative ? 'nativeOn:{' : 'on:{';
  for (var name in events) {
    res += "\" " + name + "\":" + (genHandler(name, events[name])) + ",";
  }
  return res.slice(0, -1) + '} '
}
Copy the code

This function has a lot of event handling logic, such as keyboard keys, event modifiers, etc., because 🍪 is not involved, directly post the generated code:

"on:{"input":function($event){if($event.target.composing)return; inputValue=$event.target.value}}"
Copy the code

The final input node generates the following code:

"_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"typ e\":\"text\"},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return; inputValue=$event.target.value}}})"
Copy the code

The last two AST are relatively simple, so I will not elaborate on them here. For those who are interested, please make a cup :coffee: Step by step debugging. At this point, the entire generate process is over, and the complete render generated is as follows:

"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[(isShow)?_c('child',{attrs:{\"name\":\"yjc\",\"age\":12}}):_e(),_v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\" type\":\"text\"},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inpu tValue=$event.target.value}}}),_v(\" \"),_c('div',{staticClass:\"abc\"})],1)}"
Copy the code

summary

Generate converts the AST after optimize into Render code by field matching and processing. There are too many branches in the process to cover them all at once. Through 🍪 analysis of v-if, V-model generation process, render process must be able to have a general impression. Other details in the specific problems encountered, in the appropriate position of the single step debugging, I believe that soon can solve the problem.

conclusion

The entire template compilation process can be divided into four volumes:

  • Create compilers because different platforms (web,weex) there is a different compilation process, so the difference is smoothen at the entrance;
  • parsePhase, through the re matching willtemplateString toAST, used duringregex101Tools, the end again recommended a wave, Gaga incense; 😆 😆 😆
  • optimizePhase, marking static node, static root node, inASTOn the plusstaticstaticRootInformation;
  • generatePhase, through the property symbol on the node, willASTgeneraterenderThe code.

Read here, I believe you must have a clear understanding of the template compilation process. Have a problem to point out in time! 😡 red face correction in time. Move your little hands and point a thumbs-up 👍👍