preface

Students familiar with Vue know that, starting from Vue2, in the actual operation, it is to convert the template written by the user into the render function to obtain vNode data (virtual DOM), and then continue to execute, finally through the patch to the real DOM. When data is updated, it is also used to diff the vNode data and ultimately decide which real DOM to update.

This is also a core advantage of Vue, Utah university more than once said, because the user wrote a static template, so Vue can do a lot of markup according to the template information, and then can do targeted performance optimization, this in Vue 3 to do further optimization processing, block related design.

So, let’s take a look, in Vue, template to render function, exactly experienced what kind of process, here side what is worth our reference and learning.

The text analysis

What

Template to render, Vue is the corresponding part of compile, the term compiler cn.vuejs.org/v2/guide/in… Essentially, this is the same approach that many frameworks use, AOT, which is to do things at runtime that need to be done at compile time to improve runtime performance.

The syntax of the Vue template itself is not explained in detail here. If you are interested, you can go to cn.vuejs.org/v2/guide/sy… , which is roughly the syntax (interpolation and instructions) as follows:

How about the render function, which is also covered in Vue at cn.vuejs.org/v2/guide/re… In a nutshell, it looks something like this:

So here’s our core goal:

If you want to experience this, you can do it here template-explorer.vuejs.org

Of course, Vue 3 is also available https://vue-next-template-explorer, although here we are going to analyze Vue 2 version.

How

To understand how to do this, we need to start with the source code, the compiler is available at github.com/vuejs/vue/t… Here we start with the entry file index.js:

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
 
// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) :CompiledResult {
  / / the point!
  // Step 1 parse the template to get the AST
  const ast = parse(template.trim(), options)
  // Optimizations can be ignored for now
  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
  // Step 2 generate code based on the AST
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
Copy the code

This, you’ll see, is a classic Parsing, or simplification, implementation of the compiler:

  1. Parse to get the AST
  2. Generate, get the object code

Let’s take a look at the respective implementations.

1. parse

The implementation of Parse is available at github.com/vuejs/vue/b… Here, since the code is quite long, let’s take a look at the exposed parse function first:

export function parse (template: string, options: CompilerOptions) :ASTElement | void {
  // Options processing has been ignored here
  // Important stack
  const stack = []
  constpreserveWhitespace = options.preserveWhitespace ! = =false
  const whitespaceOption = options.whitespace
  // There is only one root node, because we know that Vue 2's template can only have one root element
  // Ast is a tree structure, and root is the root node of the tree
  let root
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false
  / / parseHTML processing
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // Start end chars comment
    // About the same as the hook exposed by parseHTML for external processing
    // So purely, parseHTML is just responsible for parse, but does not generate AST logic
    // The ast generation depends on the hook function
    // It is easy to understand:
    // start is called every time a start tag is encountered
    // end is called to end the tag
    // Focus on the logic in start and end.
    // chars comment corresponds to plain text and comments
    start (tag, attrs, unary, start, end) {
      // check namespace.
      // inherit parent ns if there is one
      const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
 
      // handle IE svg bug
      /* istanbul ignore if */
      if (isIE && ns === 'svg') {
        attrs = guardIESVGBug(attrs)
      }
      // Create an ASTElement based on the tag attribute
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }
 
      if(process.env.NODE_ENV ! = ='production') {
        if (options.outputSourceRange) {
          element.start = start
          element.end = end
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) = > {
            cumulated[attr.name] = attr
            return cumulated
          }, {})
        }
        attrs.forEach(attr= > {
          if (invalidAttributeRE.test(attr.name)) {
            warn(
              `Invalid dynamic argument expression: attribute names cannot contain ` +
              `spaces, quotes, <, >, / or =.`,
              {
                start: attr.start + attr.name.indexOf(` [`),
                end: attr.start + attr.name.length
              }
            )
          }
        })
      }
 
      if(isForbiddenTag(element) && ! isServerRendering()) { element.forbidden =trueprocess.env.NODE_ENV ! = ='production' && warn(
          'Templates should only be responsible for mapping the state to the ' +
          'UI. Avoid placing tags with side-effects in your templates, such as ' +
          ` <${tag}> ` + ', as they will not be parsed.',
          { start: element.start }
        )
      }
 
      // Some pre-conversions can be ignored
      for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
      }
 
      if(! inVPre) { processPre(element)if (element.pre) {
          inVPre = true}}if (platformIsPreTag(element.tag)) {
        inPre = true
      }
      if (inVPre) {
        processRawAttrs(element)
      } else if(! element.processed) {// structural directives
        // Handle vUE instructions, etc
        processFor(element)
        processIf(element)
        processOnce(element)
      }
 
      if(! root) {// If there is no root, the current element is the root element
        root = element
        if(process.env.NODE_ENV ! = ='production') {
          checkRootConstraints(root)
        }
      }
 
      if(! unary) {// Set the current parent element, needed when dealing with children
        currentParent = element
        
      

// Then start and then end twice // is a classic stack handling, first in, last out // Any compiler can't do without the stack, and the processing is similar stack.push(element) } else { closeElement(element) } }, end (tag, start, end) { // The element being processed const element = stack[stack.length - 1] // Pop the last one // pop stack stack.length -= 1 // The latest tail is the parent of the element to be processed next currentParent = stack[stack.length - 1] if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) { element.end = end } closeElement(element) }, chars (text: string, start: number, end: number) { if(! currentParent) {if(process.env.NODE_ENV ! = ='production') { if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.', { start } ) } else if ((text = text.trim())) { warnOnce( `text "${text}" outside root element will be ignored.`, { start } ) } } return } // IE textarea placeholder bug /* istanbul ignore if */ if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } const children = currentParent.children if (inPre || text.trim()) { text = isTextTag(currentParent) ? text : decodeHTMLCached(text) } else if(! children.length) {// remove the whitespace-only node right after an opening tag text = ' ' } else if (whitespaceOption) { if (whitespaceOption === 'condense') { // in condense mode, remove the whitespace node if it contains // line break, otherwise condense to a single space text = lineBreakRE.test(text) ? ' ' : ' ' } else { text = ' '}}else { text = preserveWhitespace ? ' ' : ' ' } if (text) { if(! inPre && whitespaceOption ==='condense') { // condense consecutive whitespaces into single space text = text.replace(whitespaceRE, ' ')}let res letchild: ? ASTNodeif(! inVPre && text ! = =' ' && (res = parseText(text, delimiters))) { child = { type: 2.expression: res.expression, tokens: res.tokens, text } } else if(text ! = =' '| |! children.length || children[children.length -1].text ! = =' ') { child = { type: 3, text } } if (child) { if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) { child.start = start child.end = end } children.push(child) } } }, comment (text: string, start, end) { // adding anything as a sibling to the root node is forbidden // comments should still be allowed, but ignored if (currentParent) { const child: ASTText = { type: 3, text, isComment: true } if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) { child.start = start child.end = end } currentParent.children.push(child) } } }) // return the root node return root } Copy the code

As you can see, the most important thing to do is call parseHTML, and the most important thing to do in the passed hook is in the start tag. For the Vue scenario, using the hook processing, the final root we return is actually the root node of a tree, which is our AST, like this:

The template as follows:

<div id="app">{{ msg }}</div>
Copy the code
{
    "type": 1."tag": "div"."attrsList": [{"name": "id"."value": "app"}]."attrsMap": {
        "id": "app"
    },
    "rawAttrsMap": {},
    "children": [{"type": 2."expression": "_s(msg)"."tokens": [{"@binding": "msg"}]."text": "{{ msg }}"}]."plain": false."attrs": [{"name": "id"."value": "app"}}]Copy the code

ParseHTML is the core part of the company, and the core part (not all) is analyzed in parts. The source file github.com/vuejs/vue/b…

// Parse is a process of traversing an HTML string
export function parseHTML (html, options) {
  // HTML is an HTML string
  // Stack appears again, the best data structure for handling nested parsing problems
  // HTML handles tag nesting
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  // Initial index position index
  let index = 0
  let last, lastTag
  // The violence loop is for traversal
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    if(! lastTag || ! isPlainTextElement(lastTag)) {// No lastTag is the initial state or lastTag is script style
      // This label element needs to be treated as plain text
      // This branch should be entered in normal state
      // Determine the tag position, which is the same as the non-tag end position
      let textEnd = html.indexOf('<')
      // In the starting position
      if (textEnd === 0) {
        // Comments are ignored for now
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')
 
          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            advance(commentEnd + 3)
            continue}}// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        // Conditional comments are ignored for now
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf('] > ')
 
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue}}// Doctype is ignored
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }
 
        // End tag, the first time to ignore, other cases will enter
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          // Process the closing tag
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
 
        // Focus, in general, start tag
        const startTagMatch = parseStartTag()
        // If there is a start tag
        if (startTagMatch) {
          // Handle the related logic
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)}continue}}let text, rest, next
      if (textEnd >= 0) {
        // The rest of the HTML after removing the text
        rest = html.slice(textEnd)
        while(! endTag.test(rest) && ! startTagOpen.test(rest) && ! comment.test(rest) && ! conditionalComment.test(rest) ) {// < in plain text
          // < in plain text, be forgiving and treat it as text
          next = rest.indexOf('<'.1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }
        // Get the actual text content
        text = html.substring(0, textEnd)
      }
      // There is no < so the content is plain text
      if (textEnd < 0) {
        text = html
      }
 
      if (text) {
        // focus forward to specify the length
        advance(text.length)
      }
 
      if (options.chars && text) {
        // Handle the hook function
        options.chars(text, index - text.length, index)
      }
    } else {
      // lastTag exists and is a script style such that its contents are treated as plain text
      let endTagLength = 0
      // The tag name exists in the stack
      const stackedTag = lastTag.toLowerCase()
       
      const reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?) (< / ' + stackedTag + '[^ >] * >)'.'i'))
      / / do replacement
      // Replace 
      
XXXX
const rest = html.replace(reStackedTag, function (all, text, endTag) { // The end tag itself is the length of endTagLength = endTag.length if(! isPlainTextElement(stackedTag) && stackedTag ! = ='noscript') { text = text .replace(/ /g.'$1') / / # 7298 .replace(/ /g.'$1')}if (shouldIgnoreFirstNewline(stackedTag, text)) { text = text.slice(1)}// Handle the hook function if (options.chars) { options.chars(text) } // Replace with null return ' ' }) // Index forward note that advance is not used because HTML is actually modified to rest index += html.length - rest.length html = rest // Process the closing tag parseEndTag(stackedTag, index - endTagLength, index) } if (html === last) { options.chars && options.chars(html) if(process.env.NODE_ENV ! = ='production' && !stack.length && options.warn) { options.warn(`Mal-formatted tag at end of template: "${html}"`, { start: index + html.length }) } break}}}Copy the code

There are several key functions defined in the context of parseHTML, so they can directly access key variables like index, stack, and lastTag:

// It is easy to understand, advance n positions
function advance (n) {
    index += n
    html = html.substring(n)
}
Copy the code
// Start tag
function parseStartTag () {
  // The regular match starts like 
  const start = html.match(startTagOpen)
  if (start) {
    // It is matched
    const match = {
      tagName: start[1].attrs: [].start: index
    }
    // Move after 
    advance(start[0].length)
    let end, attr
    // Before the end is > before
    while(! (end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {// Match the attributes
      attr.start = index
      // Move gradually
      advance(attr[0].length)
      attr.end = index
      // Collect attributes
      match.attrs.push(attr)
    }
    // encounter > end
    if (end) {
      // Whether the label is self-closing, for example, < XXXX />
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}
Copy the code
// Handle start tags when they are encountered
// We use a separate function to handle the start tag
function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash
 
    if (expectHTML) {
      / / HTML
      // the p tag cannot contain the isNonPhrasingTag tag
      / / a detailed look at https://github.com/vuejs/vue/blob/v2.6.14/src/platforms/web/compiler/util.js#L18
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        // So in the browser environment it is also automatically fault-tolerant to close them directly
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }
    // The case of the closure tag can be omitted
    // a scene like < XXX /> or 

constunary = isUnaryTag(tagName) || !! unarySlashconst l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { const args = match.attrs[i] const value = args[3] || args[4] || args[5] | |' ' const shouldDecodeNewlines = tagName === 'a' && args[1= = ='href' ? options.shouldDecodeNewlinesForHref : options.shouldDecodeNewlines attrs[i] = { name: args[1].value: decodeAttr(value, shouldDecodeNewlines) } if(process.env.NODE_ENV ! = ='production' && options.outputSourceRange) { attrs[i].start = args.start + args[0].match(/^\s*/).length attrs[i].end = args.end } } if(! unary) {// If it is not autism and case, it means it can be treated as children // push a current on the stack stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }) // Set lastTag to current // Prepare for the next entry into children lastTag = tagName } // start hook processing if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } Copy the code
// End the label processing
function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index
 
    // Find the closest opened tag of the same type
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      // Here we need to find the nearest unclosed tag of the same type
      // The corresponding paired element
      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) {
      // Return to the unclosed tag, where all elements need to be closed
      // Close all the open elements, up the stack
      for (let i = stack.length - 1; i >= pos; i--) {
        if(process.env.NODE_ENV ! = ='production'&& (i > pos || ! tagName) && options.warn ) { options.warn(`tag <${stack[i].tag}> has no matching end tag.`,
            { start: stack[i].start, end: stack[i].end }
          )
        }
        / / end hook
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }
      // There is no need to change the stack length
      // Remove the open elements from the stack
      stack.length = pos
      Remember to update lastTag
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      



if (options.start) { options.start(tagName, [], true, start, end) } } else if (lowerCasedTagName === 'p') {

if (options.start) { options.start(tagName, [], false, start, end) } if (options.end) { options.end(tagName, start, end) } } } Copy the code

So with an overview of what these three functions do, and the main logic of parseHTML, we can sort through the whole process.

For convenience, a concrete example is used here, for example

<div id="app">
  <p :class="pClass">
    <span>
      This is dynamic msg:
        <span>{{ msg }}</span>
    </span>
  </p>
</div>
Copy the code

So I’m going to go straight into parseHTML, go into the while loop, and obviously go into the processing of the start tag, parseStartTag

At this point, after the previous round of processing, the HTML already looks like this, because advance advances each time:

The beginning of the original root tag div

is done.

It then goes into the logic of handleStartTag

At this point, the stack has pushed an element, our start tag div, and holds the location and attribute information, and the lastTag points to that element.

We continue with the while loop

Because there are Spaces and newlines, the value of textEnd is 3, so we need to go into the text processing logic (Spaces and newlines are text content).

So this loop will process the text and then proceed to the next loop, which is already similar to what we did in the first loop:

Again, lastTag changes to P, and then goes to the logic that handles text (Spaces, newlines), which is omitted here, and the process is the same;

Let’s skip to the first time we deal with span

The loop repeats the same process as the first one, processing the normal element and producing the result:

The top element on the stack is the outer span. Then enter a new round of processing text:

And then it goes to the span element in the inner layer again, same logic, when it’s done

It then processes the innermost layer of text and reaches the innermost layer of the closing tag ,

At this time we focus on the cycle of this round:

You can see that after this loop, the innermost span has been closed, and the stack and lastTag have been updated to the outer span.

The rest of the loop, as you can probably guess, processes the text (newline whitespace) and parseEndTag, and goes off the stack again and again until the HTML string is empty, which stops the loop.

Our parse functions work the same way. They use parseHTML’s hook functions to press, process, and stack them until they’re finished. The core of what these hooks do is build their ast step by step from the parseHTML process. So the final AST result

The phase to Parse is complete here.

2. generate

Let’s see how we can get the desired render function based on the above AST. The code is available at github.com/vuejs/vue/b…

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
) :CodegenResult {
  const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}} `.staticRenderFns: state.staticRenderFns
  }
}
Copy the code

As can be seen, generate core. The first step is to create a CodegenState instance with no specific functions, which is equivalent to the processing of configuration items, and then enter the core logic genElement with relevant codes github.com/vuejs/vue/b…

// Generate element code
export function genElement (el: ASTElement, state: CodegenState) :string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
 
  if(el.staticRoot && ! el.staticProcessed) {return genStatic(el, state)
  } else if(el.once && ! el.onceProcessed) {return genOnce(el, state)
  } else if(el.for && ! el.forProcessed) {return genFor(el, state)
  } else if(el.if && ! el.ifProcessed) {return genIf(el, state)
  } else if (el.tag === 'template'&&! el.slotTarget && ! state.pre) {return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if(! el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state) }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

Basically, it does this by element type, as in the previous example, it goes into

Next up is an important genChildren github.com/vuejs/vue/b…

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'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? ` `, 1 : ` `, 0
        : ` `
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    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

You can see that basically loop children and then call genNode to generate children code, genNode github.com/vuejs/vue/b…

function genNode (node: ASTNode, state: CodegenState) :string {
  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

Here is to determine each node type, and then basically recursively call genElement or genComment or genText to generate the corresponding code.

The final generated code is as follows:

This can be interpreted as traversing the ast above, generating their corresponding code separately, and easily handling each case with the help of recursion. Of course, there are a lot of details here are actually ignored by us, mainly look at the normal core of the general brief process, easy to understand.

At this point, this is how the complete process of compiling templates to render functions is handled in Vue.

Why

To find out why, we can break it down into two points:

  • Why introduce the Virtual DOM
  • Why is template recommended (convert template to render function to get VNode data)

Why introduce the Virtual DOM

In fact, Judah himself talked about why Virtual DOM was introduced in Vue 2, whether it was necessary and so on.

Answer from Fang Yinghang:

Here are some articles and answers for your reference (including summaries from others) :

  • The rendering function section of the official website cn.vuejs.org/v2/guide/re…
  • zhuanlan.zhihu.com/p/23752826
  • zhuanlan.zhihu.com/p/108899766
  • zhuanlan.zhihu.com/p/58335278
  • www.zhihu.com/question/28…
  • www.zhihu.com/question/31…

Why do YOU recommend templates?

This is mentioned in the comparison of the official website framework, the original cn.vuejs.org/v2/guide/co…

Of course, in addition to the above reasons, as we mentioned in the introduction, templates are static, and Vue can be optimized to further improve runtime performance using AOT technology.

This is why there are different versions built in Vue, see cn.vuejs.org/v2/guide/in…

conclusion

Through the above analysis, we know that in Vue, the approximate process from template to render function, the most core is:

  • Parse the HTML string to get your own AST
  • According to the AST, generate the final render function code

This is the core of what compilers do.

So what can we learn from this?

The compiler

Compiler, that sounds pretty cool. Through our above analysis, we also know how it is handled in Vue.

The core principles of the compiler and the relative standardization process are basically mature, whether it is the parsing of HTML, which is analyzed and studied here, and then generating the final render function code, or any other language, or your own “language”.

If you want to learn more, the best way is to look at the compilation principle. In the community, there is also a well-known project github.com/jamiebuilds… There is a “five organs” compiler, the core of only 200 lines of code, in addition to the code, annotations are the essence, even more useful than the code, it is worth our in-depth study and research, and easy to understand.

The tree

The ast we obtained from Parse is actually a tree structure. The application of trees can be found everywhere, as long as you are good at finding them. Use him, can very good help us to carry out logical abstraction, unified processing.

The stack

In the above analysis, we have seen the use of the stack several times. It has been mentioned in the reactive Principle before, but here it is a very typical scenario and one of the best practices of the stack data structure.

Basically, you can see the application of stack in many frameworks or excellent libraries in the community, which can be said to be a very useful data structure.

hook

We saw hooks in action in parseHTML options, but it’s not just there. ParseHTML exposes hook functions such as start, end, chars, and comment to allow users to hook into the execution logic of parseHTML. This is a simple but useful idea. Of course, this idea itself is often associated with plug-in design schemes or microkernels; For different scenarios, there can be more complex implementations that provide more powerful functionality, such as the underlying Tapable library in WebPack, which is essentially an application of this idea.

Regular expression

Throughout the parser process, we encountered a variety of regex scenarios, especially github.com/vuejs/vue/b… Here:

const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}] * `
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `
const startTagOpen = new RegExp(` ^ <${qnameCapture}`)
const startTagClose = /^\s*(\/?) >/
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `)
const doctype = / ^ 
      ]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = / ^ 
      
const conditionalComment = / ^ 
      
const encodedAttr = / & (? :lt|gt|quot|amp|#39); /g
const encodedAttrWithNewLines = / & (? :lt|gt|quot|amp|#39|#10|#9); /g
Copy the code

There are many kinds of re usage and re generation. Regular itself has simple, there are complex, if you can not understand the regular expression here, recommend you to read the book proficient in regular expressions, I believe that after reading, you will harvest a lot.

Other small Tips

  • Directory module split, still worth our good study
  • Ast optimization operations, although there is no detailed analysis above, but in the source code or specifically to do the AST optimization related things
  • Simple factory mode using Creator
  • StaticRenderFns, what does it do, why does it exist
  • Reuse of caching technology to improve performance
  • Avoid repeated processing, the use of various marks
  • Because it involves HTML parsing, so it is necessary to understand the HTML specification, as well as the normal browser parsing HTML fault tolerant processing, some tools in the source code also reflect github.com/vuejs/vue/b…
  • The role of makeMap is heavily used in Vue

The team number of Didi front-end technology team has been online, and we have synchronized certain recruitment information. We will continue to add more positions, and interested students can chat with us.