preface

Vue template compilation continues research

Template compilation

Rendering templates into functions can be done in two steps, parsing templates into an AST (abstract syntax tree) and then using the AST to generate rendering functions. But because static nodes do not change when data changes, they can be marked and updates skipped. So, in general logic, template compilation consists of three parts:

1. Compile the template into the AST

2. Traverse the AST to mark static nodes

3. Use AST to generate rendering functions

The parser

We convert the string passed in by template into an AST object. The data structure of stack stack is used to confirm the parent-child relationship between DOM and construct abstract syntax tree structure.

Regular parsing string

  • Check if the string starts with “<“, if so, match the start tag or end tag
  • If you start tag processing, the tag attributes will be processed along the way, and when the “>” tag is matched, you can put the tag on the stack and call the options.start method passed in.
  • If it is an end tag, call the options.end method passed in. Label out of stack.
  • If the string begins with either “<” or text, intercept the desired text and call the options.chars method passed in.
/ / property
const attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
// Dynamic attributes
const dynamicArgAttribute = /^\s*((? :v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /
// Tag names match
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = ` ((? :${ncname}\ \ :)?${ncname}) `
// 
const startTagOpen = new RegExp(` ^ <${qnameCapture}`)
Div > match start tag close XXXX >
const startTagClose = /^\s*(\/?) >/
// Match the end tag 
const endTag = new RegExp(` ^ < \ \ /${qnameCapture}[^ >] * > `)
export function parseHTML(html: string, options: any) {
  // Stack structure to parse DOM structure
  const stack: any = []
  let index = 0
  // Last last HTML content, lastTag Last tag
  let last, lastTag: any
  while (html) {
    last = html
    // start with <
    let textEnd = html.indexOf('<')
    // May be the start tag, or the end tag
    if (textEnd === 0) {
      // Is the end tag 
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
        const curIndex = index
        advance(endTagMatch[0].length)
        // Call options.end, label off the stack, and enter the next loop
        parseEndTag(endTagMatch[1], curIndex, index)
        continue
      }
      
      const startTagMatch = parseStartTag()
      
      
we can further process the start tag
if (startTagMatch) { // call options.start, push the label, and enter the next loop handleStartTag(startTagMatch) continue}}let text, rest, next // It is not a label, the description is text if (textEnd >= 0) { // In the loop, find the text before the < < to see if it meets the label requirements, can intercept the whole text "123 rest = html.slice(textEnd) while(! endTag.test(rest) && ! startTagOpen.test(rest) ) { next = rest.indexOf('<'.1) if (next < 0) break // Cut back again textEnd += next rest = html.slice(textEnd) } // Intercepts the text before the tag text = html.substring(0, textEnd) } // No <, all text if (textEnd < 0) text = html // Truncate the text if (text) advance(text.length) // Call options.chars to process the text content if (options.chars && text) { options.chars(text, index - text.length, index) } // Error text, syntax error if (html === last) { options.chars && options.chars(html) break}}// Clean up any remaining tags parseEndTag() // Intercepts a string function advance(n: number) { index += n html = html.substring(n) } function parseStartTag() { // Is the start label const start = html.match(startTagOpen) if (start) { const match: any = { tagName: start[1].attrs: [].start: index } advance(start[0].length) let end = html.match(startTagClose) // ... if (end) { // ... return match } } } function handleStartTag(match: any) { const tagName = match.tagName // Label attributes are pushed onto the stack as objects stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }) // Set the last label lastTag = tagName // Call the start method. Create the ast if (options.start) { options.start(tagName, attrs, unary, match.start, match.end) } } function parseEndTag(tagName? : any, start? : any, end? : any) { let pos, lowerCasedTagName // Call the closing tag method if (options.end) { options.end(stack[i].tag, start, end) } // Label out of stack / /... stack.pop() lastTag = stack.length && stack[pos - 1].tag } } } Call the parseHTML method.Cosnt stack = [] cosnt stack = []let currentParent: any let root: any let stack: any = [] parseHTML(html, { start(tag: any, attrs: any, unary: any, start: any, end: any) { // Create a stack syntax tree let element: any = createASTElement(tag, attrs, currentParent) processFor(element) // v-for processIf(element) // v-if processOnce(element) // v-once if(! root) root = element currentParent = element stack.push(element) },end(tag: any, start: any, end: any) { const element = stack[stack.length - 1] stack.length -= 1 currentParent = stack[stack.length - 1] closeElement(element) }, chars(text: string, start: number, end: number) { if(! currentParent) {return } const children = currentParent.children text = text.trim() if (text) { let res let child: any if ((res = parseText(text))) { // Text with {{}}, type 2 child = { type: 2.expression: res.expression, tokens: res.tokens, text } } else { // Plain text with type 3 child = { type: 3, text } } if (child) { children.push(child) } } } }) // Create the AST syntax tree function createASTElement( tag: string, attrs: Array<any>, parent: any | void ) :any { return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent, children: []}}// Parse text content with {{}} export function parseText(text: string) :any | void { const tagRE = / \ {\ {((? :.|\r? \n)+?) \}\}/g if(! tagRE.test(text)) {return } const tokens = [] const rawTokens = [] let lastIndex = tagRE.lastIndex = 0 let match, index, tokenValue while ((match = tagRE.exec(text))) { index = match.index // Add {{text to tokens first if (index > lastIndex) { tokenValue = text.slice(lastIndex, index) tokens.push(JSON.stringify(tokenValue)) } // Pass the contents of the curly braces as arguments to the _s method, which will be executed as an expression const exp = match[1].trim() tokens.push(`_s(${exp}) `) lastIndex = index + match[0].length } if (lastIndex < text.length) { tokenValue = text.slice(lastIndex) tokens.push(JSON.stringify(tokenValue)) } return { expression: tokens.join('+')}}Copy the code

The ultimate root is an abstract syntax tree

{
    attrsList: [{...}]attrsMap: {class'box'}
    children: (2) / {...}, {...}.parentundefined
    plainfalse
    rawAttrsMap: {}
    tag"div"
    type1
}
Copy the code

The optimizer

The internal implementation of the optimizer consists of two main steps:

  • Find and label all static nodes in the AST
  • Find and label all static root nodes in the AST

Static node: Nodes that never change are static nodes static root node: A node is a static root node if all of its children are static nodes and its parent is a dynamic node

export function optimize (root: ? ASTElement, options: CompilerOptions) {
    if(! root)return
    markStatic(root)
    markStaticRoots(root, false)}// Mark the static node
function markStatic (node: any) {
    node.static = isStatic(node)
    if (node.type === 1) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        const child = node.children[i]
        markStatic(child)
        // The child node is a dynamic node, change the parent node to dynamic node
        if(! child.static) { node.static =false}}}}// Mark the static root node
function markStaticRoots (node: any) {
    if (node.type === 1) {
      To qualify a node as a static root node, it must have child nodes
      // This node cannot be a child node with only one static text, otherwise the optimization costs will outweigh the benefits
      if(node.static && node.children.length && ! ( node.children.length ===1 &&
        node.children[0].type === 3
      )) {
        node.staticRoot = true
        // Find the static root node and exit
        return
      } else {
        node.staticRoot = false
      }
      if (node.children) {
        for (let i = 0, l = node.children.length; i < l; i++) {
          markStaticRoots(node.children[i])
        }
      }
    }
}

// Check whether the node is static
function isStatic (node: any) :boolean {
if (node.type === 2) { // expression
  return false
}
if (node.type === 3) { // text
  return true
}
return!!!!! (node.pre || ( ! node.hasBindings &&// Dynamic binding syntax cannot be used! node.if && ! node.for &&// Do not use v-if or v-for or v-else! isBuiltInTag(node.tag) &&// The tag name is not slot or compoennt
  isPlatformReservedTag(node.tag) && // Cannot be a component! isDirectChildOfTemplateFor(node) &&// The parent node cannot be a template with v-for
  Object.keys(node).every(isStaticKey) // There are no attributes for dynamic nodes))}Copy the code

Code generator

The code generator is the final step in template compilation and is used to convert the AST into the content of the rendering function, which can become a code string. The code string can be executed wrapped in a function, which is commonly referred to as a render function. After the render function is executed, a VNode can be generated.

Suppose we have a simple template like this:

Hello {{name}}

Processed code:

_c: the alias for creaElement, which creates a virtual node, is the h function in our handwritten render function.

with(this) {
    return _c(
        "div",
        {
            attrs: {"id": "el"}
        },
        [
            _v("Hello " + _s(name))
        ]
    )
}
Copy the code

The source code to achieve

  • Based on each label information of the AST, the concatenation _c function, the children node, is concatenated into an array
  • Use different methods to concatenate nodes _c(element node method), _e(text node method), and _v(text node method), depending on the type not used.
  • Finally, the code string is spelled with and returned to the caller.
// Pass in the AST abstract syntax tree to generate the render function code
export function generate(ast: any) :any {
  const code = ast ? genElement(ast) : '_c("div")'
  return {
    render: `with(this){return ${code}} `}}// The method to generate nodes, in the third argument to recursively generate each child element
export function genElement(el: any) :string {
  let code
  const data = el.plain ? undefined : genData(el)
  const children = genChildren(el)
  code = `_c('${el.tag}'${data ? `,${data}` : ' ' // data
    }${children ? `,${children}` : ' ' // children
    }) `
  return code
}

// Generate child nodes recursively to form a virtual DOM tree
export function genChildren(el: any,) :string | void {
  const children = el.children
  if (children.length) {
    return ` [${children.map((c: any) => genNode(c)).join(', ')}] `}}// Generate different nodes according to type
function genNode(node: any) :string {
  if (node.type === 1) {
    return genElement(node)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

// Generate a comment node
function genComment(comment: any) :string {
  return `_e(The ${JSON.stringify(comment.text)}) `
}

// Generate a text node
function genText(text: any) :string {
  return `_v(${text.type === 2 ? text.expression : JSON.stringify(text.text)}) `
}

// Generate tag attributes
export function genData(el: any) :string {
  let data = '{'
  // attributes
  if (el.attrsList) {
    data += `attrs:${genProps(el.attrsList)}, `
  }
  // ...
  data = data.replace($/ /,.' ') + '} '
  return data
}

function genProps(props: Array<any>) :string {
  let staticProps = ` `
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const value = JSON.stringify(prop.value)
    staticProps += `"${prop.name}":${value}, `
  }
  staticProps = ` {${staticProps.slice(0, -1)}} `
  return staticProps
}

Copy the code

The last

This article seven seven eight eight took a day to write, although, the reference to the “simple Vue. Js” the content of the third article, but still quite spend time, involving a lot of complex code processing is deleted, interested friends or need to see the source code. The year of 2021 is coming to an end. I hope you can cheer up and give me a lot of thumbs up. Merry Christmas

Code address: github.com/zhuye1993/m…