preface

To get started, take a look at an example lifecycle diagram from the Vue website.

As you can see from the figure, when we instantiate and initialize a Vue object, Vue checks the EL and template properties for the template string. The resulting template is then compiled into the render function.

Vue uses the outerHTML of the specified EL element as a template only if template is not specified.

But if we also specify a custom render function, vue will no longer use the first two to get the template.

This article focuses on how Vue finally compiles a template obtained from template or EL into the Render function.

Parse the HTML template into an AST node tree

As mentioned above, after obtaining the template string, VUE parses the HTML template character by character through regular expression to parse the element nodes and the instructions, attributes and event bindings set on each element node, and finally builds an AST node tree that completely describes the INFORMATION of HTML nodes.

Take a look at a few core regular expressions and AST objects.

HTML characters match core regular expressions

// 1. Match the start tag (excluding the trailing >), such as 
const startTagOpen = ^ < / ((? :[a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u207 0-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)? [a-zA-Z_][\-\.0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070 -\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)/

// 2. Match normal HTML attributes, such as id="app"
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /

V-bind: SRC ="imageSrc" :[key]="value" // 3.
const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /

4.Matches the end of the start tag, such as > or />const startTagClose = /^\s*(\/?) >/

// 5. Match closed tags, such as 
const endTag = < \ \ / / ^((? :[a-zA-Z_][\\-\\. 0-9_a-zA-Z((? :[a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)? [a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*\\:)? [a-zA-Z_][\\-\\. 0-9_a-zA-Z((? :[a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*\:)? [a-zA-Z_][\-\. 0-9_a-zA-Za-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\ u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]*)]*)[^>]*>/// 6. Match the DOCTYPE
const doctype = / ^ 
      ]+>/i

// 7. Match HTML comments
const comment = / ^ 
      

// 8. Match HTML conditional comments
const conditionalComment = / ^ 
      

Copy the code

AST node object

The following code is the creation function for the AST node. The code gives you a basic understanding of AST nodes. In a nutshell,

AST nodes are descriptions of HTML node information. For example, the AST saves the content parsed by HTML tag attributes to attrsList, attrsMap, and rawAttrsMap, keeps references to the parent node in the parent node, and points to its children through children.

{
    // Node type
    type: 1.// Tag name, such as div
    tag: "div".// Attributes that the node contains
    attrsList: [].attrsMap: {},
    rawAttrsMap: {},
    // Parent node pointer
    parent: undefined.// Child node pointer
    children: []}Copy the code

Simple tag parsing

Assume the following template:

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

The process of parsing is to capture the tag name and attribute name and value through several core regular expressions, and then use this information to create an AST node object. The main code is as follows:

// 1. Parse tags and attributes
function parseStartTag () {
  // Match the start tag
  var start = html.match(startTagOpen);
  if (start) {
    var match = {
      tagName: start[1].attrs: [].start: index
    };
    // Move the pointer forward
    advance(start[0].length);
    var end, attr;
    // Matches the attributes in the tag
    while(! (end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { attr.start = index; advance(attr[0].length);
      attr.end = index;
      match.attrs.push(attr);
    }
    if (end) {
      match.unarySlash = end[1];
      advance(end[0].length);
      match.end = index;
      returnmatch } } } ... Omit other code// 2. Create an AST node object
varelement = createASTElement(tag, attrs, currentParent); . Omit other codeCopy the code

After the above analysis, the following AST node objects are obtained:

{
    attrsList: [{name: "id".value: "app".start: 5.end: 13}].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

The next thing you need to deal with is the div child node {{MSG}}. Since the children are text nodes, we use parseText to process the text nodes, and then use this information to create the AST node object. The main code is as follows:

// 1. Parse the characters in the text node
function parseText (text: string, delimiters? : [string, string]) :TextParseResult | void {
  / /... Omit other code
  const tokens = []
  const rawTokens = []
  // tagRE defaults to /\{\{((? :.|\r? \n)+?) \}\}/g, which identifies the value inserted in the text by {{value}}
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    
    // If a filter is used in template interpolation, the filter needs to be parsed first
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp}) `)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  // Expression and tokens
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

/ /... Omit other code

// 2. Create a text AST object

child = {
    type: 2.expression: res.expression,
    tokens: res.tokens,
    text: text
}
child.start = start;
child.end = end;

/ /... Omit other code

// Add to div's child node array
children.push(child);
Copy the code

The result of text node parsing is as follows:

{
    type: 2.expression: "_s(message)".tokens: [{@binding: "message"}].text: "{{message}}"
}
Copy the code

The AST of the root div is updated to:

{
    attrsList: [{name: "id".value: "app".start: 5.end: 13}]
    attrsMap: {id: "app"}
    children: [{type: 2.expression: "_s(msg)".tokens: [{@binding: "msg"}].text: "{{msg}}"}]
    end: 14
    parent: undefined
    rawAttrsMap: {id: {name: "id".value: "app".start: 5.end: 13}}
    start: 0
    tag: "div"
    type: 1
}

Copy the code

Finally, parse the closing tag </div>. When the parser matches a closed tag, it means that a tag has been matched.

In addition to HTML attributes such as ID =”app”, placeholder=” Edit me”, there are also a lot of VUE directive attributes, such as V-for, V-ON, V-model. The parser takes the AST node object, which was already generated, further. Eventually this information is added to the AST object in the form of directives, ON, domProps, and other properties.

// Final processing of nodes
function closeElement (element) {
    // Remove the empty child node of the element
    trimEndingWhitespace(element)
    // Handle the AST and add additional attributes
    if(! inVPre && ! element.processed) { element = processElement(element, options) }/ /... Omit other code
}

// Add additional attributes to the AST
function processElement (element: ASTElement, options: CompilerOptions) {
  // Add a key attribute to the AST
  processKey(element)

  // Add the plain attribute to the ASTelement.plain = ( ! element.key && ! element.scopedSlots && ! element.attrsList.length )// Handle v-ref, add ref attribute to AST object
  processRef(element)
  // Process the slot passed to the component, adding the slotScope property to the AST
  processSlotContent(element)
  // Process slot tags and add slotName to the AST
  processSlotOutlet(element)

  // Add the Component or inlineTemplate attribute to the AST
  processComponent(element)
  / / processing
  for (let i = 0; i < transforms.length; i++) {
    element = transforms[i](element, options) || element
  }
  // Add the AST object properties, such as directives, events, and props, depending on the properties
  processAttrs(element)
  return element
}
Copy the code

Knowing the simple tag parsing process, let’s look at the parsing of v-for, V-IF, V-Model and V-ON several common instructions.

Processing of V-for instructions

Let’s look at it with a simple HTML template. Given the following HTML template:

<div id="app"><p v-for="(item, index) in items">{{item}}</p></div>
Copy the code

Before parsing the </p> tag, you get an initial AST object that looks something like this:

{
    After initial parsing, the attribute is temporarily stored in attrsList
    attrsList: [{name: "v-for".value: "(item, index) in items".start: 17.end: 47}, 
        {name: ":key".value: "index".start: 48.end: 60}].attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1
}
Copy the code

ProcessFor is then called to parse the V-for directive and add the results to the AST object:

function processFor (el: ASTElement) {
  // Parse the v-for instruction on the node
  let exp
  // Get the v-for expression from attrsList and remove the V-for from attrsList
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    {alias: "item",for:"items",iterator1: "index"}
    const res = parseFor(exp)
    // Merge into the AST object
    if (res) {
      extend(el, res)
    } else if(process.env.NODE_ENV ! = ='production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for'])}}}Copy the code

The AST object becomes the following:

{
    // V-for is incorporated into the AST object as an attribute
    alias: "item".for: "items".iterator1: "index".// At this point, v-for has been removed from attrsList
    attrsList: [{name: ":key".value: "index".start: 48.end: 60}].attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1
}
Copy the code

For the sake of simplicity, the AST node objects we have shown in the example do not contain complete information, but only important information that will be helpful.

Processing of V-if instructions

We will adjust the HTML template in the above example as follows:

<div id="app"><p v-if="seen">you can see me</p></div>
Copy the code

After parsing, the AST object of the P tag is roughly as follows:

{
    // focus on attrsList
    attrsList: [{name: "v-if".value: "seen".start: 17.end: 28}].attrsMap: {v-if: "seen"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1
}

Copy the code

Next, vue calls processIf to further process v-if:

function processIf (el) {
  // Get the v-if expression and remove the v-if from attrsList
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // Add if and ifConditions attributes to AST
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // Add else or elseIf to AST
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
Copy the code

The AST object becomes the following:

{
    if: "seen".// ifConditions array, where each item represents a condition statement, exp represents the expression of the condition statement, and block is the AST object of the label to which the condition statement applies.
    ifConditions: [{exp: "seen".block: {... }}].// focus on attrsList
    attrsList: [{name: "v-if".value: "seen".start: 17.end: 28}].attrsMap: {v-if: "seen"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1
}

Copy the code

If v-if is followed by a p tag that uses the V-else directive, we also need to execute processIfConditions to push the parsing result into the ifConditions array of the previous node with the if attribute:

function processIfConditions (el, parent) {
  // Get the last node of the V-else
  const prev = findPrevElement(parent.children)
  // If the previous node exists and has an if attribute, push the v-else parsing result to the ifConditions attribute array of that node
  if (prev && prev.if) {
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    })
  } else if(process.env.NODE_ENV ! = ='production') {
    warn(
      `v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
      `used on element <${el.tag}> without corresponding v-if.`,
      el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else'])}}Copy the code

The AST node where v-if resides becomes the following:

{
    if: "seen".// The v-else node after v-if is added to the ifConditions array.
    ifConditions: [{exp: "seen".block: {... }}, {exp: undefined.block: {... }}].// focus on attrsList
    attrsList: [{name: "v-if".value: "seen".start: 17.end: 28}].attrsMap: {v-if: "seen"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1
}

Copy the code

Processing of V-ON instructions

Adjust the HTML template in the above example as follows:

<div id="app"><p @click="show">click me</p></div>
Copy the code

After parsing, the AST object of the P tag is roughly as follows:

{
    attrsList: [{name: "@click".value: "show".start: 17.end: 30].attrsMap: {@click: "show"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1
}

Copy the code

In the next step, vUE will add the event and time handlers bound by V-ON to the Events property of the AST object:

    / /... Omit other code
    if (onRE.test(name)) {
        // If it is a V-ON instruction, give it to an event handler
        name = name.replace(onRE, ' ')
        isDynamic = dynamicArgRE.test(name)
        if (isDynamic) {
          name = name.slice(1, -1)}// Add an on attribute event handler to the AST object
        addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
    }
    // Add an event handler
    function addHandler (el: ASTElement, name: string, value: string, modifiers: ? ASTModifiers, important? : boolean, warn? :?Function, range? : Range, dynamic? : boolean) {
      / /... Omit other code
      
      let events = el.events || (el.events = {})
    
      const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range)
    
      const handlers = events[name]
      // Add the event handler to events
      /* 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

The AST object becomes the following:

{
    attrsList: [{name: "@click".value: "show".start: 17.end: 30}].attrsMap: {@click: "show"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "p".type: 1.// add a record to events
    events: {click: {value: "show".dynamic: false.start: 17.end: 30}},
    hasBindings: true
}
Copy the code

Processing of V-model instructions

Adjust the HTML template in the above example as follows:

<div id="app"><input v-model="message" placeholder="edit me"></div>
Copy the code

After parsing, the AST object for the input tag looks like this:

{
    attrsList: [{name: "v-model".value: "message".start: 21.end: 38},
        {name: "placeholder".value: "edit me".start: 39.end: 60}].attrsMap: {v-model: "message".placeholder: "edit me"},
    children: [].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "input".type: 1
}

Copy the code

In the next processing, the compiler calls addDirective to add the DIRECTIVES property to the AST node and to add the resolution result of the V-Model to the directives.

// Add a directive object to the CACHE property array of the AST node, el.directives = [{name,rawName,...}]
function addDirective (el: ASTElement, name: string, rawName: string, value: string, arg: ? string, isDynamicArg: boolean, modifiers: ? ASTModifiers, range? : Range) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}
Copy the code

The AST node becomes the following:

{
    attrsList: [{name: "placeholder".value: "edit me".start: 39.end: 60}].attrsMap: {v-model: "message".placeholder: "edit me"},
    children: [].// Add the cache property and add a record to this array
    directives: [{name: "model".rawName: "v-model".value: "message".arg: null.isDynamicArg: false.modifiers: undefined.start:21.end:38}].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {},
    tag: "input".type: 1
}
Copy the code

Generate the render function for the AST

The HTML template is parsed into a tree of AST nodes. The compiler then executes generate to generate the body of the render function for the AST.

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

The main function of the Render function is to create a virtual node vnode. To create a VNode, you need three parameters: the element label, the data object, and the list of child elements. GenElement, as the core code generation method, generates all three parts of the code in sequence.

/** * Code generation functions for AST elements *@param {ASTElement} El AST object *@param {CodegenState} State Code generation status */
function genElement (el: ASTElement, state: CodegenState) :string {
  / /... Omit other code
  let data
      
      
Attrs :{"id":"app"}}"
if(! el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state) }// 2. Next, generate the child element creation code const children = el.inlineTemplate ? null : genChildren(el, state, true) _c(tag,data,children); code = `_c('${el.tag}'${ data ? `,${data}` : ' ' // data }${ children ? `,${children}` : ' ' // children }) ` / /... Omit other code Copy the code

Data Data object code

Using the first HTML template as an example:

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

The parsed AST object is as follows:

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

Copy the code

The following code is the generation logic of the data object code:

function genData (el: ASTElement, state: CodegenState) :string {
  let data = '{'
  // Start by generating code for directives that may modify other attributes of the element
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ', '

  // key
  if (el.key) {
    data += `key:${el.key}, `
  }
  // if ref='myref' and ref='myref',
  if (el.ref) {
    data += `ref:${el.ref}, `
  }
  // refInFor
  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}", `
  }
  / / is mainly the class and style code generated staticClass, staticStyle, classBinding, styleBinding
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // Attribute generation of the attributes element, such as given el.attrs=[{name: 'id', value:'app', dynamic: Undefined, start: 0, end: 5}], it returns' attrs: {" id ":" app "} '
  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) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate}, `}}// Remove the trailing comma and add curly braces
  data = data.replace($/ /,.' ') + '} '
 
  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

As you can see from the above code, Vue generates each of the props, Attrs, Events, and directives. In this example, the AST node object only has the attrs property, so its processing will only execute the following statement:

if (el.attrs) {
    data += `attrs:${genProps(el.attrs)}, `
  }
Copy the code

The genProps function is as follows:

function genProps (props: Array<ASTAttr>) :string {
  let staticProps = ` `
  let dynamicProps = ` `
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const value = __WEEX__
      ? generateValue(prop.value)
      : transformSpecialNewlines(prop.value)
    if (prop.dynamic) {
      dynamicProps += `${prop.name}.${value}, `
    } else {
      staticProps += `"${prop.name}":${value}, `
    }
  }
  staticProps = ` {${staticProps.slice(0, -1)}} `
  if (dynamicProps) {
    return `_d(${staticProps}[${dynamicProps.slice(0, -1)}]) `
  } else {
    return staticProps
  }
}

Copy the code

[{name: “id”, value: “app”, start: 5, end: 13}]] as a string like this:

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

Generate children code

The child element creation code generation function is as follows:

function genChildren (el: ASTElement, state: CodegenState, checkSkip? : boolean, altGenElement? :Function, altGenNode? :Function
) :string | void {
  const children = el.children
  // No child element is processed
  if (children.length) {
    const el: any = children[0]
    // If the child element uses the v-for directive
    if (children.length === 1&& el.for && el.tag ! = ='template'&& el.tag ! = ='slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? ` `, 1 : ` `, 0
        : ` `
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
      
    // Iterate through the Children array, generating code for each child element
    const gen = altGenNode || genNode
    return ` [${children.map(c => gen(c, state)).join(', ')}]${
      normalizationType ? `,${normalizationType}` : ' '
    }`}}/** * Generates code to create node nodes, encapsulating various node types *@param {*} node 
 * @param {*} state 
 */
function genNode (node: ASTNode, state: CodegenState) :string {
  // Element node
  if (node.type === 1) {
    return genElement(node, state)
    / / comment
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
    // Text node
  } else {
    return genText(node)
  }
}

/** * generate create text node string, return content such as: "_v("see me")" *@param {*} Text AST node */ of the text type
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

For this example, the child element of the div is a text node, and genText generates the following code:

"[_v(_s(message))]"
Copy the code

The complete code for the Render function

Finally, in the genElement function, the pieces of code are spliced together to form a complete piece of code:

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

This is the basic render function creation steps, let’s take a look at the code generated for v-if, V-model, and V-for (v-ON is also involved in v-model processing, not a separate example).

V – if the processing

Let’s look at the v-if processing logic in the genElement function:

function genElement (el: ASTElement, state: CodegenState) :string {
  / /... Omit other code
  // The genIf command exists on the node
  if(el.if && ! el.ifProcessed) {return genIf(el, state)
  } 
}
Copy the code

If an HTML tag in the template uses v-if, the compiler calls genIf. The main code is as follows:

function genIf (el: any, state: CodegenState, altGen? :Function, altEmpty? : string) :string {
  el.ifProcessed = true 
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (conditions: ASTIfConditions, state: CodegenState, altGen? :Function, altEmpty? : string) :string {
  if(! conditions.length) {return altEmpty || '_e()'
  }
  
  // Convert the conditional statement in the ifConditions array into a ternary expression
  const condition = conditions.shift()
  // Generate a ternary expression of the form a? 1:2
  if (condition.exp) {
    return ` (${condition.exp})?${ genTernaryExp(condition.block) }:${ genIfConditions(conditions, state, altGen, altEmpty) }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

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

After genIf is executed, a ternary expression is created. The following template is used as an example:

div id="app"><p v-if="seen">you can see me</p><p v-else>you can not see me</p></div>
Copy the code

The corresponding code generated using the V-if and V-else p tags is as follows:

"(seen)? _c('p',[_v("you can see me")]):_c('p',[_v("you can not see me")])"
Copy the code

From the generated code, we can understand the difference between V-show and V-if. When using V-if to control the hiding and display of DOM elements, they need to be removed and recreated each time.

V – processing model

If we use v-Model in the following template:

<div id="app"><input v-model="message" placeholder="edit me"></div>
Copy the code

The AST node core information corresponding to the INPUT label is as follows:

{
    attrs: [{name: "placeholder".value: ""edit me"".dynamic: undefined.start: 39.end: 60}]
    attrsList: [{name: "v-model".value: "message".start: 21.end: 38}, {name: "placeholder".value: "edit me".start: 39.end: 60}].attrsMap: {v-model: "message".placeholder: "edit me"},
    children: [].directives: [{name: "model".rawName: "v-model".value: "message".arg: null.isDynamicArg: false.modifiers: undefined.start:21.end:38}].events: {input: {value: "if($event.target.composing)return; message=$event.target.value".dynamic: undefined}},
    props: [{name: "value".value: "(message)".dynamic: undefined}].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {placeholder: {name: "placeholder".value: "edit me".start: 39.end: 60},v-model: {name: "v-model".value: "message".start: 21.end: 38}},
    hasBindings: true.tag: "input".type: 1
}
Copy the code

Input has no child elements, so you only need to generate data data objects. The generated code looks like this:

"{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return; message=$event.target.value}}}"
Copy the code

In the data object, domProps and ON are added to specify the value and input event handlers. So you understand that v-model is essentially a syntactically sugar wrapped around V-bind and V-on :input.

After concatenation, the complete input generates the following code:

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

V – for processing

If v-for is used in the following template:

<div id="app"><p v-for="(item, index) in items" :key="index">{{item}}</p></div>
Copy the code

The AST core information corresponding to the P label is as follows:

{
    alias: "item".for: "items".iterator1: "index".forProcessed: true.key: "index".attrsList: [].attrsMap: {v-for: "(item, index) in items", :key: "index"},
    children: [{type: 2.expression: "_s(item)".tokens: Array(1), text: "{{item}}".start: 61,... }].parent: {type: 1.tag: "div".attrsList: Array(1), attrsMap: {... },rawAttrsMap: {... },... },rawAttrsMap: {:key: {name: ":key".value: "index".start: 48.end: 60},v-for: {name: "v-for".value: "(item, index) in items".start: 17.end: 47}},
    tag: "p".type: 1
}
Copy the code

Call the genFor function:

function genFor (el: any, state: CodegenState, altGen? :Function, altHelper? : string) :string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ' '
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ' '
  // In non-production environments, if :key is not specified, the console prompts the user
  if(process.env.NODE_ENV ! = ='production'&& state.maybeComponent(el) && el.tag ! = ='slot'&& el.tag ! = ='template' &&
    !el.key
  ) {
    state.warn(
      ` <${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'].true /* tip */
    )
  }

  el.forProcessed = true 
  // Concatenate complete code
  return `${altHelper || '_l'}((${exp}), ` +
    `function(${alias}${iterator1}${iterator2}) {` +
      `return ${(altGen || genElement)(el, state)}` +
    '}) '
}
Copy the code

The following code can be generated:

"_l((items),function(item,index){return _c('p',{key:index},[_v(_s(item))])}),0"
Copy the code

Here’s a description of the help methods used in the Render function:

  • Vue.prototype._s converts to character type
  • Vue.prototype._l Render list
  • Vue.prototype._v Creates a vnode of text type
  • Vue. Prototype. _c create vnode

conclusion

This is the general process of vUE template compilation: parsing HTML tags into AST nodes one by one using core regular expressions, and finally generating the body of the Render function based on the AST nodes. The Render function is responsible for generating vNodes.

This article tries to explain the compilation process of VUE clearly, but in the process of writing, I still feel that it is a little awkward. If you don’t understand, you can leave a message for discussion.

Next time we’ll talk about vUE rendering, such as vNode creation and diff.

Pay attention to our