The seventh part of VUE source code learning-compile (parse)

1. Overview

In the previous article, we mentioned that during the mounting process during compilation, the template will be compiled into a render expression to prepare for the formation of a vnode later.

Let's overview the compilation process.

The whole process is divided into three stages:

1. Parse, convert templat into AST model tree.

2. Optimize, mark static nodes.

3. Generate, generate a render expression.

We will also introduce it into three chapters, this chapter focuses on the first stage.

Second, the entrance

In the previous article we talked about the code in src / platform / web / entry-runtime-with-compiler.js

     //2、编译,生成render
     ...
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      ...
    }

The input parameters of compileToFunctions are template, options array, and vm object, and finally return render and staticRenderFns objects. compileToFunctions is defined in src / platform / compiler / index.js and is returned by createCompiler.

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

createCompiler, whose input is the baseOptions object, returns two methods, compile and compileToFunctions.

Continue to view createCompiler (src / compiler / index.js)

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

createCompiler is returned by the createCompilerCreator method, which is defined in src / compiler / create-compiler.js

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
     ...
      //真正实现编译的核心代码
      const compiled = baseCompile(template, finalOptions)
      ...
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

The input parameter of createCompilerCreator is the baseCompile method. The createCompiler method is defined in the function body, and the compile method is defined in createCompiler. In this method, baseCompile is called to realize the real compilation. createCompiler returns the compileToFunctions method, which is implemented in createCompileToFunctionFn

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    ....
     const compiled = compile(template, options)
    ....
    return (cache[key] = res)
  }
}

The input parameter of createCompileToFunctionFn is the compile method, which returns the compileToFunctions. This is the method we initially called and finally found the source.

The process of defining the entire entrance is very round. We use the following picture to pluck your skin layer by layer.

Why do you want to do this? This is not a flashy technique. The baseoptions that vue.js compiles on different platforms are different, but the core compilation process baseComplie is the same, so the clever use of function currying is implemented. We write the curried pseudo-function of this process

createCompilerCreator(baseCompile)(baseOptions)(compile)(template,options,vm)

3. AST

 According to the above analysis, the final compilation is to execute the baseCompile method.

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  //1、parse,将templat转成AST模型
  const ast = parse(template.trim(), options)
  //2、optimize,标注静态节点
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  //3、generate,生成render表达式
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

This method defines three stages of the compilation process. Parsing is to convert template into AST model, AST is an abstract syntax tree. We take the following template as an example.

<div id="app">
  <ul>
    <li v-for="item in items">
      itemid:{{item.id}}
    </li>
  </ul>
</div>

After being converted into the AST abstract tree model, it is as follows:

{
    "type": 1,
    "tag": "div",
    "attrsList": [
        {
            "name": "id",
            "value": "app"
        }
    ],
    "attrsMap": {
        "id": "app"
    },
    "children": [
        {
            "type": 1,
            "tag": "ul",
            "attrsList": [],
            "attrsMap": {},
            "parent": {
                "$ref": "$"
            },
            "children": [
                {
                    "type": 1,
                    "tag": "li",
                    "attrsList": [],
                    "attrsMap": {
                        "v-for": "item in items"
                    },
                    "parent": {
                        "$ref": "$[\"children\"][0]"
                    },
                    "children": [
                        {
                            "type": 2,
                            "expression": "\"\\n      itemid:\"+_s(item.id)+\"\\n    \"",
                            "tokens": [
                                "\n      itemid:",
                                {
                                    "@binding": "item.id"
                                },
                                "\n    "
                            ],
                            "text": "\n      itemid:{{item.id}}\n    "
                        }
                    ],
                    "for": "items",
                    "alias": "item",
                    "plain": true
                }
            ],
            "plain": true
        }
    ],
    "plain": false,
    "attrs": [
        {
            "name": "id",
            "value": "\"app\""
        }
    ]
}

Each element of the AST contains information about its own nodes (tag, attr, etc.), and at the same time, parent and children point to their parent element and child element, respectively, nested layer by layer, forming a tree. We will not delve into the description of each attribute for the time being, but first have an intuitive understanding. Let's see how this tree is formed.

Four, parse

The parse method is defined in src / parser / index.js. This method has more content. We write the structure as follows:

export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  ....
  //定义AST模型对象
  let root
  ...

  //主要的解析方法
  parseHTML(template, {
    ...
  })
  //返回AST
  return root
}

The input parameters are template and options, and the output is the generated AST model root. It is mainly done by calling the parseHTML method, which also has more content, and we only write the structure

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  //循环处理html
  while (html) {
    last = html
    // Make sure we're not in a plaintext content element like script/style
    //处理非script,style,textarea
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      //1."<"字符打头
      if (textEnd === 0) {
        // Comment:
        //1.1、处理标准注释,<!--
        if (comment.test(html)) {
          ...
        }

        // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
        //1.2、处理条件注释,<![
        if (conditionalComment.test(html)) {
          ...
        }

        // Doctype:
        //1.3、处理申明,DOCTYPE
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          ...
        }

        // End tag:
        //1.4、处理结束标签
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          ...
        }

        // Start tag:
        //1.5、处理开始标签
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          ...
        }
      }
      
      //2、非"<"打头,作为text内容处理
      let text, rest, next
      if (textEnd >= 0) {
        ....
       }
     ...
     }else{
       ....
     }
   
  }

  ...

}

Through the while loop, the incoming HTML characters are parsed line by line. The entire method can be divided into two parts:

1. The characters starting with "<" are further judged into types, which are divided into standard notes, conditional notes, applications, end tags, start tags, and different treatments.

2. Characters that do not start with "<" are treated as text.

Let's take the previous template as an example, focusing on the analysis of the start, end, and text module parsing process.

Five, startTag

The analysis of the html template starts from the first sentence:

<div id="app">
  <ul>
    ...

Since it starts with the "<" character, it enters the loop and is processed by the code segment of the start tag:

const startTagMatch = parseStartTag()
if (startTagMatch) {
      handleStartTag(startTagMatch)
      if (shouldIgnoreFirstNewline(lastTag, html)) {
            advance(1)
      }
      continue
 }

1、parseStartTag

Analyze the template through various regular expressions, and save the relevant information in the match object.

function parseStartTag () {
    //1、匹配<${qnameCapture}字符,如:<div
    const start = html.match(startTagOpen)
    //start=[<div,div,index=0]
    if (start) {
      //定义match对象保存相关属性
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      //2、步进tag的长度
      advance(start[0].length)
      //3、循环查找该标签的attr,直到结束符>
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        //步进该attr的长度
        advance(attr[0].length)
        match.attrs.push(attr)
      }
      //4、tag结束,记录全局的位置
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

(1) RegExp (`^ <$ {qnameCapture}`) matches the starting tag. In this example, the matching character is "<div" and the match object is initialized.

(2) The length of the stepping tag,

function advance (n) {
    //index为全局位置
    index += n
    //从n位置开始截取,后面的字符作为新的html
    html = html.substring(n)
}

The characters after intercepting the tags are used as new html, after completion, they are as follows:

   id="app">
  <ul>
    ...

(3) Loop through the attributes of the tag until it encounters an end character>

The loop condition is judged for the first time. Html.match (attribute) matches the attribute character id = "app", enters the loop, steps the number of characters of the attribute, and saves the attribute in the match object. The new html after stepping is as follows:

       >
  <ul>
    ...

The second judgment of the loop condition, html.match (startTagClose) matches the end character>, and jumps out directly.

(4) The end of the tag, step the length of the end character, and record and save the global position, in this case, the length of <div id = "app"> 14. The new html characters are:

     <ul>
    ...

At this point, the start tag of the div is parsed, and the match object is returned to continue the following processing flow.

2、handleStartTag

The input parameter of handleStartTag is the match object, which mainly implements the regularization of the attribute object and calls the start method to create the AST model of the tag.

  //处理开始标签
  function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash
    
    //有些tag可以作结束处理
    if (expectHTML) {
      if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
        parseEndTag(lastTag)
      }
      if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
        parseEndTag(tagName)
      }
    }

    //是否为单元素,如<img  />
    const unary = isUnaryTag(tagName) || !!unarySlash
   
    //1、整理attr为字面量对象
    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
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
    }
    
    //2、非单元素,压入到stack,并在lastTag中缓存
    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
      lastTag = tagName
    }
    //3、创建该标签的AST模型,并建立关联关系
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

The main process has the following three parts:

1. Loop regularization attrs is a literal object. After the regularization, the objects are as follows:

attrs=[{name=id,value=app}]

2. For non-single elements, push it into the stack and cache the current tagname as lastTag. This stack performs closed-loop processing in the following end tag.

3. Continue to call the start method to create an AST model of the tag element and build a model tree.

3、start

options.start is the core method in which the AST model is created and the association is established.

start (tag, attrs, unary) {
      // 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)
      }
      
      //1、创建ASTelement
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      if (ns) {
        element.ns = ns
      }

      if (isForbiddenTag(element) && !isServerRendering()) {
        element.forbidden = true
        process.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.'
        )
      }
      
      //2、以下是处理属性中各类指令,从attrsList中删除相关的属性,
      // apply pre-transforms
      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
        processFor(element)
        processIf(element)
        processOnce(element)
        // element-scope stuff
        processElement(element, options)
      }

      function checkRootConstraints (el) {
        if (process.env.NODE_ENV !== 'production') {
          if (el.tag === 'slot' || el.tag === 'template') {
            warnOnce(
              `Cannot use <${el.tag}> as component root element because it may ` +
              'contain multiple nodes.'
            )
          }
          if (el.attrsMap.hasOwnProperty('v-for')) {
            warnOnce(
              'Cannot use v-for on stateful component root element because ' +
              'it renders multiple elements.'
            )
          }
        }
      }

      // tree management
      //3、构建AST模型树
      if (!root) {
        //如第一个元素,设置根元素
        root = element
        checkRootConstraints(root)
      } else if (!stack.length) {
        //其他元素,构建关联关系
        // allow root elements with v-if, v-else-if and v-else
        if (root.if && (element.elseif || element.else)) {
          checkRootConstraints(element)
          addIfCondition(root, {
            exp: element.elseif,
            block: element
          })
        } else if (process.env.NODE_ENV !== 'production') {
          warnOnce(
            `Component template should contain exactly one root element. ` +
            `If you are using v-if on multiple elements, ` +
            `use v-else-if to chain them instead.`
          )
        }
      }
      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 {
          //建立父子element关系
          currentParent.children.push(element)
          element.parent = currentParent
        }
      }
       //4、非单元素,将元素push到stack数组,
      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    }

Ignoring the details, there are four main parts:

1. Call createASTElement to create the AST model object of the label element. '

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,//1-标签,2-表达式text,3-普通内容
    tag,//标签
    attrsList: attrs,//标签属性数组
    attrsMap: makeAttrsMap(attrs),//标签属性map
    parent,//父元素
    children: []//子元素
  }
}

In the third part, we talked about the final AST model, which is defined here. The AST model is a literal object that defines the relevant information of the tag element, including tags, attributes, associated parent-child elements, etc.

2. Process various instructions in the attribute and delete the related attributes from the attrsList to prepare for subsequent processing.

3. Construct the AST model tree. The first tag element serves as the root element, such as the div in this example, and the next tag element establishes the association by setting parent and children. Eventually form a tree.

4. For non-single elements, push the current AST element to the stack array (note the difference with the above stack, the objects saved by the two are different, and are also closed-loop preparations for the end of the later). For single elements, call closeElement to do end processing.

Having said so much, I actually processed the <div id = "app"> line. Next, the while loop continues to process the following two lines, and the process is the same.

<ul>
    <li v-for="item in items">

Six, text

After the start tag processing is completed, when the following line is parsed, it is processed as text because it does not start with "<".

itemid:{{item.id}}
   </li>
  </ul>
</div>

Enter the following code block

let text, rest, next
      if (textEnd >= 0) {
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.test(rest)
        ) {
          // < in plain text, be forgiving and treat it as wen
          //普通文本中包含的<字符,作为普通字符处理
          next = rest.indexOf('<', 1)
          if (next < 0) break
          textEnd += next
          rest = html.slice(textEnd)
        }

        //1、获取text内容,并步进到新的位置
        text = html.substring(0, textEnd)
        advance(textEnd)
      }
      //html的<字符匹配结束,将剩余字符都作为text处理
      if (textEnd < 0) {
        text = html
        html = ''
      }
      //2、创建text的AST模型
      if (options.chars && text) {
        options.chars(text)
      }

1. According to textend, get the characters in front of the text content (ie </ li>), itemid: {{item.id}}.

2. Call chars to process characters. Create an AST model.

Let's look at its main logic:

chars (text: string) {
      ...
      //创建AST模型
      if (text) {
        let res
        //包含表达式的text
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          children.push({
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          })
        } 
        //纯文本的text
        else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          children.push({
            type: 3,
            text
          })
        }
      }
    }

Incorporate text as the child of its own element into the AST model tree. In this case, it is the tag element of <li>. According to whether the expression is included in the text (ie "{{}}"), there are two cases.

1. Expression text, type 3, and use expression and tokens attributes to save the expression.

2. Plain text, type 2.

This example belongs to the first case. After the analysis is complete, the following:

{
      "type": 2,
      "expression": "\"\\n      itemid:\"+_s(item.id)+\"\\n    \"",
      "tokens": [
                 itemid:",
                 {"@binding": "item.id"},
                 "\n    "
               ],
                "text": "\n      itemid:{{item.id}}\n    "
  }

You can look at the contents of expressions and tokens, and we will analyze them in detail later.

The text text analysis is complete, and then the processing ends with the tag.

Seven, end

After parsing to the end tag, closed-loop processing is performed for the element object of the tag. Keep reading the rest of the html

   </li>
  </ul>
</div>

Code snippet processed:

const endTagMatch = html.match(endTag)
if (endTagMatch) {
    const curIndex = index
    //步进结束tag的长度
    advance(endTagMatch[0].length)
    parseEndTag(endTagMatch[1], curIndex, index)
    continue
 }

    html.match (endTag) regular matches </ xx> such characters, determined to be the tag end character, record the current end character position into curIndex, then step the length of the end tag, call parseEndTag for processing.

function parseEndTag (tagName, start, end) {
    ...

    // Find the closest opened tag of the same type
    //1、从stack数组中查找结束的tag标签,并记录位置pos
    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
    }
    //2、当pos>0,关闭从pos到最后的所有元素,理论上只会有一个,但也要防止不规范多写了结束标签
    if (pos >= 0) {
      // 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.`
          )
        }
        //处理end
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      //从stack中删除元素
      stack.length = pos
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') {
      ...
    }
  }

1. Find the tag in the stack. At the beginning of the process, we push the match object of each tag into the stack. In this example, the stack has three objects

[{tag: div, ...}, {tag: ul, ...}, {tag: li, ...}]

Match from back to front, to the third object li. Record pos as 2.

2. When pos> 0 indicates that the label is matched, the end method is called for processing, and the object is deleted from the stack.

end () {
      // remove trailing whitespace
      //从AST中查找该标签的模型对象
      const element = stack[stack.length - 1]
      //删除text为空格的child
      const lastNode = element.children[element.children.length - 1]
      if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
        element.children.pop()
      }
      // pop stack
      //stack中删除该模型模型对象,并变更当前的currentParent
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      //关闭
      closeElement(element)
}

The end method does closed-loop processing for the tag element object, deletes the AST model object from the stack, and updates the current parent object.

After the process is completed </ li>, continue to process the tags such as </ ul>, </ div>.

After parseHTML finishes processing all the html characters, it will trace back to the parse method and finally return the root object of the AST model tree.

8. Total

This chapter focuses on the analysis of the parse process. Below we use a flowchart to sort out the entire process

Published 33 original articles · Liked 95 · Visitors 30,000+

Guess you like

Origin blog.csdn.net/tcy83/article/details/88383934